diff --git a/website/index.html b/website/index.html
index 3780887..5311bea 100644
--- a/website/index.html
+++ b/website/index.html
@@ -24,7 +24,7 @@
content="一键切换 Kimi / 智谱 GLM / MiniMax / DeepSeek 等供应商,用量与账单一目了然,开源 MIT。"
/>
-
+
@@ -33,7 +33,7 @@
name="twitter:description"
content="一键切换 Kimi / 智谱 GLM / MiniMax / DeepSeek 等供应商,用量与账单一目了然,开源 MIT。"
/>
-
+
diff --git a/website/package-lock.json b/website/package-lock.json
index 06e9f0a..7a20a1f 100644
--- a/website/package-lock.json
+++ b/website/package-lock.json
@@ -16,6 +16,7 @@
"react-router-dom": "^7.18.2"
},
"devDependencies": {
+ "@types/node": "^26.1.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
@@ -1299,6 +1300,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "26.1.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
+ "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.3.0"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -2723,6 +2734,13 @@
"node": ">=14.17"
}
},
+ "node_modules/undici-types": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
+ "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
diff --git a/website/package.json b/website/package.json
index 92569c2..308d390 100644
--- a/website/package.json
+++ b/website/package.json
@@ -17,6 +17,7 @@
"react-router-dom": "^7.18.2"
},
"devDependencies": {
+ "@types/node": "^26.1.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
diff --git a/website/public/sitemap.xml b/website/public/sitemap.xml
index 28e0e8d..813d306 100644
--- a/website/public/sitemap.xml
+++ b/website/public/sitemap.xml
@@ -6,27 +6,17 @@
1.0
- https://billowliu2.github.io/KimiSwitch/#/features
+ https://billowliu2.github.io/KimiSwitch/features
monthly
0.8
- https://billowliu2.github.io/KimiSwitch/#/showcase
- monthly
- 0.8
-
-
- https://billowliu2.github.io/KimiSwitch/#/performance
- monthly
- 0.7
-
-
- https://billowliu2.github.io/KimiSwitch/#/changelog
+ https://billowliu2.github.io/KimiSwitch/changelog
weekly
0.7
- https://billowliu2.github.io/KimiSwitch/#/download
+ https://billowliu2.github.io/KimiSwitch/download
weekly
0.9
diff --git a/website/src/App.tsx b/website/src/App.tsx
index 52b988f..703eb36 100644
--- a/website/src/App.tsx
+++ b/website/src/App.tsx
@@ -8,6 +8,10 @@ import Download from "./sections/Download";
import Privacy from "./sections/Privacy";
import CTA from "./sections/CTA";
import Footer from "./components/Footer";
+import { useLang, type Dict } from "./i18n";
+
+const SITE_URL = "https://billowliu2.github.io/KimiSwitch";
+type SeoKey = keyof Dict["seo"];
function ScrollToTop() {
const { pathname } = useLocation();
@@ -17,6 +21,47 @@ function ScrollToTop() {
return null;
}
+/** Update per-page title / description / OG / canonical for SEO. */
+function RouteMeta() {
+ const { pathname } = useLocation();
+ const { t } = useLang();
+
+ useEffect(() => {
+ const page = pathname.replace(/\/+$/, "");
+ const seoKey: SeoKey =
+ page === "/features" || page === "/changelog" || page === "/download"
+ ? (page.slice(1) as SeoKey)
+ : "home";
+ const m = t.seo[seoKey];
+ const url = `${SITE_URL}${page === "" ? "/" : page}`;
+
+ document.title = m.title;
+ setMeta("name", "description", m.description);
+ setMeta("property", "og:title", m.title);
+ setMeta("property", "og:description", m.description);
+ setMeta("property", "og:url", url);
+ setMeta("name", "twitter:title", m.title);
+ setMeta("name", "twitter:description", m.description);
+
+ const canon = document.querySelector('link[rel="canonical"]');
+ if (canon) canon.setAttribute("href", url);
+ }, [pathname, t]);
+
+ return null;
+}
+
+function setMeta(attr: "name" | "property", key: string, content: string) {
+ let el = document.head.querySelector(
+ `meta[${attr}="${key}"]`,
+ );
+ if (!el) {
+ el = document.createElement("meta");
+ el.setAttribute(attr, key);
+ document.head.appendChild(el);
+ }
+ el.setAttribute("content", content);
+}
+
const homePage = (
<>
@@ -30,6 +75,7 @@ export default function App() {
+
-
+
-
+
,
);
diff --git a/website/vite.config.ts b/website/vite.config.ts
index 97f2f63..b3c62c1 100644
--- a/website/vite.config.ts
+++ b/website/vite.config.ts
@@ -1,11 +1,30 @@
-import { defineConfig } from "vite";
+import { defineConfig, type Plugin } from "vite";
import react from "@vitejs/plugin-react";
+import { copyFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+
+/**
+ * GitHub Pages SPA fallback: any deep path (e.g. /KimiSwitch/features) is
+ * served by 404.html; duplicating the built index.html there lets the
+ * BrowserRouter boot and render the right route. This keeps every page
+ * indexable as a real URL instead of a `#/` fragment.
+ */
+function generate404(): Plugin {
+ return {
+ name: "generate-404",
+ apply: "build",
+ closeBundle() {
+ const dist = fileURLToPath(new URL("./dist", import.meta.url));
+ copyFileSync(`${dist}/index.html`, `${dist}/404.html`);
+ },
+ };
+}
// GitHub Pages subpath: billowliu2.github.io/KimiSwitch/
export default defineConfig({
base: "/KimiSwitch/",
- plugins: [react()],
+ plugins: [react(), generate404()],
resolve: {
alias: { "@": "/src" },
},
-});
\ No newline at end of file
+});