Files
SnapOtter/apps/docs/.vitepress/config.mts
T
SnapOtterandGitHub e6718ada71 fix(seo): emit clean docs URLs and de-index the demo (#598)
The docs sitemap listed .html URLs that Cloudflare Pages 308-redirects to
their clean form, while every page's canonical/hreflang already pointed at
the clean URL. Google indexed the redirecting .html variant and picked its
own canonical (GSC "Duplicate, Google chose different canonical"), and burned
crawl budget re-fetching redirecting URLs. Setting VitePress cleanUrls:true
makes the sitemap and internal links extension-less so they match the
canonicals. CF Pages already serves the clean URL at 200 and 308s the .html
form, so no hosting change is needed and legacy .html hits consolidate.

Also mark demo.snapotter.com noindex: it mirrors the real app under the same
domain property, so its routes were being crawled and indexed as thin,
duplicate pages.
2026-07-21 15:22:15 +08:00

596 lines
28 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";
const NON_EN = SUPPORTED_LOCALES.filter((l) => l.code !== "en");
const HOSTNAME = "https://docs.snapotter.com";
// 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,
sitemap: { hostname: "https://docs.snapotter.com" },
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) }]);
head.push(["link", { rel: "alternate", hreflang: "x-default", href: urlFor("en") }]);
head.push(["link", { rel: "alternate", hreflang: "en", href: urlFor("en") }]);
for (const l of NON_EN) {
head.push(["link", { rel: "alternate", hreflang: l.code, href: urlFor(l.code) }]);
}
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",
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", link: "/tools/image/resize" },
{ text: "Crop", link: "/tools/image/crop" },
{ text: "Rotate & Flip", link: "/tools/image/rotate" },
{ text: "Convert", link: "/tools/image/convert" },
{ text: "Compress", link: "/tools/image/compress" },
],
},
{
text: "Optimization",
items: [
{ text: "Optimize for Web", link: "/tools/image/optimize-for-web" },
{ text: "Remove Metadata", link: "/tools/image/strip-metadata" },
{ text: "Edit 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: "Sharpening", 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 / Combine", link: "/tools/image/stitch" },
{ text: "Image Splitting", 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: "OCR / Text Extraction", link: "/tools/image/ocr" },
{ text: "Face / PII Blur", 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 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: "Volume Adjust", 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 Convert", link: "/tools/pdf/pdfa-convert" },
{ text: "Crop PDF", link: "/tools/pdf/crop-pdf" },
{ text: "N-up PDF", 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 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: "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",
},
};
}