import fs from "node:fs"; import path from "node:path"; import { defineConfig, type UserConfig } from "vite"; import tsConfigPaths from "vite-tsconfig-paths"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import { cloudflare } from "@cloudflare/vite-plugin"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import { languageAlternates, LOCALES, localizePath } from "./src/i18n/locale"; const repoRoot = path.resolve(__dirname, "../.."); const siteHost = "https://paseo.sh"; const routesDir = path.join(__dirname, "src/routes/{-$locale}"); // A translation such as `cli.zh-CN.md` is the same page as `cli.md` in another language. const TRANSLATION_FILE = /\.[A-Za-z]{2,3}(?:-[A-Za-z0-9]+)*\.md$/; function discoverDocsRoutes(): string[] { const docsDir = path.join(repoRoot, "public-docs"); if (!fs.existsSync(docsDir)) return ["/docs"]; const routes = new Set(["/docs"]); const walk = (dir: string) => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { walk(full); continue; } if (!entry.name.endsWith(".md") || TRANSLATION_FILE.test(entry.name)) continue; const rel = path .relative(docsDir, full) .replace(/\.md$/, "") .replace(/\/index$/, ""); if (rel === "index" || rel === "") continue; routes.add(`/docs/${rel.split(path.sep).join("/")}`); } }; walk(docsDir); return [...routes].sort(); } function discoverAgentRoutes(): string[] { const reserved = new Set([ "route", "agents", "blog", "changelog", "docs", "download", "download_.thanks", "hub", "index", "sponsor", "privacy", "terms", ]); return fs .readdirSync(routesDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith(".tsx")) .map((entry) => entry.name.replace(/\.tsx$/, "")) .filter((name) => !reserved.has(name)) .sort() .map((slug) => `/${slug}`); } function discoverAlternativeRoutes(): string[] { const alternativesDir = path.join(routesDir, "alternatives"); if (!fs.existsSync(alternativesDir)) return []; const slugs = fs .readdirSync(alternativesDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith(".tsx")) .map((entry) => entry.name.replace(/\.tsx$/, "")) .sort(); return ["/alternatives", ...slugs.map((slug) => `/alternatives/${slug}`)]; } function discoverBlogRoutes(): string[] { const postsDir = path.join(__dirname, "posts"); if (!fs.existsSync(postsDir)) return ["/blog"]; const slugs = fs .readdirSync(postsDir, { withFileTypes: true }) .filter((entry) => entry.isFile() && entry.name.endsWith(".md")) .filter((entry) => !TRANSLATION_FILE.test(entry.name)) .map((entry) => entry.name.replace(/\.md$/, "")) .sort(); return ["/blog", ...slugs.map((slug) => `/blog/${slug}`)]; } // Author and detail URLs are served by /sitemap-plugins.xml as the registry changes. function discoverPluginRoutes(): string[] { return ["/plugins"]; } const sitemapPages = [ "/", "/agents", "/changelog", "/download", "/hub", "/privacy", "/terms", ...discoverAgentRoutes(), ...discoverAlternativeRoutes(), ...discoverPluginRoutes(), ...discoverDocsRoutes(), ...discoverBlogRoutes(), ].flatMap((routePath) => { // Every page exists in each language; each URL lists all of them as alternates. const alternateRefs = languageAlternates(routePath).map((alternate) => ({ hreflang: alternate.hreflang, href: `${siteHost}${alternate.path}`, })); return LOCALES.map((locale) => ({ path: localizePath(routePath, locale), sitemap: { alternateRefs }, })); }); export default defineConfig((): UserConfig => { return { server: { host: "0.0.0.0", port: 8082, strictPort: false, fs: { allow: [repoRoot], }, watch: { ignored: ["**/.tanstack/**"], }, }, plugins: [ cloudflare({ viteEnvironment: { name: "ssr" } }), tsConfigPaths({ projects: [path.join(__dirname, "tsconfig.json")] }), tanstackStart({ router: { quoteStyle: "double", semicolons: true, }, pages: sitemapPages, sitemap: { host: siteHost, }, }), react(), tailwindcss(), ], }; });