mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Search Console flagged four reasons on 2026-07-28: soft 404, both duplicate-canonical variants, and noindex. All four sat on docs.snapotter.com; the landing site was clean. The cause was boilerplate dominance rather than a broken tag. /tools/video/crop-video carries 1.3 KB of unique body against 3.5 KB of identical chrome (nav, sidebar, 21-language switcher), so unrelated tool docs measured 52-60% full-page similarity. Across 20 locales that was 3,640 of 3,822 submitted URLs. Google read the lot as one duplicate cluster and began electing arbitrary representatives: /changelog became the canonical for /tools/image/favicon, and /uk/guide/getting-started for /nl/tools/image/resize. English tool docs indexed 2 of 10 and localized 4 of 10, while the landing page for those same five tools indexed 5 of 5. Translated pages now emit `noindex, follow` with a self-canonical, and sitemap.transformItems drops them from the sitemap. Self-canonical rather than pointing at English, since noindex paired with a cross-canonical sends two conflicting instructions. hreflang is removed outright: the annotation only means something between pages that can all be indexed. Readers see no change. The language switcher and every in-page link behave exactly as before. Verified against a real build: sitemap 3,822 to 182 URLs with zero translated entries, all 3,640 translated files carrying the noindex and no English file doing so, docs e2e 100 passed.
635 lines
30 KiB
TypeScript
635 lines
30 KiB
TypeScript
import { defineConfig } from "vitepress";
|
|
import llmstxt from "vitepress-plugin-llms";
|
|
import { pagefindPlugin } from "vitepress-plugin-pagefind";
|
|
import pkg from "../../../package.json";
|
|
import { SUPPORTED_LOCALES } from "../../../packages/shared/src/i18n/index.ts";
|
|
import { t } from "./i18n/ui.mjs";
|
|
import { pageOnlySidebar } from "./llms-sidebar.mjs";
|
|
|
|
const NON_EN = SUPPORTED_LOCALES.filter((l) => l.code !== "en");
|
|
const HOSTNAME = "https://docs.snapotter.com";
|
|
|
|
// Matches a path whose first segment is one of the translated locales, with or
|
|
// without a leading slash (VitePress hands transformItems a relative page path,
|
|
// but the sitemap stream is happy either way).
|
|
const TRANSLATED_PREFIX = new RegExp(`^/?(${NON_EN.map((l) => l.code).join("|")})(/|$)`);
|
|
|
|
// Prefix every `link` in a sidebar/nav tree with /<locale>.
|
|
// biome-ignore lint/suspicious/noExplicitAny: VitePress nav/sidebar item trees are recursively typed.
|
|
function prefixLinks(items: any[], locale: string): any[] {
|
|
return (items ?? []).map((it) => {
|
|
const next = { ...it };
|
|
if (typeof next.link === "string" && next.link.startsWith("/")) {
|
|
next.link = `/${locale}${next.link}`;
|
|
}
|
|
if (Array.isArray(next.items)) next.items = prefixLinks(next.items, locale);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
// Translate top-level nav labels for a locale via the shared UI catalog. Labels
|
|
// with no mapping (e.g. the version item) pass through unchanged. The nav is a
|
|
// flat list, so no recursion is needed here.
|
|
const NAV_KEY: Record<string, string> = {
|
|
Guide: "nav.guide",
|
|
Tools: "nav.tools",
|
|
"API Reference": "nav.apiReference",
|
|
Changelog: "nav.changelog",
|
|
};
|
|
// biome-ignore lint/suspicious/noExplicitAny: VitePress nav item trees are recursively typed.
|
|
function localizeNav(items: any[], locale: string): any[] {
|
|
return (items ?? []).map((it) => {
|
|
const next = { ...it };
|
|
const key = NAV_KEY[next.text];
|
|
if (key) next.text = t(locale, key);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
// Structural sidebar labels -> UI-catalog key. Individual tool names (the
|
|
// /tools/ leaves) are intentionally absent, so they stay in English; the
|
|
// acronym-only guide labels (OIDC / SSO, SAML SSO) are absent for the same
|
|
// reason. Reuses nav.* / home.mod.* / home.card.* keys where the label matches.
|
|
const SIDEBAR_KEY: Record<string, string> = {
|
|
Guide: "nav.guide",
|
|
Tools: "nav.tools",
|
|
Image: "home.mod.image",
|
|
Video: "home.mod.video",
|
|
Audio: "home.mod.audio",
|
|
PDF: "home.mod.pdf",
|
|
Files: "home.mod.files",
|
|
Essentials: "sidebar.cat.essentials",
|
|
Optimization: "sidebar.cat.optimization",
|
|
Adjustments: "sidebar.cat.adjustments",
|
|
"Watermark & Overlay": "sidebar.cat.watermarkOverlay",
|
|
Utilities: "sidebar.cat.utilities",
|
|
Layout: "sidebar.cat.layout",
|
|
Format: "sidebar.cat.format",
|
|
"AI Tools": "sidebar.cat.aiTools",
|
|
"API reference": "sidebar.sec.apiReference",
|
|
Project: "sidebar.sec.project",
|
|
"REST API": "home.card.restApi",
|
|
"Image engine": "sidebar.page.imageEngine",
|
|
"AI engine": "sidebar.page.aiEngine",
|
|
Changelog: "nav.changelog",
|
|
"Getting started": "sidebar.guide.gettingStarted",
|
|
Architecture: "sidebar.guide.architecture",
|
|
Configuration: "sidebar.guide.configuration",
|
|
"SCIM Provisioning": "sidebar.guide.scimProvisioning",
|
|
"Users, Roles & Permissions": "sidebar.guide.usersRolesPermissions",
|
|
Database: "sidebar.guide.database",
|
|
"Upgrading from 1.x": "sidebar.guide.upgrading",
|
|
Deployment: "sidebar.guide.deployment",
|
|
"Security & Hardening": "sidebar.guide.securityHardening",
|
|
"What SnapOtter collects": "sidebar.guide.telemetry",
|
|
"Supported Formats": "sidebar.guide.supportedFormats",
|
|
"Hardware requirements": "sidebar.guide.hardware",
|
|
"Docker tags": "sidebar.guide.dockerTags",
|
|
"Developer guide": "sidebar.guide.developer",
|
|
"Translation guide": "sidebar.guide.translations",
|
|
Contributing: "sidebar.guide.contributing",
|
|
};
|
|
|
|
// Translate a locale's sidebar tree. Only "structural" nodes are eligible:
|
|
// group headers (anything with child items) and non-tool doc pages (/guide/,
|
|
// /api/, /changelog). Tool leaves (/tools/) never qualify, so a tool whose name
|
|
// happens to collide with a structural label is still left in English.
|
|
// biome-ignore lint/suspicious/noExplicitAny: VitePress sidebar item trees are recursively typed.
|
|
function localizeSidebar(items: any[], locale: string): any[] {
|
|
return (items ?? []).map((it) => {
|
|
const next = { ...it };
|
|
const link = typeof next.link === "string" ? next.link : "";
|
|
const structural =
|
|
Array.isArray(next.items) ||
|
|
link.startsWith("/guide/") ||
|
|
link.startsWith("/api/") ||
|
|
link === "/changelog";
|
|
if (structural) {
|
|
const key = SIDEBAR_KEY[next.text];
|
|
if (key) next.text = t(locale, key);
|
|
}
|
|
if (Array.isArray(next.items)) next.items = localizeSidebar(next.items, locale);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
export default defineConfig({
|
|
title: "SnapOtter",
|
|
description:
|
|
"Documentation for SnapOtter - A Self-Hosted File Manipulation Suite. 200+ tools for image, video, audio, PDF, and file processing. Local AI, pipelines, REST API.",
|
|
base: "/",
|
|
appearance: { initialValue: "light" },
|
|
srcDir: ".",
|
|
outDir: "./.vitepress/dist",
|
|
ignoreDeadLinks: [/localhost/],
|
|
|
|
// Extension-less URLs in both internal links and the sitemap, so they match
|
|
// the clean canonical/hreflang emitted in transformHead below. Without this
|
|
// the sitemap listed .html URLs that Cloudflare Pages 308-redirects to the
|
|
// clean form, so Google indexed the redirecting variant and overrode our
|
|
// canonical (GSC "Duplicate, Google chose different canonical"). CF Pages
|
|
// serves the clean URL at 200, so no extra server config is needed.
|
|
cleanUrls: true,
|
|
|
|
// Only the English tree is submitted. The translated trees are noindexed in
|
|
// transformHead below, and submitting pages we ask Google not to index is a
|
|
// contradiction that shows up in Search Console as sitemap coverage errors.
|
|
// Drops the submitted count from 3,822 to 182.
|
|
sitemap: {
|
|
hostname: HOSTNAME,
|
|
transformItems: (items) => items.filter((item) => !TRANSLATED_PREFIX.test(item.url)),
|
|
},
|
|
|
|
// VitePress defaults to shiki's `github-light`, whose comment (#6a737d, 4.45:1)
|
|
// and string (#22863a, 4.28:1) tokens both miss AA against the code-block
|
|
// background. The high-contrast variant is built for exactly this and is bundled
|
|
// with shiki already. Dark mode was never the failing side.
|
|
markdown: {
|
|
theme: { light: "github-light-high-contrast", dark: "github-dark" },
|
|
},
|
|
|
|
head: [
|
|
["meta", { name: "theme-color", content: "#E07832" }],
|
|
[
|
|
"link",
|
|
{
|
|
rel: "preload",
|
|
href: "/fonts/bricolage-grotesque-var.woff2",
|
|
as: "font",
|
|
type: "font/woff2",
|
|
crossorigin: "",
|
|
},
|
|
],
|
|
["link", { rel: "icon", type: "image/png", sizes: "48x48", href: "/favicon.png" }],
|
|
["link", { rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
|
|
["link", { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" }],
|
|
["link", { rel: "llms-txt", href: "/llms.txt" }],
|
|
["meta", { property: "og:type", content: "website" }],
|
|
["meta", { property: "og:site_name", content: "SnapOtter Docs" }],
|
|
["meta", { property: "og:image", content: "https://docs.snapotter.com/og-image.png" }],
|
|
["meta", { property: "og:image:width", content: "1280" }],
|
|
["meta", { property: "og:image:height", content: "640" }],
|
|
["meta", { property: "og:image:alt", content: "SnapOtter - Self-Hosted File Processing" }],
|
|
["meta", { name: "twitter:card", content: "summary_large_image" }],
|
|
["meta", { name: "twitter:site", content: "@SnapOtterHQ" }],
|
|
["meta", { name: "twitter:image", content: "https://docs.snapotter.com/og-image.png" }],
|
|
],
|
|
|
|
transformHead({ pageData }) {
|
|
const head: Array<[string, Record<string, string>]> = [];
|
|
const rel = pageData.relativePath.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, "");
|
|
const codes = NON_EN.map((l) => l.code);
|
|
const firstSeg = rel.split("/")[0];
|
|
const isLocale = codes.includes(firstSeg);
|
|
const enRel = isLocale ? rel.split("/").slice(1).join("/") : rel;
|
|
const current = isLocale ? firstSeg : "en";
|
|
|
|
const urlFor = (code: string) =>
|
|
code === "en" ? `${HOSTNAME}/${enRel}` : `${HOSTNAME}/${code}/${enRel}`;
|
|
|
|
head.push(["link", { rel: "canonical", href: urlFor(current) }]);
|
|
|
|
// The translated trees are machine translations of pages whose unique body is
|
|
// already small next to the shared chrome (nav, sidebar, 21-language switcher).
|
|
// At 20 locales x 182 pages Google read the lot as one duplicate cluster and
|
|
// began electing arbitrary representatives across languages: it picked
|
|
// /changelog as the canonical for /tools/image/favicon, and
|
|
// /uk/guide/getting-started for /nl/tools/image/resize. Most were dropped, and
|
|
// carrying 3,640 pages it refused to index starved the English ones of crawl
|
|
// budget.
|
|
//
|
|
// Self-canonical rather than a cross-canonical to English: pairing noindex with
|
|
// a canonical pointing elsewhere sends two conflicting instructions. "follow"
|
|
// keeps the outbound links live. Readers lose nothing, since the language
|
|
// switcher and every in-page link behave exactly as before.
|
|
if (isLocale) {
|
|
head.push(["meta", { name: "robots", content: "noindex, follow" }]);
|
|
}
|
|
|
|
// No hreflang. The annotation only means something between pages that can all
|
|
// be indexed, and English is now the only one; pointing it at noindexed URLs is
|
|
// ignored at best. Restore this alongside the noindex above if the translated
|
|
// trees ever become indexable again.
|
|
const ogLocale = current === "en" ? "en_US" : current.replace("-", "_");
|
|
head.push(["meta", { property: "og:locale", content: ogLocale }]);
|
|
head.push(["meta", { property: "og:url", content: urlFor(current) }]);
|
|
head.push(["meta", { property: "og:title", content: pageData.title }]);
|
|
if (pageData.description) {
|
|
head.push(["meta", { property: "og:description", content: pageData.description }]);
|
|
head.push(["meta", { name: "twitter:description", content: pageData.description }]);
|
|
}
|
|
head.push(["meta", { name: "twitter:title", content: pageData.title }]);
|
|
return head;
|
|
},
|
|
|
|
vite: {
|
|
build: {
|
|
rollupOptions: {
|
|
onwarn(warning, defaultHandler) {
|
|
if (
|
|
warning.code === "INVALID_ANNOTATION" &&
|
|
warning.id?.includes("@vueuse/core/dist/index.js")
|
|
) {
|
|
return;
|
|
}
|
|
defaultHandler(warning);
|
|
},
|
|
},
|
|
},
|
|
plugins: [
|
|
pagefindPlugin({
|
|
btnPlaceholder: "Search",
|
|
placeholder: "Search tools, guides, and the API…",
|
|
emptyText: "No matches found. Try a different term or check the spelling.",
|
|
heading: "{{searchResult}} results",
|
|
// Show a few sub-section matches per page so deep-linked headings surface.
|
|
pageResultCount: 3,
|
|
// Per-locale placeholders. No forceLanguage: Pagefind auto-detects per
|
|
// <html lang> (VitePress sets it per locale) and langReload loads the
|
|
// right index on switch. Strings stay English until Plan 05 localizes them.
|
|
locales: Object.fromEntries(
|
|
NON_EN.map((l) => [
|
|
l.code,
|
|
{
|
|
btnPlaceholder: "Search",
|
|
placeholder: "Search tools, guides, and the API…",
|
|
emptyText: "No matches found. Try a different term or check the spelling.",
|
|
heading: "{{searchResult}} results",
|
|
pageResultCount: 3,
|
|
},
|
|
]),
|
|
),
|
|
}),
|
|
llmstxt({
|
|
domain: "https://docs.snapotter.com",
|
|
sidebar: pageOnlySidebar,
|
|
customLLMsTxtTemplate: `# {title}
|
|
|
|
{description}
|
|
|
|
{details}
|
|
|
|
## Docs
|
|
|
|
{toc}
|
|
|
|
## API Quick Reference
|
|
|
|
- Base URL: \`http://localhost:1349\`
|
|
- Auth: Session token via \`POST /api/auth/login\` or API key (\`Authorization: Bearer si_...\`)
|
|
- Tools: \`POST /api/v1/tools/{section}/{toolId}\` (multipart: file + settings JSON)
|
|
- Batch: \`POST /api/v1/tools/{section}/{toolId}/batch\` (multiple files, returns ZIP)
|
|
- Pipelines: \`POST /api/v1/pipeline/execute\` (chain tools sequentially)
|
|
- Interactive API docs on running instance: \`/api/docs\`
|
|
- OpenAPI spec on running instance: \`/api/v1/openapi.yaml\`
|
|
|
|
## Source
|
|
|
|
- [GitHub](https://github.com/snapotter-hq/snapotter)
|
|
- License: AGPLv3 (commercial license also available)
|
|
`,
|
|
customTemplateVariables: {
|
|
description:
|
|
"SnapOtter is a self-hosted, open-source file processing platform with 200+ tools across image, video, audio, PDF, and files. Includes AI/ML tools. Runs via Docker Compose with GPU auto-detection.",
|
|
details:
|
|
"Process images (resize, compress, convert, remove backgrounds, upscale, OCR), videos (trim, merge, subtitles), audio (normalize, transcribe, convert), PDFs (merge, split, watermark, redact), and files (CSV, JSON, XML conversion) - without sending files to external services.",
|
|
},
|
|
}),
|
|
],
|
|
},
|
|
|
|
themeConfig: buildBaseTheme(),
|
|
|
|
locales: {
|
|
root: { label: "English", lang: "en" },
|
|
...Object.fromEntries(
|
|
NON_EN.map((l) => [
|
|
l.code,
|
|
{
|
|
label: l.nativeName,
|
|
lang: l.code,
|
|
dir: l.dir,
|
|
link: `/${l.code}/`,
|
|
themeConfig: {
|
|
nav: prefixLinks(localizeNav(buildBaseTheme().nav, l.code), l.code),
|
|
sidebar: prefixLinks(localizeSidebar(buildBaseTheme().sidebar, l.code), l.code),
|
|
editLink: {
|
|
pattern: "https://github.com/snapotter-hq/snapotter/edit/main/apps/docs/:path",
|
|
text: t(l.code, "sidebar.editLink"),
|
|
},
|
|
},
|
|
},
|
|
]),
|
|
),
|
|
},
|
|
});
|
|
|
|
// Function declaration (hoisted) so defineConfig above can call it while the
|
|
// nav/sidebar/footer tree stays defined once. Root uses it verbatim; each locale
|
|
// gets a /<locale>-prefixed copy of nav + sidebar via prefixLinks().
|
|
function buildBaseTheme() {
|
|
return {
|
|
logo: "/logo.png",
|
|
|
|
nav: [
|
|
// No "Home" item: the logo already links home, and dropping it keeps the
|
|
// nav within the viewport at 768px (see github-stars.css for the wider
|
|
// responsive-nav fix).
|
|
{ text: "Guide", link: "/guide/getting-started" },
|
|
{ text: "Tools", link: "/tools/image/resize" },
|
|
{ text: "API Reference", link: "/api/rest" },
|
|
{ text: "Changelog", link: "/changelog" },
|
|
{
|
|
text: `v${pkg.version}`,
|
|
link: "/changelog",
|
|
},
|
|
],
|
|
|
|
sidebar: [
|
|
{
|
|
text: "Guide",
|
|
items: [
|
|
{ text: "Getting started", link: "/guide/getting-started" },
|
|
{ text: "Architecture", link: "/guide/architecture" },
|
|
{ text: "Configuration", link: "/guide/configuration" },
|
|
{ text: "OIDC / SSO", link: "/guide/oidc" },
|
|
{ text: "SAML SSO", link: "/guide/saml" },
|
|
{ text: "SCIM Provisioning", link: "/guide/scim" },
|
|
{ text: "Users, Roles & Permissions", link: "/guide/users-roles" },
|
|
{ text: "Database", link: "/guide/database" },
|
|
{ text: "Upgrading from 1.x", link: "/guide/upgrading" },
|
|
{ text: "Deployment", link: "/guide/deployment" },
|
|
{ text: "Security & Hardening", link: "/guide/security" },
|
|
{ text: "What SnapOtter collects", link: "/guide/telemetry" },
|
|
{ text: "Supported Formats", link: "/guide/supported-formats" },
|
|
{ text: "Hardware requirements", link: "/guide/deployment#hardware-requirements" },
|
|
{ text: "Low-resource setups", link: "/guide/low-resource" },
|
|
{ text: "Docker tags", link: "/guide/docker-tags" },
|
|
{ text: "Developer guide", link: "/guide/developer" },
|
|
{ text: "Translation guide", link: "/guide/translations" },
|
|
{ text: "Contributing", link: "/guide/contributing" },
|
|
],
|
|
},
|
|
{
|
|
text: "Tools",
|
|
items: [
|
|
{ text: "Conversion Presets", link: "/tools/conversion-presets" },
|
|
{
|
|
text: "Image",
|
|
collapsed: false,
|
|
items: [
|
|
{
|
|
text: "Essentials",
|
|
items: [
|
|
{ text: "Resize Image", link: "/tools/image/resize" },
|
|
{ text: "Crop Image", link: "/tools/image/crop" },
|
|
{ text: "Rotate & Flip Image", link: "/tools/image/rotate" },
|
|
{ text: "Convert Image", link: "/tools/image/convert" },
|
|
{ text: "Compress Image", link: "/tools/image/compress" },
|
|
],
|
|
},
|
|
{
|
|
text: "Optimization",
|
|
items: [
|
|
{ text: "Optimize for Web", link: "/tools/image/optimize-for-web" },
|
|
{ text: "Remove Image Metadata", link: "/tools/image/strip-metadata" },
|
|
{ text: "Edit Image Metadata", link: "/tools/image/edit-metadata" },
|
|
{ text: "Bulk Rename", link: "/tools/image/bulk-rename" },
|
|
{ text: "Image to PDF", link: "/tools/image/image-to-pdf" },
|
|
{ text: "Favicon Generator", link: "/tools/image/favicon" },
|
|
],
|
|
},
|
|
{
|
|
text: "Adjustments",
|
|
items: [
|
|
{ text: "Adjust Colors", link: "/tools/image/adjust-colors" },
|
|
{ text: "Sharpen Image", link: "/tools/image/sharpening" },
|
|
{ text: "Replace & Invert Color", link: "/tools/image/replace-color" },
|
|
{ text: "Color Blindness Simulation", link: "/tools/image/color-blindness" },
|
|
{ text: "Duotone", link: "/tools/image/duotone" },
|
|
{ text: "Pixelate", link: "/tools/image/pixelate" },
|
|
{ text: "Vignette", link: "/tools/image/vignette" },
|
|
],
|
|
},
|
|
{
|
|
text: "Watermark & Overlay",
|
|
items: [
|
|
{ text: "Text Watermark", link: "/tools/image/watermark-text" },
|
|
{ text: "Image Watermark", link: "/tools/image/watermark-image" },
|
|
{ text: "Text Overlay", link: "/tools/image/text-overlay" },
|
|
{ text: "Image Composition", link: "/tools/image/compose" },
|
|
{ text: "Meme Generator", link: "/tools/image/meme-generator" },
|
|
],
|
|
},
|
|
{
|
|
text: "Utilities",
|
|
items: [
|
|
{ text: "Image Info", link: "/tools/image/info" },
|
|
{ text: "Image Compare", link: "/tools/image/compare" },
|
|
{ text: "Find Duplicates", link: "/tools/image/find-duplicates" },
|
|
{ text: "Color Palette", link: "/tools/image/color-palette" },
|
|
{ text: "QR Code Generator", link: "/tools/image/qr-generate" },
|
|
{ text: "HTML to Image", link: "/tools/image/html-to-image" },
|
|
{ text: "Barcode Reader", link: "/tools/image/barcode-read" },
|
|
{ text: "Image to Base64", link: "/tools/image/image-to-base64" },
|
|
{ text: "Histogram", link: "/tools/image/histogram" },
|
|
{ text: "LQIP Placeholder", link: "/tools/image/lqip-placeholder" },
|
|
{ text: "Barcode Generator", link: "/tools/image/barcode-generate" },
|
|
],
|
|
},
|
|
{
|
|
text: "Layout",
|
|
items: [
|
|
{ text: "Collage & Grid", link: "/tools/image/collage" },
|
|
{ text: "Stitch Images", link: "/tools/image/stitch" },
|
|
{ text: "Split Image", link: "/tools/image/split" },
|
|
{ text: "Border & Frame", link: "/tools/image/border" },
|
|
{ text: "Beautify Screenshot", link: "/tools/image/beautify" },
|
|
{ text: "Circle Crop", link: "/tools/image/circle-crop" },
|
|
{ text: "Image Pad", link: "/tools/image/image-pad" },
|
|
{ text: "Sprite Sheet", link: "/tools/image/sprite-sheet" },
|
|
],
|
|
},
|
|
{
|
|
text: "Format",
|
|
items: [
|
|
{ text: "SVG to Raster", link: "/tools/image/svg-to-raster" },
|
|
{ text: "Image to SVG", link: "/tools/image/vectorize" },
|
|
{ text: "GIF Tools", link: "/tools/image/gif-tools" },
|
|
{ text: "GIF/WebP Converter", link: "/tools/image/gif-webp" },
|
|
],
|
|
},
|
|
{
|
|
text: "AI Tools",
|
|
items: [
|
|
{ text: "Remove Background", link: "/tools/image/remove-background" },
|
|
{ text: "Image Upscaling", link: "/tools/image/upscale" },
|
|
{ text: "Object Eraser", link: "/tools/image/erase-object" },
|
|
{ text: "Extract Text from Image (OCR)", link: "/tools/image/ocr" },
|
|
{ text: "Blur Faces & PII", link: "/tools/image/blur-faces" },
|
|
{ text: "Smart Crop", link: "/tools/image/smart-crop" },
|
|
{ text: "Image Enhancement", link: "/tools/image/image-enhancement" },
|
|
{ text: "Face Enhancement", link: "/tools/image/enhance-faces" },
|
|
{ text: "AI Colorization", link: "/tools/image/colorize" },
|
|
{ text: "Noise Removal", link: "/tools/image/noise-removal" },
|
|
{ text: "Red Eye Removal", link: "/tools/image/red-eye-removal" },
|
|
{ text: "Photo Restoration", link: "/tools/image/restore-photo" },
|
|
{ text: "Passport Photo", link: "/tools/image/passport-photo" },
|
|
{ text: "Content-Aware Resize", link: "/tools/image/content-aware-resize" },
|
|
{ text: "AI Canvas Expand", link: "/tools/image/ai-canvas-expand" },
|
|
{ text: "PNG Transparency Fixer", link: "/tools/image/transparency-fixer" },
|
|
{ text: "Background Replace", link: "/tools/image/background-replace" },
|
|
{ text: "Blur Background", link: "/tools/image/blur-background" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
text: "Video",
|
|
items: [
|
|
{ text: "Convert Video", link: "/tools/video/convert-video" },
|
|
{ text: "Compress Video", link: "/tools/video/compress-video" },
|
|
{ text: "Trim Video", link: "/tools/video/trim-video" },
|
|
{ text: "Mute Video", link: "/tools/video/mute-video" },
|
|
{ text: "Video to GIF", link: "/tools/video/video-to-gif" },
|
|
{ text: "Resize Video", link: "/tools/video/resize-video" },
|
|
{ text: "Crop Video", link: "/tools/video/crop-video" },
|
|
{ text: "Rotate Video", link: "/tools/video/rotate-video" },
|
|
{ text: "Change FPS", link: "/tools/video/change-fps" },
|
|
{ text: "Video Color", link: "/tools/video/video-color" },
|
|
{ text: "Video Speed", link: "/tools/video/video-speed" },
|
|
{ text: "Reverse Video", link: "/tools/video/reverse-video" },
|
|
{ text: "Normalize Video Audio", link: "/tools/video/video-loudnorm" },
|
|
{ text: "Aspect Pad", link: "/tools/video/aspect-pad" },
|
|
{ text: "Blur Pad", link: "/tools/video/blur-pad" },
|
|
{ text: "Watermark Video", link: "/tools/video/watermark-video" },
|
|
{ text: "Stabilize Video", link: "/tools/video/stabilize-video" },
|
|
{ text: "GIF to Video", link: "/tools/video/gif-to-video" },
|
|
{ text: "Video to WebP", link: "/tools/video/video-to-webp" },
|
|
{ text: "Video to Frames", link: "/tools/video/video-to-frames" },
|
|
{ text: "Merge Videos", link: "/tools/video/merge-videos" },
|
|
{ text: "Replace Audio", link: "/tools/video/replace-audio" },
|
|
{ text: "Burn Subtitles", link: "/tools/video/burn-subtitles" },
|
|
{ text: "Embed Subtitles", link: "/tools/video/embed-subtitles" },
|
|
{ text: "Extract Subtitles", link: "/tools/video/extract-subtitles" },
|
|
{ text: "Images to Video", link: "/tools/video/images-to-video" },
|
|
{ text: "Clean Video Metadata", link: "/tools/video/video-metadata" },
|
|
{ text: "Auto Subtitles", link: "/tools/video/auto-subtitles" },
|
|
{ text: "Extract Audio", link: "/tools/video/extract-audio" },
|
|
],
|
|
},
|
|
{
|
|
text: "Audio",
|
|
items: [
|
|
{ text: "Convert Audio", link: "/tools/audio/convert-audio" },
|
|
{ text: "Trim Audio", link: "/tools/audio/trim-audio" },
|
|
{ text: "Adjust Volume", link: "/tools/audio/volume-adjust" },
|
|
{ text: "Normalize Audio", link: "/tools/audio/normalize-audio" },
|
|
{ text: "Fade Audio", link: "/tools/audio/fade-audio" },
|
|
{ text: "Reverse Audio", link: "/tools/audio/reverse-audio" },
|
|
{ text: "Audio Speed", link: "/tools/audio/audio-speed" },
|
|
{ text: "Pitch Shift", link: "/tools/audio/pitch-shift" },
|
|
{ text: "Audio Channels", link: "/tools/audio/audio-channels" },
|
|
{ text: "Silence Removal", link: "/tools/audio/silence-removal" },
|
|
{ text: "Noise Reduction", link: "/tools/audio/noise-reduction" },
|
|
{ text: "Merge Audio", link: "/tools/audio/merge-audio" },
|
|
{ text: "Split Audio", link: "/tools/audio/split-audio" },
|
|
{ text: "Ringtone Maker", link: "/tools/audio/ringtone-maker" },
|
|
{ text: "Waveform Image", link: "/tools/audio/waveform-image" },
|
|
{ text: "Audio Metadata", link: "/tools/audio/audio-metadata" },
|
|
{ text: "Transcribe Audio", link: "/tools/audio/transcribe-audio" },
|
|
],
|
|
},
|
|
{
|
|
text: "PDF",
|
|
items: [
|
|
{ text: "PDF to Image", link: "/tools/pdf/pdf-to-image" },
|
|
{ text: "Merge PDFs", link: "/tools/pdf/merge-pdf" },
|
|
{ text: "Split PDF", link: "/tools/pdf/split-pdf" },
|
|
{ text: "Compress PDF", link: "/tools/pdf/compress-pdf" },
|
|
{ text: "Rotate PDF", link: "/tools/pdf/rotate-pdf" },
|
|
{ text: "Extract Pages", link: "/tools/pdf/extract-pages" },
|
|
{ text: "Remove Pages", link: "/tools/pdf/remove-pages" },
|
|
{ text: "Organize PDF", link: "/tools/pdf/organize-pdf" },
|
|
{ text: "Protect PDF", link: "/tools/pdf/protect-pdf" },
|
|
{ text: "Unlock PDF", link: "/tools/pdf/unlock-pdf" },
|
|
{ text: "Repair PDF", link: "/tools/pdf/repair-pdf" },
|
|
{ text: "Web-Optimize PDF", link: "/tools/pdf/linearize-pdf" },
|
|
{ text: "Grayscale PDF", link: "/tools/pdf/grayscale-pdf" },
|
|
{ text: "PDF/A Converter", link: "/tools/pdf/pdfa-convert" },
|
|
{ text: "Crop PDF", link: "/tools/pdf/crop-pdf" },
|
|
{ text: "Pages Per Sheet (N-up)", link: "/tools/pdf/nup-pdf" },
|
|
{ text: "Booklet PDF", link: "/tools/pdf/booklet-pdf" },
|
|
{ text: "Watermark PDF", link: "/tools/pdf/watermark-pdf" },
|
|
{ text: "PDF Page Numbers", link: "/tools/pdf/pdf-page-numbers" },
|
|
{ text: "Flatten PDF", link: "/tools/pdf/flatten-pdf" },
|
|
{ text: "Redact PDF", link: "/tools/pdf/redact-pdf" },
|
|
{ text: "Sign PDF", link: "/tools/pdf/sign-pdf" },
|
|
{ text: "PDF to Text", link: "/tools/pdf/pdf-to-text" },
|
|
{ text: "PDF to Word", link: "/tools/pdf/pdf-to-word" },
|
|
{ text: "PDF Metadata", link: "/tools/pdf/pdf-metadata" },
|
|
{ text: "PDF OCR", link: "/tools/pdf/ocr-pdf" },
|
|
],
|
|
},
|
|
{
|
|
text: "Files",
|
|
items: [
|
|
{ text: "Convert Document", link: "/tools/files/convert-document" },
|
|
{ text: "Convert Presentation", link: "/tools/files/convert-presentation" },
|
|
{ text: "Convert Spreadsheet", link: "/tools/files/convert-spreadsheet" },
|
|
{ text: "Excel to PDF", link: "/tools/files/excel-to-pdf" },
|
|
{ text: "Word to PDF", link: "/tools/files/word-to-pdf" },
|
|
{ text: "PowerPoint to PDF", link: "/tools/files/powerpoint-to-pdf" },
|
|
{ text: "HTML to PDF", link: "/tools/files/html-to-pdf" },
|
|
{ text: "Markdown to Word", link: "/tools/files/markdown-to-docx" },
|
|
{ text: "Markdown to HTML", link: "/tools/files/markdown-to-html" },
|
|
{ text: "Markdown to PDF", link: "/tools/files/markdown-to-pdf" },
|
|
{ text: "Convert from EPUB", link: "/tools/files/epub-convert" },
|
|
{ text: "Convert to EPUB", link: "/tools/files/to-epub" },
|
|
{ text: "Chart Maker", link: "/tools/files/chart-maker" },
|
|
{ text: "CSV to Excel", link: "/tools/files/csv-excel" },
|
|
{ text: "CSV to JSON", link: "/tools/files/csv-json" },
|
|
{ text: "JSON to XML", link: "/tools/files/json-xml" },
|
|
{ text: "Split CSV", link: "/tools/files/split-csv" },
|
|
{ text: "Merge CSVs", link: "/tools/files/merge-csvs" },
|
|
{ text: "Convert YAML / JSON", link: "/tools/files/yaml-json" },
|
|
{ text: "XML to CSV", link: "/tools/files/xml-to-csv" },
|
|
{ text: "Create ZIP", link: "/tools/files/create-zip" },
|
|
{ text: "Extract ZIP", link: "/tools/files/extract-zip" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
text: "API reference",
|
|
items: [
|
|
{ text: "REST API", link: "/api/rest" },
|
|
{ text: "Image engine", link: "/api/image-engine" },
|
|
{ text: "AI engine", link: "/api/ai" },
|
|
],
|
|
},
|
|
{
|
|
text: "Project",
|
|
items: [{ text: "Changelog", link: "/changelog" }],
|
|
},
|
|
],
|
|
|
|
footer: {
|
|
message:
|
|
'Released under the <a href="https://github.com/snapotter-hq/snapotter/blob/main/LICENSE">AGPLv3 License</a>.',
|
|
copyright:
|
|
'AI-friendly docs available at <a href="/llms.txt">/llms.txt</a> · <a href="/llms-full.txt">/llms-full.txt</a>',
|
|
},
|
|
|
|
socialLinks: [
|
|
{ icon: "github", link: "https://github.com/snapotter-hq/snapotter" },
|
|
{ icon: "discord", link: "https://discord.gg/hr3s7HPUsr" },
|
|
],
|
|
|
|
editLink: {
|
|
pattern: "https://github.com/snapotter-hq/snapotter/edit/main/apps/docs/:path",
|
|
text: "Edit this page on GitHub",
|
|
},
|
|
};
|
|
}
|