feat: reposition to self-hosted file-processing infrastructure (#469)

Leads public copy with self-hosted file-processing infrastructure and demotes tool count to a proof point across README, docs, llms.txt, DockerHub, and the landing site. Adds a /self-hosted hub plus 7 job-intent SEO pages with a build-time validator, deepens the flagship /alternatives pages, adds a remove.bg page, three shared components, and landing e2e coverage.
This commit is contained in:
SnapOtter
2026-07-10 13:26:41 +08:00
committed by GitHub
parent 331e3bfde0
commit af011d5538
24 changed files with 1051 additions and 31 deletions
@@ -0,0 +1,28 @@
---
// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values.
interface Props {
heading?: string;
body?: string;
}
const {
heading = "Deploying this across a team?",
body = "Audit trails, per-tool permissions, SSO, and air-gapped deployment for regulated environments. Talk to us about running SnapOtter as shared infrastructure.",
} = Astro.props;
---
<section class="border-t border-border bg-background-alt py-16">
<div class="mx-auto max-w-4xl px-6 text-center">
<div class="reveal">
<h2 class="font-display text-2xl font-bold tracking-tight md:text-3xl">{heading}</h2>
<p class="mx-auto mt-4 max-w-xl text-muted">{body}</p>
<div class="mt-8 flex flex-wrap justify-center gap-4">
<a href="/contact" class="btn-primary inline-flex items-center gap-2 rounded-lg px-6 py-3 text-sm font-semibold">
Book a demo
</a>
<a href="/enterprise" 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">
See enterprise features
</a>
</div>
</div>
</div>
</section>
+1 -1
View File
@@ -18,7 +18,7 @@ const columns = [
title: "Solutions",
links: [
{ label: "Enterprise", href: "/#enterprise" },
{ label: "Self-Hosted", href: "/#features" },
{ label: "Self-Hosted", href: "/self-hosted" },
{ label: "Open Source", href: "/#open-source" },
{ label: "Alternatives", href: "/alternatives" },
{ label: "Pricing", href: "/#pricing" },
+4 -4
View File
@@ -35,14 +35,14 @@ const trustBadges = ["Self-hosted", "Open source", "Air-gap capable", "Complianc
))}
</div>
<!-- Headline: one precise promise for privacy-sensitive buyers -->
<!-- Headline: names the buying trigger (the network boundary), not "the cloud" -->
<h1 class="animate-fade-up mt-8 font-display text-4xl font-extrabold tracking-tight text-white sm:text-5xl md:text-6xl lg:text-7xl" style="animation-delay: 0.1s; text-shadow: 0 2px 30px rgba(90, 20, 0, 0.28);">
File processing for privacy-sensitive teams.
Private file processing for data that can't leave your network.
</h1>
<!-- Subtitle: one calm supporting line -->
<!-- Subtitle: capability breadth plus the infrastructure surfaces -->
<p class="animate-fade-up mx-auto mt-6 max-w-2xl text-lg text-white/90 md:text-xl" style="animation-delay: 0.15s; text-shadow: 0 1px 16px rgba(90, 20, 0, 0.22);">
Run image, video, audio, PDF, and file tools with a free layer-based image editor, local AI, batch workflows, and enterprise controls.
Convert, compress, OCR, transcribe, strip metadata, and run local AI across image, video, audio, PDF, and documents. Through a UI, REST API, and pipelines, on infrastructure you own.
</p>
<!-- Hero tool search -->
+1
View File
@@ -10,6 +10,7 @@ const links = [
{ label: "Enterprise", href: "/#enterprise" },
{ label: "Pricing", href: "/#pricing" },
{ label: "Alternatives", href: "/alternatives" },
{ label: "Developers", href: "/self-hosted/file-conversion-api" },
{ label: "Docs", href: "https://docs.snapotter.com", external: true },
{ label: "Talk to a human", href: "/contact" },
];
@@ -0,0 +1,90 @@
---
// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values.
interface Props {
// Canonical single-container command. Keep in sync with HeroQuickstart.astro,
// DOCKERHUB.md, README.md, and apps/docs/guide/getting-started.md.
docker: string;
// Optional REST API example (already newline-formatted).
curl?: string;
dockerLabel?: string;
curlLabel?: string;
}
const {
docker,
curl,
dockerLabel = "Self-host in one command",
curlLabel = "Process a file over the REST API",
} = Astro.props;
const blocks = [
...(docker ? [{ label: dockerLabel, code: docker }] : []),
...(curl ? [{ label: curlLabel, code: curl }] : []),
];
---
<div class="not-prose space-y-5">
{blocks.map((b) => (
<div>
<p class="mb-2 font-mono text-xs font-semibold uppercase tracking-[0.15em] text-primary">
{b.label}
</p>
<button
type="button"
class="so-copy group relative flex w-full items-start gap-3 rounded-xl border border-white/10 bg-terminal-bg px-4 py-3.5 text-start font-mono text-[0.8125rem] leading-[1.7] text-terminal-fg"
data-copy={b.code}
aria-label="Copy this command to your clipboard"
title="Click to copy"
>
<span class="shrink-0 select-none font-bold text-terminal-green">$</span>
<span dir="ltr" class="min-w-0 flex-1 overflow-x-auto">
<code class="whitespace-pre">{b.code}</code>
</span>
<span class="shrink-0 text-terminal-muted transition-colors group-hover:text-primary-light" aria-hidden="true">
<svg class="copy-idle h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<svg class="copy-done hidden h-4 w-4 text-terminal-green" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
</span>
<span class="copy-toast pointer-events-none absolute -top-7 end-3 rounded-md bg-foreground px-2 py-0.5 font-body text-[0.6875rem] font-semibold text-white opacity-0 transition-opacity">Copied</span>
</button>
</div>
))}
</div>
<style>
.so-copy { transition: border-color 0.2s, box-shadow 0.2s; }
.so-copy:hover { border-color: rgba(224, 120, 50, 0.5); }
.so-copy:focus-visible { outline: 2px solid var(--color-primary-light); outline-offset: 3px; }
.so-copy.copied { border-color: rgba(74, 222, 128, 0.55); }
.so-copy.copied .copy-toast { opacity: 1; }
</style>
<script>
document.querySelectorAll(".so-copy").forEach((btn) => {
const idle = btn.querySelector(".copy-idle");
const done = btn.querySelector(".copy-done");
let timer: ReturnType<typeof setTimeout> | undefined;
const flip = (copied: boolean) => {
idle?.classList.toggle("hidden", copied);
done?.classList.toggle("hidden", !copied);
btn.classList.toggle("copied", copied);
};
btn.addEventListener("click", async () => {
const text = btn.getAttribute("data-copy") ?? "";
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch {}
document.body.removeChild(ta);
}
flip(true);
if (timer) clearTimeout(timer);
timer = setTimeout(() => flip(false), 2000);
});
});
</script>
@@ -0,0 +1,21 @@
---
// biome-ignore-all lint/correctness/noUnusedVariables: Astro template consumes frontmatter values.
interface Props {
title: string;
body: string;
}
const { title, body } = Astro.props;
---
<div class="reveal rounded-xl border border-danger/20 bg-danger/5 p-6">
<div class="flex items-start gap-3">
<svg class="mt-0.5 h-5 w-5 shrink-0 text-danger" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<div>
<p class="text-sm font-semibold text-danger">{title}</p>
<p class="mt-2 text-sm leading-relaxed text-muted">{body}</p>
</div>
</div>
</div>
@@ -7,11 +7,6 @@ const starCount = await getStarCount();
const { display: pullsDisplay } = await getImagePulls();
const stats = [
{
value: "200+",
label: "Processing Tools",
sublabel: "Across 5 file modalities",
},
{
value: formatCompact(starCount),
label: "GitHub Stars",
@@ -22,6 +17,11 @@ const stats = [
label: "Image Pulls",
sublabel: "Deployed worldwide",
},
{
value: "200+",
label: "Processing Tools",
sublabel: "Across 5 file modalities",
},
{
value: "20+",
label: "Languages",