mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 16:34:36 +00:00
Every page of the website now has a Simplified Chinese version under
/zh-CN: the interface, docs, blog posts, comparison pages, legal pages,
all changelog releases and the homepage mockup. English URLs are unchanged.
The URL alone decides the language. There is no browser-language redirect
and no stored preference, because either would have to redirect the
unprefixed English URLs that people share and search engines index.
Internal links carry the page's language instead, and hreflang alternates
point Chinese search results at /zh-CN.
Translated headings pin the English anchor with {#id}, so section links
work in both languages. content-translations.test.ts fails when a page
has no translation or a translation's headings drift from the English.
Changelog releases and plugin listings without a translation render
whole in English under a notice, so a release never waits on one.
Plugin manifests and the registry gain optional translations and
OVERVIEW.zh-CN.md fields; older clients and registries ignore them.
149 lines
4.4 KiB
TypeScript
149 lines
4.4 KiB
TypeScript
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<string>(["/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(),
|
|
],
|
|
};
|
|
});
|