Skip to content

Commit 7d9ee76

Browse files
committed
fix(website): stabilize localized client metadata
1 parent 0def291 commit 7d9ee76

6 files changed

Lines changed: 130 additions & 30 deletions

File tree

website/config/buildLocale.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { normalizeSiteLocale } from '../src/i18n/locale'
22

33
export function getBuildLocale() {
4-
// eslint-disable-next-line node/prefer-global/process -- Docusaurus 会在服务端和客户端构建中注入该环境变量
5-
return normalizeSiteLocale(process.env.DOCUSAURUS_CURRENT_LOCALE)
4+
// eslint-disable-next-line node/prefer-global/process -- 此模块也会进入浏览器 bundle,需安全访问可选的 Node 全局
5+
return normalizeSiteLocale(globalThis.process?.env.DOCUSAURUS_CURRENT_LOCALE)
66
}

website/config/siteMetadata.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type { SiteLocale } from '../src/i18n/locale'
22
import { getSiteConfigCopy } from '../src/i18n/siteConfig'
33
import { getBuildLocale } from './buildLocale'
44

5-
// eslint-disable-next-line node/prefer-global/process -- Docusaurus 会在服务端和客户端构建中注入该环境变量
6-
const envSiteUrl = process.env.SITE_URL
5+
// eslint-disable-next-line node/prefer-global/process -- 此模块也会进入浏览器 bundle,需安全访问可选的 Node 全局
6+
const envSiteUrl = globalThis.process?.env.SITE_URL
77

88
export const siteUrl = envSiteUrl || 'https://tw.icebreaker.top'
99
export const siteName = 'weapp-tailwindcss'

website/src/i18n/locale.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ export const localePreferenceStorageKey = 'weapp-tailwindcss:website:locale'
88
const ENGLISH_PREFIX_RE = /^en(?:[-_]|$)/i
99
const CHINESE_PREFIX_RE = /^zh(?:[-_]|$)/i
1010
const EN_LOCALE_PATH_RE = /^\/en(?:\/|$)/
11+
const REPEATED_PATH_SEPARATOR_RE = /\/{2,}/g
12+
13+
function normalizePathname(pathname: string): string {
14+
if (!pathname) {
15+
return '/'
16+
}
17+
const withLeadingSlash = pathname.startsWith('/') ? pathname : `/${pathname}`
18+
return withLeadingSlash.replace(REPEATED_PATH_SEPARATOR_RE, '/')
19+
}
1120

1221
export function normalizeSiteLocale(value?: string | null): SiteLocale {
1322
if (!value) {
@@ -31,24 +40,24 @@ export function getLocalePrefix(locale: SiteLocale): string {
3140
}
3241

3342
export function stripLocalePrefix(pathname: string): string {
34-
if (!pathname || pathname === '/') {
43+
const normalizedPathname = normalizePathname(pathname)
44+
if (normalizedPathname === '/') {
3545
return '/'
3646
}
3747

38-
if (pathname === '/en') {
48+
if (normalizedPathname === '/en') {
3949
return '/'
4050
}
4151

42-
if (EN_LOCALE_PATH_RE.test(pathname)) {
43-
return pathname.replace('/en', '') || '/'
52+
if (EN_LOCALE_PATH_RE.test(normalizedPathname)) {
53+
return normalizedPathname.replace('/en', '') || '/'
4454
}
4555

46-
return pathname
56+
return normalizedPathname
4757
}
4858

4959
export function toLocalePath(pathname: string, locale: SiteLocale): string {
50-
const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`
51-
const strippedPath = stripLocalePrefix(normalizedPath)
60+
const strippedPath = stripLocalePrefix(pathname)
5261

5362
if (locale === 'en') {
5463
return strippedPath === '/' ? '/en/' : `/en${strippedPath}`

website/src/theme/Layout.tsx

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { ComponentProps } from 'react'
22
import Head from '@docusaurus/Head'
33
import { useLocation } from '@docusaurus/router'
4-
import { getSiteLanguage, siteUrl } from '@site/config/siteMetadata'
4+
import { getSiteLanguage } from '@site/config/siteMetadata'
55
import { UiManagementProvider } from '@site/src/features/ui-management/context'
6-
import { localePreferenceStorageKey, stripLocalePrefix, toAbsoluteLocaleUrl } from '@site/src/i18n/locale'
6+
import { localePreferenceStorageKey } from '@site/src/i18n/locale'
77
import { useCurrentSiteLocale } from '@site/src/i18n/runtime'
88
import Layout from '@theme-original/Layout'
99
import React, { useEffect } from 'react'
@@ -14,14 +14,6 @@ export default function LayoutWrapper(props: LayoutWrapperProps) {
1414
const location = useLocation()
1515
const locale = useCurrentSiteLocale()
1616
const isHomepage = location.pathname === '/'
17-
const relativePath = stripLocalePrefix(location.pathname)
18-
const isContentPage = relativePath === '/docs'
19-
|| relativePath.startsWith('/docs/')
20-
|| relativePath === '/blog'
21-
|| relativePath.startsWith('/blog/')
22-
const canonicalUrl = toAbsoluteLocaleUrl(siteUrl, location.pathname, locale)
23-
const alternateZhUrl = toAbsoluteLocaleUrl(siteUrl, location.pathname, 'zh-cn')
24-
const alternateEnUrl = toAbsoluteLocaleUrl(siteUrl, location.pathname, 'en')
2517

2618
useEffect(() => {
2719
if (typeof window === 'undefined') {
@@ -38,19 +30,15 @@ export default function LayoutWrapper(props: LayoutWrapperProps) {
3830
return (
3931
<UiManagementProvider>
4032
<div className="relative">
41-
<Head>
42-
{!isContentPage && <link rel="canonical" href={canonicalUrl} />}
43-
<link rel="alternate" hrefLang="zh-CN" href={alternateZhUrl} />
44-
<link rel="alternate" hrefLang="en-US" href={alternateEnUrl} />
45-
<link rel="alternate" hrefLang="x-default" href={alternateZhUrl} />
46-
<meta httpEquiv="Content-Language" content={getSiteLanguage(locale)} />
47-
</Head>
4833
{/* {location.pathname !== '/' && (
4934
<div className="pointer-events-none absolute inset-0 z-[201]">
5035
<div className="light-effect pointer-events-none absolute right-[13.14%]"></div>
5136
</div>
5237
)} */}
5338
<Layout {...props} />
39+
<Head>
40+
<meta httpEquiv="Content-Language" content={getSiteLanguage(locale)} />
41+
</Head>
5442
{isHomepage && (
5543
<div className="
5644
pointer-events-none absolute inset-0 z-[202] flex-none
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { MetaHTMLAttributes } from 'react'
2+
import Head from '@docusaurus/Head'
3+
import { useLocation } from '@docusaurus/router'
4+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
5+
import { toAbsoluteLocaleUrl } from '@site/src/i18n/locale'
6+
import SearchMetadata from '@theme/SearchMetadata'
7+
import React from 'react'
8+
9+
interface SiteThemeConfig {
10+
image?: string
11+
metadata?: Array<MetaHTMLAttributes<HTMLMetaElement>>
12+
}
13+
14+
function LocaleHeaders() {
15+
const {
16+
siteConfig: { url: siteUrl },
17+
i18n: { currentLocale, defaultLocale, localeConfigs },
18+
} = useDocusaurusContext()
19+
const { pathname } = useLocation()
20+
const currentHtmlLang = localeConfigs[currentLocale].htmlLang
21+
const toOpenGraphLocale = (value: string) => value.replace('-', '_')
22+
23+
return (
24+
<Head>
25+
{Object.entries(localeConfigs).map(([locale, { htmlLang }]) => (
26+
<link
27+
key={locale}
28+
rel="alternate"
29+
href={toAbsoluteLocaleUrl(siteUrl, pathname, locale === 'en' ? 'en' : 'zh-cn')}
30+
hrefLang={htmlLang}
31+
/>
32+
))}
33+
<link
34+
rel="alternate"
35+
href={toAbsoluteLocaleUrl(siteUrl, pathname, defaultLocale === 'en' ? 'en' : 'zh-cn')}
36+
hrefLang="x-default"
37+
/>
38+
<meta property="og:locale" content={toOpenGraphLocale(currentHtmlLang)} />
39+
{Object.values(localeConfigs)
40+
.filter(config => currentHtmlLang !== config.htmlLang)
41+
.map(config => (
42+
<meta
43+
key={`meta-og-${config.htmlLang}`}
44+
property="og:locale:alternate"
45+
content={toOpenGraphLocale(config.htmlLang)}
46+
/>
47+
))}
48+
</Head>
49+
)
50+
}
51+
52+
function CanonicalUrlHeaders() {
53+
const {
54+
siteConfig: { url: siteUrl },
55+
i18n: { currentLocale },
56+
} = useDocusaurusContext()
57+
const { pathname } = useLocation()
58+
const canonicalUrl = toAbsoluteLocaleUrl(siteUrl, pathname, currentLocale === 'en' ? 'en' : 'zh-cn')
59+
60+
return (
61+
<Head>
62+
<meta property="og:url" content={canonicalUrl} />
63+
<link rel="canonical" href={canonicalUrl} />
64+
</Head>
65+
)
66+
}
67+
68+
export default function SiteMetadata() {
69+
const {
70+
siteConfig,
71+
i18n: { currentLocale },
72+
} = useDocusaurusContext()
73+
const { metadata = [], image: defaultImage } = siteConfig.themeConfig as SiteThemeConfig
74+
const defaultImageUrl = defaultImage
75+
? new URL(defaultImage, `${siteConfig.url}/`).toString()
76+
: undefined
77+
78+
return (
79+
<>
80+
<Head>
81+
<meta name="twitter:card" content="summary_large_image" />
82+
<body />
83+
</Head>
84+
{defaultImageUrl && (
85+
<Head>
86+
<meta property="og:image" content={defaultImageUrl} />
87+
<meta name="twitter:image" content={defaultImageUrl} />
88+
</Head>
89+
)}
90+
<CanonicalUrlHeaders />
91+
<LocaleHeaders />
92+
<SearchMetadata tag="default" locale={currentLocale} />
93+
<Head>
94+
{metadata.map((metadatum, index) => (
95+
<meta key={index} {...metadatum} />
96+
))}
97+
</Head>
98+
</>
99+
)
100+
}

website/tests/index.spec.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ const viewports: ViewportCase[] = [
5555
] as const
5656

5757
async function setStoredLocale(page: Parameters<typeof test>[0]['page'], locale: 'zh-cn' | 'en') {
58-
await page.addInitScript((key, value) => {
58+
await page.addInitScript(({ key, value }) => {
5959
window.localStorage.setItem(key, value)
60-
}, localeStorageKey, locale)
60+
}, { key: localeStorageKey, value: locale })
6161
}
6262

6363
async function setNavigatorLanguages(page: Parameters<typeof test>[0]['page'], languages: string[]) {
@@ -302,6 +302,7 @@ test.describe('mobile navbar sidebar', () => {
302302
)
303303

304304
test('keeps the document menu visible and navigable', async ({ page }) => {
305+
await setStoredLocale(page, 'zh-cn')
305306
await page.goto(new URL('/docs/intro', baseURL).toString(), {
306307
waitUntil: 'networkidle',
307308
})
@@ -332,6 +333,7 @@ test.describe('mobile navbar sidebar', () => {
332333

333334
test.describe('color mode transition strategy', () => {
334335
test('desktop uses a short view transition for theme changes', async ({ page }) => {
336+
await setStoredLocale(page, 'zh-cn')
335337
await page.setViewportSize({ width: 1440, height: 900 })
336338
await page.addInitScript(() => {
337339
window.localStorage.setItem('theme', 'dark')
@@ -360,6 +362,7 @@ test.describe('color mode transition strategy', () => {
360362
})
361363

362364
test('non fine-pointer environments skip view transitions for theme changes', async ({ page }) => {
365+
await setStoredLocale(page, 'zh-cn')
363366
await page.setViewportSize({ width: 1440, height: 900 })
364367
await page.addInitScript(() => {
365368
window.localStorage.setItem('theme', 'dark')

0 commit comments

Comments
 (0)