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:
5 files changed
+110
-45
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in new issue
Block a user