Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion bin/build-pages.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ export interface GeneratedPage {
*/
kind: 'home' | 'landing' | 'doc';
}
export const LOCALES: Record<string, { prefix: string; lang: string; label: string; home: string; dir: string }>;
export const LOCALES: Record<
string,
{ prefix: string; lang: string; label: string; home: string; dir: string; og: string }
>;
export const LANDING_SLUGS: string[];
export const PAGES: Array<{
slug: string;
Expand Down
36 changes: 27 additions & 9 deletions bin/build-pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ const REPO = 'https://github.com/ranuts/document';

/** Locales the shell knows about. `prefix` is the URL directory; '' = root. */
export const LOCALES = {
en: { prefix: '', lang: 'en', label: 'EN', home: '/', dir: 'ltr' },
'zh-CN': { prefix: '/zh-CN', lang: 'zh-CN', label: '中文', home: '/zh-CN/', dir: 'ltr' },
ja: { prefix: '/ja', lang: 'ja', label: '日本語', home: '/ja/', dir: 'ltr' },
de: { prefix: '/de', lang: 'de', label: 'Deutsch', home: '/de/', dir: 'ltr' },
es: { prefix: '/es', lang: 'es', label: 'Español', home: '/es/', dir: 'ltr' },
ko: { prefix: '/ko', lang: 'ko', label: '한국어', home: '/ko/', dir: 'ltr' },
pt: { prefix: '/pt', lang: 'pt', label: 'Português', home: '/pt/', dir: 'ltr' },
en: { prefix: '', lang: 'en', label: 'EN', home: '/', dir: 'ltr', og: 'en_US' },
'zh-CN': { prefix: '/zh-CN', lang: 'zh-CN', label: '中文', home: '/zh-CN/', dir: 'ltr', og: 'zh_CN' },
ja: { prefix: '/ja', lang: 'ja', label: '日本語', home: '/ja/', dir: 'ltr', og: 'ja_JP' },
de: { prefix: '/de', lang: 'de', label: 'Deutsch', home: '/de/', dir: 'ltr', og: 'de_DE' },
es: { prefix: '/es', lang: 'es', label: 'Español', home: '/es/', dir: 'ltr', og: 'es_ES' },
ko: { prefix: '/ko', lang: 'ko', label: '한국어', home: '/ko/', dir: 'ltr', og: 'ko_KR' },
// pt_BR, not pt_PT: the pages, the shell strings and the vendor locale the
// editor loads (pt.json) are all Brazilian Portuguese.
pt: { prefix: '/pt', lang: 'pt', label: 'Português', home: '/pt/', dir: 'ltr', og: 'pt_BR' },
};
const DEFAULT_LOCALE = 'en';

Expand Down Expand Up @@ -614,6 +616,10 @@ function renderHome({ locale, data, locales }) {
const alternates = locales
.map((l) => ` <link rel="alternate" hreflang="${l}" href="${ORIGIN + LOCALES[l].home}" />`)
.join('\n');
const ogAlternates = locales
.filter((l) => l !== locale)
.map((l) => ` <meta property="og:locale:alternate" content="${LOCALES[l].og}" />`)
.join('\n');
const langOptions = locales
.map((l) => ` <r-option value="${l}" data-href="${LOCALES[l].home}">${LOCALES[l].label}</r-option>`)
.join('\n');
Expand Down Expand Up @@ -751,6 +757,8 @@ ${alternates}

<meta property="og:type" content="website" />
<meta property="og:site_name" content="Online Document Editor" />
<meta property="og:locale" content="${L.og}" />
${ogAlternates}
<meta property="og:title" content="${e(data.title)}" />
<meta property="og:description" content="${e(data.ogDescription || data.description)}" />
<meta property="og:url" content="${url}" />
Expand Down Expand Up @@ -959,6 +967,7 @@ function renderPage({ page, locale, meta, body, headings, faq, steps, source })
applicationCategory: 'BusinessApplication',
operatingSystem: 'Any (web browser)',
isAccessibleForFree: true,
inLanguage: L.lang,
description: meta.appDescription || description,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
}
Expand Down Expand Up @@ -1006,10 +1015,17 @@ function renderPage({ page, locale, meta, body, headings, faq, steps, source })
crumbs.push({ '@type': 'ListItem', position: crumbs.length + 1, name: meta.breadcrumb || title, item: url });
graph.push({ '@type': 'BreadcrumbList', itemListElement: crumbs });

const alternates = Object.keys(LOCALES)
.filter((l) => page.sources[l])
const translations = Object.keys(LOCALES).filter((l) => page.sources[l]);
const alternates = translations
.map((l) => ` <link rel="alternate" hreflang="${l}" href="${ORIGIN + routeFor(l, page.slug)}" />`)
.join('\n');
// Open Graph carries the same set as hreflang, in its own spelling: the
// language of this page, then the others it exists in. Without it a share of
// /ja/ or /pt/ is treated as English by every consumer that reads OG.
const ogAlternates = translations
.filter((l) => l !== locale)
.map((l) => ` <meta property="og:locale:alternate" content="${LOCALES[l].og}" />`)
.join('\n');
const langOptions = Object.keys(LOCALES)
.filter((l) => page.sources[l])
.map(
Expand Down Expand Up @@ -1095,6 +1111,8 @@ ${alternates}

<meta property="og:type" content="${isLanding ? 'website' : 'article'}" />
<meta property="og:site_name" content="Online Document Editor" />
<meta property="og:locale" content="${L.og}" />
${ogAlternates}
<meta property="og:title" content="${escapeHtml(title)}" />
<meta property="og:description" content="${escapeHtml(cardDescription)}" />
<meta property="og:url" content="${url}" />
Expand Down
23 changes: 23 additions & 0 deletions docs/explorations/2026-08-23-seven-languages-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ landing.css。白色主题下看不出来,暗色下就是页面四周一圈底
它遍历 `public/` 下所有 HTML,七种语言进来之后从 97 KB 涨到 680 KB——同样的事实说七遍。
对读它的模型只有害处。改成只收英文页面,开头说明镜像在哪;`/llms.txt` 仍然逐语言列出。

## 8. Open Graph 不知道站点有七种语言(已修)

`hreflang` 是完整的七语言互指,但 Open Graph 那边**一个字都没说**:既没有 `og:locale`
也没有 `og:locale:alternate`。后果是任何读 OG 的消费者(Facebook、LinkedIn、Slack、
各类抓取器)把 `/ja/` 和 `/pt/` 的分享都当成英文页。补上之后每页声明自己的
`og:locale`,并列出其余六种的 `og:locale:alternate`。

映射写在 `LOCALES` 表里,因为 OG 要的是 `language_TERRITORY` 而不是 BCP47:
`en_US` / `zh_CN` / `ja_JP` / `de_DE` / `es_ES` / `ko_KR` / **`pt_BR`**。最后一个再次
确认了第 6 条的取舍——页面、shell 词条、vendor 包(`pt.json`)、OG locale 四处都是巴西葡语。

顺带补上落地页 `WebApplication` 节点缺的 `inLanguage`(首页和文档页早就有)。

## 复查过但没有问题的

- **SEO 覆盖**:7 种语言 × 21 页齐全;hreflang 是完整的七语言互指 + x-default;
Expand All @@ -60,6 +73,16 @@ landing.css。白色主题下看不出来,暗色下就是页面四周一圈底
- **德语长词**:`Installieren und im Flugzeug nutzen`(35 字符)在 pillars 列里正常换行,
没有溢出。
- **`/embed-demo`**:仍然是英文,这是 CLAUDE.md 的规矩(共用页面用英文),不是遗漏。
- **站内死链**:158 个页面、5043 条内链、165 个不同目标,逐个请求,零 4xx。
- **基础可达性**:五类页面上没有缺 alt 的图、没有无名链接/按钮、标题层级不跳级、
每页恰好一个 h1、`<html lang>` 都在。格式索引的行、右栏链接、语言选择器都能 Tab 到。
- **首屏性能**:落地页 10 个请求 / 392 KB,FCP 44 ms。首页看起来 9 MB 是
`landing-prefetch.js` 在预热三个编辑器引擎(既有行为,有 E2E 钉着),发生在
load 之后;线上走 br 压缩(`sdk-all.js` 13.7 MB → 3.0 MB)。
- **缓存头**:`sdkjs/` 与 `web-apps/` 故意不上 `immutable`(里面有我们的补丁和
iframe HTML),4 小时 + `must-revalidate`。这次补齐的 locale JSON 也在这棵树里,
但 SW 的 runtime cache 按 `VENDOR_VERSION` 命名,vendor 一变就是新 cache,
拿不到旧副本。

## 还没做的

Expand Down
7 changes: 7 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@

<meta property="og:type" content="website" />
<meta property="og:site_name" content="Online Document Editor" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
<meta property="og:locale:alternate" content="ja_JP" />
<meta property="og:locale:alternate" content="de_DE" />
<meta property="og:locale:alternate" content="es_ES" />
<meta property="og:locale:alternate" content="ko_KR" />
<meta property="og:locale:alternate" content="pt_BR" />
<meta property="og:title" content="Open DOCX, XLSX &amp; PPTX in Your Browser — Free Document Viewer &amp; Editor" />
<meta property="og:description" content="Open, view and edit DOCX, XLSX, PPTX and CSV in your browser — no Office, no upload, no sign-up. Free and offline-capable." />
<meta property="og:url" content="https://edit.chaxus.com/" />
Expand Down
16 changes: 16 additions & 0 deletions test/unit/landing-pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@ describe('landing pages', () => {
}
});

/**
* Open Graph has its own spelling of the same set hreflang carries. A
* share of /ja/ or /pt/ without og:locale is read as English by every
* consumer that looks at OG -- which is most of them.
*/
it('declares its Open Graph locale, and the ones it is also published in', () => {
const og = (l: string) => LOCALES[l].og;
expect(attr(html, /property="og:locale" content="([^"]+)"/), `${route} og:locale`).toBe(og(locale));

const alternates = [...html.matchAll(/property="og:locale:alternate" content="([^"]+)"/g)].map((m) => m[1]);
const expected = Object.keys(LOCALES)
.filter((l) => l !== locale && routes.has(routeIn(l, enRoute)))
.map(og);
expect(alternates.sort(), `${route} og:locale:alternate`).toEqual(expected.sort());
});

it('offers every translation it has in the language switch', () => {
for (const other of Object.keys(LOCALES)) {
const target = routeIn(other, enRoute);
Expand Down
Loading