mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Zero client JS, 165 static pages, self-hosted fonts, Otter Orange design system. Includes tool SEO data for all 157 tools, enterprise page, and updated e2e landing tests.
407 lines
16 KiB
Plaintext
407 lines
16 KiB
Plaintext
---
|
|
import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOLS } from "@snapotter/shared";
|
|
import Footer from "@/components/Footer.astro";
|
|
import JsonLd from "@/components/JsonLd.astro";
|
|
import Navbar from "@/components/Navbar.astro";
|
|
import { TOOL_SEO } from "@/data/tool-seo";
|
|
import Base from "@/layouts/Base.astro";
|
|
|
|
export function getStaticPaths() {
|
|
return TOOLS.map((tool) => ({ params: { slug: tool.id }, props: { tool } }));
|
|
}
|
|
|
|
const { tool } = Astro.props;
|
|
|
|
const categoryMap = new Map(CATEGORIES.map((c) => [c.id, c]));
|
|
const category = categoryMap.get(tool.category);
|
|
const seo = TOOL_SEO[tool.id];
|
|
const related = TOOLS.filter((t) => t.category === tool.category && t.id !== tool.id).slice(0, 4);
|
|
const catColor = category?.color ?? "#737373";
|
|
const isAiTool = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id);
|
|
const toolCount = TOOLS.length;
|
|
|
|
const title = seo ? `${seo.searchTitle} | SnapOtter` : `${tool.name} | SnapOtter`;
|
|
const description = seo?.longDescription ?? tool.description;
|
|
|
|
const breadcrumbJsonLd = {
|
|
"@context": "https://schema.org",
|
|
"@type": "BreadcrumbList",
|
|
itemListElement: [
|
|
{ "@type": "ListItem", position: 1, name: "Home", item: "https://snapotter.com" },
|
|
{ "@type": "ListItem", position: 2, name: "Tools", item: "https://snapotter.com/tools" },
|
|
{
|
|
"@type": "ListItem",
|
|
position: 3,
|
|
name: tool.name,
|
|
item: `https://snapotter.com/tools/${tool.id}`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const softwareJsonLd = {
|
|
"@context": "https://schema.org",
|
|
"@type": "WebApplication",
|
|
name: `SnapOtter ${tool.name}`,
|
|
description,
|
|
applicationCategory: "MultimediaApplication",
|
|
operatingSystem: "Linux, macOS, Windows (via Docker)",
|
|
browserRequirements: "Requires JavaScript",
|
|
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
|
license: "https://www.gnu.org/licenses/agpl-3.0.en.html",
|
|
url: `https://snapotter.com/tools/${tool.id}`,
|
|
...(seo?.features && { featureList: seo.features }),
|
|
isPartOf: { "@type": "SoftwareApplication", name: "SnapOtter", url: "https://snapotter.com" },
|
|
};
|
|
|
|
const modalityLabel =
|
|
tool.modality === "image"
|
|
? "image"
|
|
: tool.modality === "video"
|
|
? "video"
|
|
: tool.modality === "audio"
|
|
? "audio"
|
|
: "file";
|
|
const uploadStep =
|
|
tool.modality === "image"
|
|
? "Drag and drop your image file or click to browse. Supports JPEG, PNG, WebP, AVIF, TIFF, HEIC, and RAW formats."
|
|
: tool.modality === "video"
|
|
? "Drag and drop your video file or click to browse. Supports MP4, MOV, AVI, MKV, WebM, and more."
|
|
: tool.modality === "audio"
|
|
? "Drag and drop your audio file or click to browse. Supports MP3, WAV, FLAC, AAC, OGG, and more."
|
|
: "Drag and drop your file or click to browse. Supports a wide range of input formats.";
|
|
|
|
const howToJsonLd = {
|
|
"@context": "https://schema.org",
|
|
"@type": "HowTo",
|
|
name: `How to use ${tool.name} in SnapOtter`,
|
|
description,
|
|
totalTime: "PT1M",
|
|
tool: { "@type": "HowToTool", name: "SnapOtter" },
|
|
step: [
|
|
{
|
|
"@type": "HowToStep",
|
|
position: 1,
|
|
name: "Deploy SnapOtter",
|
|
text: "Run SnapOtter with a single Docker command: docker run -p 1349:1349 snapotter/snapotter",
|
|
},
|
|
{
|
|
"@type": "HowToStep",
|
|
position: 2,
|
|
name: `Open ${tool.name}`,
|
|
text: `Navigate to the ${tool.name} tool in SnapOtter's interface.`,
|
|
},
|
|
{
|
|
"@type": "HowToStep",
|
|
position: 3,
|
|
name: `Upload your ${modalityLabel}`,
|
|
text: uploadStep,
|
|
},
|
|
{
|
|
"@type": "HowToStep",
|
|
position: 4,
|
|
name: "Adjust settings and process",
|
|
text: `Configure ${tool.name} settings to your needs, then click process. Download your result or continue with another tool.`,
|
|
},
|
|
],
|
|
};
|
|
|
|
const faqJsonLd =
|
|
seo?.faqs && seo.faqs.length > 0
|
|
? {
|
|
"@context": "https://schema.org",
|
|
"@type": "FAQPage",
|
|
mainEntity: seo.faqs.map((faq) => ({
|
|
"@type": "Question",
|
|
name: faq.q,
|
|
acceptedAnswer: { "@type": "Answer", text: faq.a },
|
|
})),
|
|
}
|
|
: null;
|
|
|
|
const allJsonLd = [
|
|
breadcrumbJsonLd,
|
|
softwareJsonLd,
|
|
howToJsonLd,
|
|
...(faqJsonLd ? [faqJsonLd] : []),
|
|
];
|
|
---
|
|
|
|
<Base
|
|
title={title}
|
|
description={description}
|
|
canonical={`https://snapotter.com/tools/${tool.id}`}
|
|
>
|
|
<Fragment slot="head">
|
|
<JsonLd data={allJsonLd} />
|
|
</Fragment>
|
|
|
|
<Navbar />
|
|
|
|
<main id="main">
|
|
<!-- Breadcrumb -->
|
|
<nav class="mx-auto max-w-4xl px-6 pt-28 md:pt-36" aria-label="Breadcrumb">
|
|
<ol class="flex items-center gap-2 text-sm text-muted">
|
|
<li>
|
|
<a href="/" class="transition-colors hover:text-foreground">Home</a>
|
|
</li>
|
|
<li aria-hidden="true">/</li>
|
|
<li>
|
|
<a href="/tools" class="transition-colors hover:text-foreground">Tools</a>
|
|
</li>
|
|
<li aria-hidden="true">/</li>
|
|
<li class="font-medium text-foreground">{tool.name}</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<!-- Hero -->
|
|
<section class="mx-auto max-w-4xl px-6 pt-8 pb-16">
|
|
<div class="reveal">
|
|
<div class="mb-6 flex items-center gap-3">
|
|
<div
|
|
class="flex h-14 w-14 items-center justify-center rounded-2xl"
|
|
style={`background-color: ${catColor}14`}
|
|
>
|
|
<svg
|
|
class="h-7 w-7"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke={catColor}
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<rect width="14" height="14" x="5" y="5" rx="2" />
|
|
</svg>
|
|
</div>
|
|
<span
|
|
class="rounded-full px-3 py-1 text-xs font-semibold"
|
|
style={`background-color: ${catColor}14; color: ${catColor}`}
|
|
>
|
|
{category?.name}
|
|
</span>
|
|
{isAiTool && (
|
|
<span class="rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700">
|
|
AI-Powered
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<h1 class="font-display text-4xl font-bold tracking-tight md:text-5xl">
|
|
{tool.name}
|
|
</h1>
|
|
<p class="mt-4 text-lg leading-relaxed text-muted md:text-xl">
|
|
{description}
|
|
</p>
|
|
|
|
<div class="mt-8 flex flex-wrap gap-4">
|
|
<a
|
|
href="https://docs.snapotter.com/guide/getting-started"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="btn-primary inline-flex items-center gap-2 rounded-lg px-6 py-3 text-sm font-semibold"
|
|
>
|
|
<svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 12.5V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v12c0 1.1.9 2 2 2h7.5" />
|
|
<path d="m22 17-5 5-5-5" />
|
|
<path d="M17 22V12" />
|
|
</svg>
|
|
Deploy with Docker
|
|
</a>
|
|
<a
|
|
href="https://github.com/snapotter-hq/snapotter"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="inline-flex items-center gap-2 rounded-lg border border-border px-6 py-3 text-sm font-semibold transition-colors hover:bg-background-alt"
|
|
>
|
|
<svg class="h-[18px] w-[18px]" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
|
</svg>
|
|
View Source
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Features -->
|
|
{seo?.features && seo.features.length > 0 && (
|
|
<section class="border-t border-border bg-background-alt py-16">
|
|
<div class="mx-auto max-w-4xl px-6">
|
|
<div class="reveal">
|
|
<div class="mb-6 flex items-center gap-2">
|
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke={catColor} stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
|
</svg>
|
|
<h2 class="font-display text-2xl font-bold tracking-tight md:text-3xl">
|
|
Features
|
|
</h2>
|
|
</div>
|
|
<ul class="grid gap-3 sm:grid-cols-2">
|
|
{seo.features.map((feature) => (
|
|
<li class="flex gap-3">
|
|
<svg class="mt-0.5 h-[18px] w-[18px] shrink-0" viewBox="0 0 24 24" fill="none" stroke={catColor} stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
<polyline points="22 4 12 14.01 9 11.01" />
|
|
</svg>
|
|
<span class="text-sm leading-relaxed">{feature}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<!-- Use Cases -->
|
|
{seo?.useCases && seo.useCases.length > 0 && (
|
|
<section class="py-16">
|
|
<div class="mx-auto max-w-4xl px-6">
|
|
<div class="reveal">
|
|
<h2 class="font-display text-2xl font-bold tracking-tight md:text-3xl">
|
|
What you can do
|
|
</h2>
|
|
<ul class="mt-8 grid gap-4 sm:grid-cols-2">
|
|
{seo.useCases.map((useCase) => (
|
|
<li class="flex gap-3">
|
|
<svg class="mt-1 h-4 w-4 shrink-0 text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
|
</svg>
|
|
<span class="text-sm leading-relaxed text-muted">{useCase}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<!-- Self-Hosted Callout -->
|
|
<section class="border-t border-border bg-background-alt py-16">
|
|
<div class="mx-auto max-w-4xl px-6">
|
|
<div class="reveal">
|
|
<div class="flex items-start gap-4">
|
|
<svg class="mt-1 h-6 w-6 shrink-0 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect width="20" height="8" x="2" y="2" rx="2" ry="2" />
|
|
<rect width="20" height="8" x="2" y="14" rx="2" ry="2" />
|
|
<line x1="6" x2="6.01" y1="6" y2="6" />
|
|
<line x1="6" x2="6.01" y1="18" y2="18" />
|
|
</svg>
|
|
<div>
|
|
<h2 class="font-display text-xl font-bold">
|
|
{isAiTool
|
|
? "AI that runs on your hardware. No cloud APIs, no usage limits."
|
|
: "Self-hosted. Your files never leave your network."}
|
|
</h2>
|
|
<p class="mt-2 text-sm leading-relaxed text-muted">
|
|
{isAiTool
|
|
? `Unlike cloud AI services, SnapOtter's ${tool.name} runs the ML model directly on your server. Your files are processed locally with no data sent to external APIs. No per-file fees, no rate limits, no privacy concerns. Deploy once with Docker and use it as much as you need.`
|
|
: `SnapOtter runs entirely on your own infrastructure. Files processed with ${tool.name} are never uploaded to third-party servers. Deploy a single Docker container and process files with full privacy, no watermarks, and no usage limits. Open source under AGPL-3.0.`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- FAQs -->
|
|
{seo?.faqs && seo.faqs.length > 0 && (
|
|
<section class="py-16">
|
|
<div class="mx-auto max-w-4xl px-6">
|
|
<div class="reveal">
|
|
<h2 class="font-display text-2xl font-bold tracking-tight md:text-3xl">
|
|
Frequently asked questions
|
|
</h2>
|
|
<dl class="mt-8 space-y-6">
|
|
{seo.faqs.map((faq) => (
|
|
<div class="rounded-xl border border-border bg-surface p-6">
|
|
<dt class="text-sm font-semibold">{faq.q}</dt>
|
|
<dd class="mt-3 text-sm leading-relaxed text-muted">{faq.a}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<!-- Related Tools -->
|
|
{related.length > 0 && (
|
|
<section class="border-t border-border bg-background-alt py-16">
|
|
<div class="mx-auto max-w-4xl px-6">
|
|
<div class="reveal">
|
|
<h2 class="font-display text-2xl font-bold tracking-tight">
|
|
More {category?.name} tools
|
|
</h2>
|
|
<div class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
{related.map((t) => {
|
|
const relCat = categoryMap.get(t.category);
|
|
const relColor = relCat?.color ?? "#737373";
|
|
return (
|
|
<a
|
|
href={`/tools/${t.id}`}
|
|
class="group flex flex-col rounded-xl border border-border bg-surface p-5 transition-all hover:border-primary hover:shadow-md"
|
|
>
|
|
<svg
|
|
class="h-6 w-6"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke={relColor}
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
>
|
|
<rect width="14" height="14" x="5" y="5" rx="2" />
|
|
</svg>
|
|
<span class="mt-3 text-sm font-bold">{t.name}</span>
|
|
<p class="mt-1.5 text-xs leading-relaxed text-muted">{t.description}</p>
|
|
<span class="mt-auto inline-flex items-center gap-1 pt-4 text-xs font-medium text-primary transition-all group-hover:gap-2">
|
|
Learn more
|
|
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
|
</svg>
|
|
</span>
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
<!-- Bottom CTA -->
|
|
<section class="py-20">
|
|
<div class="mx-auto max-w-4xl px-6 text-center">
|
|
<div class="reveal">
|
|
<h2 class="font-display text-3xl font-bold tracking-tight">
|
|
Ready to try {tool.name}?
|
|
</h2>
|
|
<p class="mx-auto mt-4 max-w-lg text-muted">
|
|
Deploy SnapOtter in under a minute. All {toolCount} tools included. Open source and free forever.
|
|
</p>
|
|
<div class="mt-8 flex flex-wrap justify-center gap-4">
|
|
<a
|
|
href="https://docs.snapotter.com/guide/getting-started"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="btn-primary inline-flex items-center gap-2 rounded-lg px-6 py-3 text-sm font-semibold"
|
|
>
|
|
Get Started
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M5 12h14" /><path d="m12 5 7 7-7 7" />
|
|
</svg>
|
|
</a>
|
|
<a
|
|
href="/#pricing"
|
|
class="inline-flex items-center gap-2 rounded-lg border border-border px-6 py-3 text-sm font-semibold transition-colors hover:bg-background-alt"
|
|
>
|
|
View Pricing
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<Footer />
|
|
</Base>
|