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 /. // 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 = { 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 = { 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]> = []; 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 // (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 /-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 AGPLv3 License.', copyright: 'AI-friendly docs available at /llms.txt · /llms-full.txt', }, 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", }, }; }