mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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>
|
||||
@@ -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" },
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user