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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 复现不了,只能靠冒烟兜底。
Expand Down
14 changes: 14 additions & 0 deletions bin/pages/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
9 changes: 8 additions & 1 deletion bin/pages/entities.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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. */
Expand All @@ -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 },
];

/**
Expand All @@ -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,
});

Expand Down
3 changes: 2 additions & 1 deletion bin/pages/render-home.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* the decoration around them is layout. content/<locale>/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';
Expand Down Expand Up @@ -330,6 +330,7 @@ ${faq}
<span>${e(data.foot.copy)}</span>
<nav>
${footLinks}
<a href="${authorHome(locale)}" rel="author">${e(ui.byAuthor)}</a>
</nav>
<r-theme-switch class="theme-switch" label="${e(ui.themeLabel)}"></r-theme-switch>
<span class="lic">${e(data.foot.license)}</span>
Expand Down
3 changes: 2 additions & 1 deletion bin/pages/render-page.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -223,6 +223,7 @@ ${aside} </div>
<footer class="page-foot">
${footer}
<a href="${REPO}" rel="noopener">GitHub</a>
<a href="${authorHome(locale)}" rel="author">${escapeHtml(ui.byAuthor)}</a>
<r-theme-switch class="theme-switch" label="${ui.themeLabel}"></r-theme-switch>
<p class="tm">${escapeHtml(ui.trademark)}</p>
</footer>
Expand Down
7 changes: 7 additions & 0 deletions bin/pages/ui.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -42,6 +43,7 @@ export const UI = {
siteName: 'Document Editor',
langAria: '语言',
themeLabel: '主题',
byAuthor: '作者 chaxus',
home: '首页',
openEditor: '打开编辑器',
onThisPage: '本页目录',
Expand Down Expand Up @@ -74,6 +76,7 @@ export const UI = {
siteName: 'Document Editor',
langAria: '言語',
themeLabel: 'テーマ',
byAuthor: '作者:chaxus',
home: 'ホーム',
openEditor: 'エディタを開く',
onThisPage: 'このページの目次',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -167,6 +172,7 @@ export const UI = {
siteName: 'Document Editor',
langAria: '언어',
themeLabel: '테마',
byAuthor: '제작: chaxus',
home: '홈',
openEditor: '편집기 열기',
onThisPage: '이 페이지의 목차',
Expand Down Expand Up @@ -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',
Expand Down
46 changes: 46 additions & 0 deletions docs/explorations/2026-10-28-author-links.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -478,6 +499,7 @@ <h2>Questions, answered</h2>
<a href="/convert/xlsx-to-csv">XLSX → CSV</a>
<a href="/onlyoffice-online-free">Editor details</a>
<a href="https://github.com/ranuts/document">GitHub</a>
<a href="https://chaxus.com/" rel="author">By chaxus</a>
</nav>
<r-theme-switch class="theme-switch" label="Theme"></r-theme-switch>
<span class="lic">AGPL-3.0</span>
Expand Down
2 changes: 2 additions & 0 deletions public/llms.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
31 changes: 31 additions & 0 deletions test/unit/landing-pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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=`
Expand Down Expand Up @@ -566,6 +587,12 @@ describe('page shell', () => {
it.each(pages)('$label has exactly one main landmark', ({ html, label }) => {
expect((html.match(/<main[\s>]/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(/<footer[\s\S]*?<\/footer>/)?.[0] ?? '';
const href = localeOf(route) === 'zh-CN' ? 'https://chaxus.com/zh/' : 'https://chaxus.com/';
expect(footer, rel).toContain(`href="${href}" rel="author"`);
});
});

/**
Expand All @@ -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));
Expand Down
Loading