feat(i18n): default the cn build to Chinese

The .cn site exists for Chinese readers, so the unprefixed routes should
serve Chinese there. Rspress omits the prefix for the default locale, so
this also moves the routes:

              .com                      .cn
  English     /                         /en-gb/
  Chinese     /zh-hans/                 /

- Make the default locale a per-target value rather than a constant, and
  move the localePrefix helper inside createConfig so a prefix can no
  longer be hardcoded elsewhere.
- Build the sitemap priority map from localePrefix. The keys are full
  route paths, so the previous hardcoded "/zh-hans/..." entries would
  have silently stopped matching on .cn.
- Widen the nav Home activeMatch to cover either prefixed home, since the
  two targets put a different locale behind the prefix.

Note: README.md also carries the licence wording changed in the next
commit — a single file's hunks cannot be split non-interactively.
This commit is contained in:
zihluwang committed 2026-09-25 15:38:03 +08:00
1 parent 861852961f
commit 164c319ac1
5 files changed
+110 -45

No files matched your search

+8 -3
View File
@@ -19,9 +19,14 @@ pnpm format # Prettier — format all files
`rspress.config.ts` exports a `createConfig(target)` factory; `rspress.cn.config.ts` `rspress.config.ts` exports a `createConfig(target)` factory; `rspress.cn.config.ts`
is a three-line wrapper calling it with `"cn"`, passed via `--config`. The two is a three-line wrapper calling it with `"cn"`, passed via `--config`. The two
targets differ in the canonical origin used for `head`, the sitemap and targets differ in the default (unprefixed) language, the canonical origin used
`robots.txt`, and in which social links are rendered. Both write to `doc_build/`, for `head`, the sitemap and `robots.txt`, and in which social links are rendered.
so build and deploy one target at a time. Both write to `doc_build/`, so build and deploy one target at a time.
`.com` defaults to `en-gb` and `.cn` to `zh-hans`, so the same content is served
at different paths per target — Rspress omits the prefix for the default locale.
Anything referencing a locale prefix must go through the `localePrefix` helper
inside `createConfig`, never a hardcoded `/zh-hans`.
## Architecture ## Architecture
+28 -3
View File
@@ -42,14 +42,28 @@ pnpm format # Prettier — format all files
The site is published on two domains, and the two builds are not The site is published on two domains, and the two builds are not
interchangeable. `pnpm build:cn` runs `rspress build --config interchangeable. `pnpm build:cn` runs `rspress build --config
rspress.cn.config.ts`, which differs from the default in three ways: rspress.cn.config.ts`, which differs from the default in four ways:
| | `build:com` | `build:cn` | | | `build:com` | `build:cn` |
| ------------------------------- | ----------------- | -------------- | | ------------------------------- | ----------------- | -------------- |
| Default language (unprefixed) | `en-gb` | `zh-hans` |
| Canonical, `hreflang`, `og:url` | `onixbyte.com` | `onixbyte.cn` | | Canonical, `hreflang`, `og:url` | `onixbyte.com` | `onixbyte.cn` |
| `sitemap.xml`, `robots.txt` | `onixbyte.com` | `onixbyte.cn` | | `sitemap.xml`, `robots.txt` | `onixbyte.com` | `onixbyte.cn` |
| Social links | GitHub, Douyin, X | GitHub, Douyin | | Social links | GitHub, Douyin, X | GitHub, Douyin |
**The default language differs per target**, so the routes do too. Rspress
serves the default locale without a prefix:
| | `.com` | `.cn` |
| --------------- | ------------------------------- | ----------------------------- |
| English home | `/` | `/en-gb/` |
| Chinese home | `/zh-hans/` | `/` |
| English article | `/blogs/git-cheatsheet` | `/en-gb/blogs/git-cheatsheet` |
| Chinese article | `/zh-hans/blogs/git-cheatsheet` | `/blogs/git-cheatsheet` |
Content paths under `docs/` are unaffected — only the prefixes change, and
Rspress rewrites internal links per locale automatically.
X is omitted from the `.cn` build because it is not reachable from mainland X is omitted from the `.cn` build because it is not reachable from mainland
China. Which links count as domestic is a single list in `rspress.config.ts` China. Which links count as domestic is a single list in `rspress.config.ts`
(`SOCIAL_LINKS`) — move an entry between the two arrays to change a build. (`SOCIAL_LINKS`) — move an entry between the two arrays to change a build.
@@ -73,7 +87,7 @@ second build overwrites the first. To preview the `.cn` variant in dev, run
docs/ docs/
├── public/ # Static assets served from the site root ├── public/ # Static assets served from the site root
│ └── onixbyte-*.svg │ └── onixbyte-*.svg
├── en-gb/ # English content (default locale, no URL prefix) ├── en-gb/ # English content
│ ├── _nav.json # Top navigation; labels are i18n keys │ ├── _nav.json # Top navigation; labels are i18n keys
│ ├── index.md # Marketing homepage │ ├── index.md # Marketing homepage
│ ├── blogs/ # Technical articles ("Insights") │ ├── blogs/ # Technical articles ("Insights")
@@ -89,6 +103,7 @@ theme/ # Custom Rspress theme
├── index.css # Brand colours and section styling ├── index.css # Brand colours and section styling
├── lib/ ├── lib/
│ ├── site.ts # Shared site constants │ ├── site.ts # Shared site constants
│ ├── i18n.ts # Types i18n.json for useI18n()
│ └── social-icons.ts # Inline SVG for social links with no preset icon │ └── social-icons.ts # Inline SVG for social links with no preset icon
└── components/ └── components/
├── Author.tsx # Byline, rendered from `author` frontmatter ├── Author.tsx # Byline, rendered from `author` frontmatter
@@ -146,6 +161,16 @@ Redirects for the retired `/projects/` paths live in `vercel.json`. The `.cn`
host needs the equivalent rule in its own web server config, which is not host needs the equivalent rule in its own web server config, which is not
tracked in this repository. tracked in this repository.
## Contributing
This repository is **not open for external contributions** — pull requests are
not accepted. Issues reporting broken links, factual errors, typos or
reproducible bugs are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md).
## Licence ## Licence
MIT All rights reserved. Nothing here is licensed for reuse: no permission is
granted to copy, modify or distribute any part of this repository. Reading it
and linking to it are fine. See [LICENCE](./LICENCE) for the full notice, and
[THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md) for the dependencies, which
keep their own licences.
+1 -1
View File
@@ -2,7 +2,7 @@
{ {
"text": "nav.home", "text": "nav.home",
"link": "/", "link": "/",
"activeMatch": "^/$|^/zh-hans/$" "activeMatch": "^/(en-gb/|zh-hans/)?$"
}, },
{ {
"text": "nav.products", "text": "nav.products",
+1 -1
View File
@@ -2,7 +2,7 @@
{ {
"text": "nav.home", "text": "nav.home",
"link": "/", "link": "/",
"activeMatch": "^/$|^/zh-hans/$" "activeMatch": "^/(en-gb/|zh-hans/)?$"
}, },
{ {
"text": "nav.products", "text": "nav.products",
+72 -37
View File
@@ -22,13 +22,44 @@ const ORIGINS: Record<SiteTarget, string> = {
cn: "https://onixbyte.cn", cn: "https://onixbyte.cn",
} }
/** Locale used for the unprefixed routes. */ /** Every locale the site is published in, default first for each target. */
const DEFAULT_LANG = "en-gb" const LANGS = ["en-gb", "zh-hans"] as const
/**
* Locale served from the unprefixed routes.
*
* `.com` leads with English; `.cn` leads with Chinese, so a visitor landing on
* the bare domain gets the language that site exists for. The other locale then
* moves to a prefix — on `.cn`, English lives at `/en-gb/`.
*/
const DEFAULT_LANG: Record<SiteTarget, string> = {
com: "en-gb",
cn: "zh-hans",
}
/** hreflang and Open Graph locale codes, keyed by Rspress lang. */ /** hreflang and Open Graph locale codes, keyed by Rspress lang. */
const HREFLANG: Record<string, string> = { "en-gb": "en-GB", "zh-hans": "zh-Hans" } const HREFLANG: Record<string, string> = { "en-gb": "en-GB", "zh-hans": "zh-Hans" }
const OG_LOCALE: Record<string, string> = { "en-gb": "en_GB", "zh-hans": "zh_CN" } const OG_LOCALE: Record<string, string> = { "en-gb": "en_GB", "zh-hans": "zh_CN" }
/** Language-switcher labels, keyed by Rspress lang. */
const LOCALE_LABELS: Record<string, string> = {
"en-gb": "English (Great Britain)",
"zh-hans": "简体中文",
}
/** Site title and description, keyed by Rspress lang. */
const LOCALE_META: Record<string, { title: string; description: string }> = {
"en-gb": {
title: "OnixByte",
description:
"OnixByte builds self-developed SaaS products and delivers custom software engineering services.",
},
"zh-hans": {
title: "OnixByte",
description: "曜珀科技自主研发 SaaS 产品,并提供定制化软件开发服务。",
},
}
/** /**
* Social links, split by whether they are reliably reachable from mainland * Social links, split by whether they are reliably reachable from mainland
* China. The `.cn` build renders only the `domestic` set. * China. The `.cn` build renders only the `domestic` set.
@@ -73,19 +104,31 @@ const SOCIAL_LINKS: Record<"domestic" | "international", SocialLink[]> = {
type CustomMapsOption = NonNullable<Parameters<typeof pluginSitemap>[0]["customMaps"]> type CustomMapsOption = NonNullable<Parameters<typeof pluginSitemap>[0]["customMaps"]>
type SitemapOverrides = Record<string, Omit<CustomMapsOption[string], "loc">> type SitemapOverrides = Record<string, Omit<CustomMapsOption[string], "loc">>
const SITEMAP_PRIORITIES = { const PRIORITISED_ROUTES = [
"/products/": { priority: "1.0" }, ["/products/", "1.0"],
"/zh-hans/products/": { priority: "1.0" }, ["/services/", "1.0"],
"/services/": { priority: "1.0" }, ["/opensource-projects/", "0.8"],
"/zh-hans/services/": { priority: "1.0" }, ] as const
"/opensource-projects/": { priority: "0.8" },
"/zh-hans/opensource-projects/": { priority: "0.8" },
} satisfies SitemapOverrides
const localePrefix = (lang: string) => (lang === DEFAULT_LANG ? "" : `/${lang}`) /**
* Builds the override map for a target.
*
* The keys are full route paths, so they depend on which locale is unprefixed —
* `/products/` means English on `.com` and Chinese on `.cn`.
*/
function sitemapPriorities(localePrefix: (lang: string) => string): SitemapOverrides {
return Object.fromEntries(
LANGS.flatMap((lang) =>
PRIORITISED_ROUTES.map(([routePath, priority]) => [
`${localePrefix(lang)}${routePath}`,
{ priority },
])
)
) satisfies SitemapOverrides
}
/** Strips the language prefix, always returning a leading slash. */ /** Strips the language prefix, always returning a leading slash. */
function bareRoutePath(routePath: string, lang: string): string { function bareRoutePath(localePrefix: (lang: string) => string, routePath: string, lang: string) {
const prefix = localePrefix(lang) const prefix = localePrefix(lang)
const rest = prefix && routePath.startsWith(prefix) ? routePath.slice(prefix.length) : routePath const rest = prefix && routePath.startsWith(prefix) ? routePath.slice(prefix.length) : routePath
return rest.startsWith("/") ? rest : `/${rest}` return rest.startsWith("/") ? rest : `/${rest}`
@@ -112,16 +155,19 @@ function pluginRobots(origin: string): RspressPlugin {
export function createConfig(target: SiteTarget): UserConfig { export function createConfig(target: SiteTarget): UserConfig {
const origin = ORIGINS[target] const origin = ORIGINS[target]
const defaultLang = DEFAULT_LANG[target]
const socialLinks = [ const socialLinks = [
...SOCIAL_LINKS.domestic, ...SOCIAL_LINKS.domestic,
...(target === "cn" ? [] : SOCIAL_LINKS.international), ...(target === "cn" ? [] : SOCIAL_LINKS.international),
] ]
/** Rspress drops the prefix from the default locale's routes. */
const localePrefix = (lang: string) => (lang === defaultLang ? "" : `/${lang}`)
return defineConfig({ return defineConfig({
root: path.join(__dirname, "docs"), root: path.join(__dirname, "docs"),
title: "OnixByte", title: LOCALE_META[defaultLang].title,
description: description: LOCALE_META[defaultLang].description,
"OnixByte builds self-developed SaaS products and delivers custom software engineering services.",
icon: "/onixbyte-icon.svg", icon: "/onixbyte-icon.svg",
logo: { logo: {
light: "/onixbyte-light-logo.svg", light: "/onixbyte-light-logo.svg",
@@ -139,8 +185,8 @@ export function createConfig(target: SiteTarget): UserConfig {
head: [ head: [
(route) => { (route) => {
const canonical = `${origin}${route.routePath}` const canonical = `${origin}${route.routePath}`
const bare = bareRoutePath(route.routePath, route.lang) const bare = bareRoutePath(localePrefix, route.routePath, route.lang)
const otherLang = route.lang === DEFAULT_LANG ? "zh-hans" : DEFAULT_LANG const otherLang = LANGS.find((lang) => lang !== route.lang) ?? route.lang
const alternate = `${origin}${localePrefix(otherLang)}${bare === "/" ? "/" : bare}` const alternate = `${origin}${localePrefix(otherLang)}${bare === "/" ? "/" : bare}`
const defaultUrl = `${origin}${bare === "/" ? "/" : bare}` const defaultUrl = `${origin}${bare === "/" ? "/" : bare}`
@@ -172,22 +218,13 @@ export function createConfig(target: SiteTarget): UserConfig {
markdown: { markdown: {
showLineNumbers: true, showLineNumbers: true,
}, },
lang: DEFAULT_LANG, lang: defaultLang,
locales: [ // The default locale is listed first so the language switcher leads with it.
{ locales: [defaultLang, ...LANGS.filter((lang) => lang !== defaultLang)].map((lang) => ({
lang: "en-gb", lang,
label: "English (Great Britain)", label: LOCALE_LABELS[lang],
title: "OnixByte", ...LOCALE_META[lang],
description: })),
"OnixByte builds self-developed SaaS products and delivers custom software engineering services.",
},
{
lang: "zh-hans",
label: "简体中文",
title: "OnixByte",
description: "曜珀科技自主研发 SaaS 产品,并提供定制化软件开发服务。",
},
],
// Local full-text search. This is already the Rspress v2 default; stated // Local full-text search. This is already the Rspress v2 default; stated
// explicitly so the intent is visible. The index is emitted per locale to // explicitly so the intent is visible. The index is emitted per locale to
// `static/search_index.<lang>.<hash>.json` at build time. // `static/search_index.<lang>.<hash>.json` at build time.
@@ -200,12 +237,10 @@ export function createConfig(target: SiteTarget): UserConfig {
siteUrl: origin, siteUrl: origin,
defaultChangeFreq: "weekly", defaultChangeFreq: "weekly",
defaultPriority: "0.5", defaultPriority: "0.5",
// Keys are full route paths, so the non-default locale needs its own
// entries — `/products/` does not match `/zh-hans/products/`.
// Double assertion rather than a plain cast: the plugin's `Sitemap` type // Double assertion rather than a plain cast: the plugin's `Sitemap` type
// requires `loc`, so the two types do not overlap enough for a single // requires `loc`, so the two types do not overlap enough for a single
// assertion. See `SITEMAP_PRIORITIES` above for why omitting it is right. // assertion. See `sitemapPriorities` above for why omitting it is right.
customMaps: SITEMAP_PRIORITIES as unknown as CustomMapsOption, customMaps: sitemapPriorities(localePrefix) as unknown as CustomMapsOption,
}), }),
pluginRobots(origin), pluginRobots(origin),
], ],