diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7a1cd13..5e619c8e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -198,6 +198,7 @@ notes. Entries describe what users experience, not internal refactors. ### Changed +- Every site page's footer links to the author's site, chaxus.com. - The editor page fetches less than half the JavaScript it did before it can start (163 KB instead of 350 KB compressed). Browser-agent text reading and the install prompt load when first needed, and an unused character-set diff --git a/CLAUDE.md b/CLAUDE.md index 65145db89..a00ee35da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -348,6 +348,10 @@ index.html)的 canonical/hreflang/JSON-LD/sitemap/双语互指契约——新 页面级 id。**app 节点的 `url` 恒为站点根**,语言写在 `WebPage.inLanguage`; 改成当前页 URL 就等于宣称七种语言是七个产品。文档页只发 app 的 stub(`appStub()`), 落地页才发完整节点。上面那个测试会拒绝重复 `@id` 与解析不到定义的 `@id` 引用。 +**作者是站外的同一个人**(2026-10-28):`WebSite` 与完整 app 节点的 `author` / `publisher` 都引用 +`https://chaxus.com/#person`,Person 节点每页定义一次;`@id` / `name` / `url` 必须和 chaxus.com、 +ran 写的逐字一致(它们靠 `@id` 合并),唯一来源是 `bin/pages/constants.mjs` 的 `AUTHOR`。 +页脚的 `rel="author"` 链接 zh-CN 指 `chaxus.com/zh/`、其余指英文根。 见 docs/explorations/2026-08-23-entity-graph-from-apple.md。**线上冒烟** `.github/workflows/prod-smoke.yml`:每次 push main 等部署 上线后即跑 + 每日;`E2E_BASE_URL=<站点>` 可把任意 spec 打到部署站。CF 面板里 的 Cache Rules 等不在仓库、CI 复现不了,只能靠冒烟兜底。 diff --git a/bin/pages/constants.mjs b/bin/pages/constants.mjs index 2e0ac73c7..44c1d579c 100644 --- a/bin/pages/constants.mjs +++ b/bin/pages/constants.mjs @@ -11,3 +11,17 @@ export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); export const ORIGIN = 'https://edit.chaxus.com'; export const REPO = 'https://github.com/ranuts/document'; export const SITE_NAME = 'Online Document Editor'; + +/** + * The author, shared with chaxus.com and the ran sites. `id`, `name` and `url` + * must stay identical in all three places: the graph merges on `@id`. + */ +export const AUTHOR = { + id: 'https://chaxus.com/#person', + name: 'chaxus', + url: 'https://chaxus.com/', + sameAs: ['https://github.com/chaxus'], +}; + +/** chaxus.com has an English root and a Chinese section; other locales get English. */ +export const authorHome = (locale) => (locale === 'zh-CN' ? 'https://chaxus.com/zh/' : AUTHOR.url); diff --git a/bin/pages/entities.mjs b/bin/pages/entities.mjs index 7640c646a..6b5cc9311 100644 --- a/bin/pages/entities.mjs +++ b/bin/pages/entities.mjs @@ -2,7 +2,7 @@ * The JSON-LD entity graph: the three things this site is about, named once * and referred to by `@id` from every page. */ -import { ORIGIN, REPO, SITE_NAME } from './constants.mjs'; +import { AUTHOR, ORIGIN, REPO, SITE_NAME } from './constants.mjs'; import { LOCALES } from './locales.mjs'; /** @@ -22,6 +22,7 @@ export const ID = { site: `${ORIGIN}/#website`, app: `${ORIGIN}/#app`, source: `${ORIGIN}/#source`, + person: AUTHOR.id, }; /** The site identity is shared by every page, without promotional publisher branding. */ @@ -35,7 +36,11 @@ export const siteEntities = () => [ // and not about whichever page is being read. Each page states its own // language on its WebPage node. inLanguage: Object.keys(LOCALES), + author: { '@id': ID.person }, + publisher: { '@id': ID.person }, }, + // Defined here, once per page, so every reference to it resolves locally. + { '@type': 'Person', '@id': ID.person, name: AUTHOR.name, url: AUTHOR.url, sameAs: AUTHOR.sameAs }, ]; /** @@ -56,6 +61,8 @@ export const appEntity = (extra = {}) => ({ // The repository is the editor's other public identity, not the org's. sameAs: [REPO], isPartOf: { '@id': ID.site }, + author: { '@id': ID.person }, + publisher: { '@id': ID.person }, ...extra, }); diff --git a/bin/pages/render-home.mjs b/bin/pages/render-home.mjs index f156b0035..9959aca5e 100644 --- a/bin/pages/render-home.mjs +++ b/bin/pages/render-home.mjs @@ -5,7 +5,7 @@ * the decoration around them is layout. content//home.json holds the * strings; this holds the structure. */ -import { ORIGIN, REPO } from './constants.mjs'; +import { ORIGIN, REPO, authorHome } from './constants.mjs'; import { ID, appEntity, siteEntities, sourceEntity } from './entities.mjs'; import { DEFAULT_LOCALE, LOCALES } from './locales.mjs'; import { langMenu } from './chrome.mjs'; @@ -330,6 +330,7 @@ ${faq} ${e(data.foot.copy)} ${e(data.foot.license)} diff --git a/bin/pages/render-page.mjs b/bin/pages/render-page.mjs index 691a947e0..ffafc706c 100644 --- a/bin/pages/render-page.mjs +++ b/bin/pages/render-page.mjs @@ -2,7 +2,7 @@ * The template every generated page other than a homepage uses: prose from * markdown, dropped into the shared shell. */ -import { ORIGIN, REPO } from './constants.mjs'; +import { ORIGIN, REPO, authorHome } from './constants.mjs'; import { ID, appEntity, appStub, siteEntities, sourceEntity } from './entities.mjs'; import { DEFAULT_LOCALE, LOCALES } from './locales.mjs'; import { langMenu, routeFor } from './chrome.mjs'; @@ -223,6 +223,7 @@ ${aside} diff --git a/bin/pages/ui.mjs b/bin/pages/ui.mjs index 58e7a507c..f9e52a9fa 100644 --- a/bin/pages/ui.mjs +++ b/bin/pages/ui.mjs @@ -10,6 +10,7 @@ export const UI = { siteName: 'Document Editor', langAria: 'Language', themeLabel: 'Theme', + byAuthor: 'By chaxus', home: 'Home', openEditor: 'Open editor', onThisPage: 'On this page', @@ -42,6 +43,7 @@ export const UI = { siteName: 'Document Editor', langAria: '语言', themeLabel: '主题', + byAuthor: '作者 chaxus', home: '首页', openEditor: '打开编辑器', onThisPage: '本页目录', @@ -74,6 +76,7 @@ export const UI = { siteName: 'Document Editor', langAria: '言語', themeLabel: 'テーマ', + byAuthor: '作者:chaxus', home: 'ホーム', openEditor: 'エディタを開く', onThisPage: 'このページの目次', @@ -105,6 +108,7 @@ export const UI = { siteName: 'Document Editor', langAria: 'Sprache', themeLabel: 'Design', + byAuthor: 'Von chaxus', home: 'Startseite', openEditor: 'Editor öffnen', onThisPage: 'Auf dieser Seite', @@ -136,6 +140,7 @@ export const UI = { siteName: 'Document Editor', langAria: 'Idioma', themeLabel: 'Tema', + byAuthor: 'Por chaxus', home: 'Inicio', openEditor: 'Abrir el editor', onThisPage: 'En esta página', @@ -167,6 +172,7 @@ export const UI = { siteName: 'Document Editor', langAria: '언어', themeLabel: '테마', + byAuthor: '제작: chaxus', home: '홈', openEditor: '편집기 열기', onThisPage: '이 페이지의 목차', @@ -198,6 +204,7 @@ export const UI = { siteName: 'Document Editor', langAria: 'Idioma', themeLabel: 'Tema', + byAuthor: 'Por chaxus', home: 'Início', openEditor: 'Abrir o editor', onThisPage: 'Nesta página', diff --git a/docs/explorations/2026-10-28-author-links.md b/docs/explorations/2026-10-28-author-links.md new file mode 100644 index 000000000..6e222bc74 --- /dev/null +++ b/docs/explorations/2026-10-28-author-links.md @@ -0,0 +1,46 @@ +# Linking edit.chaxus.com back to its author + +chaxus.com links to edit.chaxus.com, but nothing on edit linked back. Search +engines and assistants therefore could not tell that edit, the ran sites and +chaxus.com share one author. The chaxus.com side is adding JSON-LD with a +Person node at `https://chaxus.com/#person`. This change makes edit refer to +the same node. + +## What changed + +- **Footer link.** Every generated page (homepages and content pages, all seven + locales) has a `rel="author"` link in the footer. zh-CN pages point to + `https://chaxus.com/zh/`; every other locale points to + `https://chaxus.com/`, since chaxus.com has only those two languages. The + link text is localized in `bin/pages/ui.mjs` (`byAuthor`). +- **Structured data.** `siteEntities()` now defines the Person once per page, + and `WebSite` and the full `WebApplication` node reference it as both + `author` and `publisher`. The `appStub()` on documentation pages stays a + stub. + - The values (`@id`, `name`, `url`, `sameAs`) live only in `AUTHOR` in + `bin/pages/constants.mjs`. + - They must match chaxus.com and ran exactly, because the graph merges on + `@id`. +- **llms.txt** states the author in its opening lines. + +## Tests and reverse validation + +`test/unit/landing-pages.test.ts` gains three checks: + +- every page that carries the site node names the Person, with exact values, + as author and publisher of the site and the full app node; +- every generated page links the author in its footer with the locale's URL; +- llms.txt names the author before its first section. + +With the generator, `index.html` and `llms.txt` changes reverted, all 355 new +cases fail (177 pages for each of the first two checks, plus llms.txt). With +the change, all pass. The existing graph check (no unresolved `@id` reference) +still passes, because the Person is defined in the same graph that refers to +it. + +## Rendered + +Checked in a real browser against the Vite dev server, at 1280 px on `/`, +`/zh-CN/` and `/help`, and at 390 px on `/` and `/zh-CN/help`. The link uses +the existing footer link style. The Chinese label wraps onto its own line on a +phone, and no page scrolls horizontally. diff --git a/index.html b/index.html index 36e05c44f..4854aa33f 100644 --- a/index.html +++ b/index.html @@ -98,6 +98,21 @@ "es", "ko", "pt" + ], + "author": { + "@id": "https://chaxus.com/#person" + }, + "publisher": { + "@id": "https://chaxus.com/#person" + } + }, + { + "@type": "Person", + "@id": "https://chaxus.com/#person", + "name": "chaxus", + "url": "https://chaxus.com/", + "sameAs": [ + "https://github.com/chaxus" ] }, { @@ -129,6 +144,12 @@ "isPartOf": { "@id": "https://edit.chaxus.com/#website" }, + "author": { + "@id": "https://chaxus.com/#person" + }, + "publisher": { + "@id": "https://chaxus.com/#person" + }, "description": "Open, view and edit DOCX, XLSX, PPTX and CSV in your browser without Office or an account. Core editing requires no document upload; offline use depends on cached resources.", "featureList": [ "Open and view DOCX, XLSX, PPTX and CSV without Microsoft Office", @@ -478,6 +499,7 @@

Questions, answered

XLSX → CSV Editor details GitHub + AGPL-3.0 diff --git a/public/llms.txt b/public/llms.txt index b4d007c20..669c23b6b 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -1,5 +1,7 @@ # Online Document Editor +By chaxus (https://chaxus.com/). + > A free, privacy-first document editor that runs entirely in the browser. Edit > Microsoft Word (DOCX), Excel (XLSX), PowerPoint (PPTX) and CSV files, and open > PDF files, with no required document upload or sign-up. Core editing and diff --git a/test/unit/landing-pages.test.ts b/test/unit/landing-pages.test.ts index 260c23957..34d624cad 100644 --- a/test/unit/landing-pages.test.ts +++ b/test/unit/landing-pages.test.ts @@ -179,6 +179,27 @@ describe('landing pages', () => { for (const ref of refs) expect(ids, `${ref} is defined in the graph`).toContain(ref); }); + /** + * chaxus.com and the ran sites describe the same person with the same + * @id, name and url; only identical values let a consumer merge them. + */ + it('names chaxus as author and publisher of the site and the editor', () => { + const graph = jsonLdGraph(html); + const site = graph.find((n) => n['@type'] === 'WebSite'); + if (!site) return; + const person = { '@id': 'https://chaxus.com/#person' }; + expect(graph.find((n) => n['@type'] === 'Person')).toEqual({ + '@type': 'Person', + ...person, + name: 'chaxus', + url: 'https://chaxus.com/', + sameAs: ['https://github.com/chaxus'], + }); + expect([site.author, site.publisher]).toEqual([person, person]); + const app = graph.find((n) => n['@type'] === 'WebApplication' && n.applicationCategory); + if (app) expect([app.author, app.publisher]).toEqual([person, person]); + }); + /** * A visitor who picked a language on the site must not land in an editor * that guesses one from browser settings. The app resolves `?locale=` @@ -566,6 +587,12 @@ describe('page shell', () => { it.each(pages)('$label has exactly one main landmark', ({ html, label }) => { expect((html.match(/]/g) ?? []).length, label).toBe(1); }); + /** The way back to the author's site; chaxus.com has only English and Chinese. */ + it.each(GENERATED)('$rel links its author in the footer', ({ html, route, rel }) => { + const footer = html.match(//)?.[0] ?? ''; + const href = localeOf(route) === 'zh-CN' ? 'https://chaxus.com/zh/' : 'https://chaxus.com/'; + expect(footer, rel).toContain(`href="${href}" rel="author"`); + }); }); /** @@ -583,6 +610,10 @@ describe('llms.txt', () => { .filter((section) => /^(Links|Pages)\b/.test(section)) .flatMap((section) => section.split('\n').filter((line) => line.startsWith('- '))); + it('names its author up front', () => { + expect(llms.split('\n## ')[0]).toContain('By chaxus (https://chaxus.com/)'); + }); + it('lists its pages as markdown links', () => { expect(linkBullets.length).toBeGreaterThan(20); const unlinked = linkBullets.filter((line) => !/^- \[[^\]]+\]\(\S+\)/.test(line));