diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index c9e7ff1dd..a841f8137 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -266,6 +266,22 @@ Four checks, by href shape: | absolute `/docs/...` | `content/docs/` as a **route** | no `foo.md`, `foo.mdx` or `foo/index.md*` backs it — a `.md`/`.mdx` suffix always fails, since that URL 404s whatever is on disk | | any other absolute (`/spec/...`, `/img/...`) | the **site itself**: route segments enumerated from `apps/site/app`, plus static files under `apps/site/public` | no route pattern or static file matches | +**Two href shapes are checked on _every_ surface, both rules included**, because they look external +but are decidable offline: + +| Href shape | Resolved against | Rejected when | +|---|---|---| +| this repo's own `https://github.com/objectstack-ai/objectui/(blob\|tree)/main/...` (objectui#3536) | the path in the working tree | that path is not in the checkout. Only `main` and only this repo — other refs and repos cannot be answered offline | +| this site's own `https://[www.]objectui.org/...` (objectui#3603) | the origin is stripped, and what remains goes through the two absolute rows above, unchanged | the resulting route does not resolve — so `…/docs/guide/foo.md` fails for exactly the reason `/docs/guide/foo.md` does | + +The second one had been invisible since the beginning: `judgeHref()` skipped every href carrying a +scheme, so a route written with the site's own origin was never checked while the identical +origin-less route was checked strictly. That blind spot was never confined to package READMEs — +`content/docs/` writes 6 such URLs itself — which is why the fix strips the origin in `judgeHref()` +rather than special-casing any surface. Measured before landing: 11 across the scanned tree, zero +dead. Prefer the origin-less form (`/docs/guide/plugins`) in new prose: it survives a domain change, +and both spellings are now checked identically. + Every other surface — `examples/`, `README.md`, `CONTRIBUTING.md`, `ROADMAP.md`, `docs/` — is read on **GitHub**, not served by the site, so a relative href there names a path on disk and is checked for existence only: a directory (`./packages/core`) or a non-markdown file (`./vite.config.ts`) is a @@ -334,7 +350,7 @@ There are **two** link checkers, and they cover different things (objectui#3213) | | Covers | Network | Runs | |---|---|---|---| -| `scripts/check-doc-links.mjs` | **Internal** links in `content/docs/` (relative hrefs, `/docs/...` routes, every other site-absolute href against `apps/site`), in `examples/`, `README.md`, `CONTRIBUTING.md`, `ROADMAP.md` and `docs/` (as paths on disk), plus this repo's own `blob/main/` and `tree/main/` GitHub URLs everywhere — **except** anything inside a code fence | No | `docs-links.yml` — every push and PR, no path filter (previous section) | +| `scripts/check-doc-links.mjs` | **Internal** links in `content/docs/` (relative hrefs, `/docs/...` routes, every other site-absolute href against `apps/site`), in `examples/`, `README.md`, `CONTRIBUTING.md`, `ROADMAP.md` and `docs/` (as paths on disk), plus this repo's own `blob/main/` and `tree/main/` GitHub URLs and this site's own `objectui.org` URLs everywhere — **except** anything inside a code fence | No | `docs-links.yml` — every push and PR, no path filter (previous section) | | Lychee (this workflow) | **External** URLs, plus **relative** in-repo file links, in `content/docs/`, `docs/` and `README.md` | Yes | Weekly cron and manual dispatch | Lychee sweeps **both** documentation trees: `content/docs/` (the 183 pages the site publishes) and diff --git a/packages/auth/README.md b/packages/auth/README.md index 5a7dfdbe9..cd7f6d112 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -220,7 +220,6 @@ function MyComponent() { ## Links -- 📚 [Documentation](https://www.objectui.org/docs/packages/auth) - 📦 [npm package](https://www.npmjs.com/package/@object-ui/auth) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) diff --git a/packages/collaboration/README.md b/packages/collaboration/README.md index cbf4df969..4e5759df6 100644 --- a/packages/collaboration/README.md +++ b/packages/collaboration/README.md @@ -129,7 +129,6 @@ Threaded comment component with @mentions: ## Links -- 📚 [Documentation](https://www.objectui.org/docs/packages/collaboration) - 📦 [npm package](https://www.npmjs.com/package/@object-ui/collaboration) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) diff --git a/packages/i18n/README.md b/packages/i18n/README.md index 567b929d2..6ae613e76 100644 --- a/packages/i18n/README.md +++ b/packages/i18n/README.md @@ -149,7 +149,6 @@ isRTL('en'); // false ## Links -- 📚 [Documentation](https://www.objectui.org/docs/packages/i18n) - 📦 [npm package](https://www.npmjs.com/package/@object-ui/i18n) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) diff --git a/packages/mobile/README.md b/packages/mobile/README.md index dbf8f5aab..20da3c3e4 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -140,7 +140,6 @@ registerServiceWorker({ cacheStrategy: 'network-first' }); ## Links -- 📚 [Documentation](https://www.objectui.org/docs/packages/mobile) - 📦 [npm package](https://www.npmjs.com/package/@object-ui/mobile) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) diff --git a/packages/permissions/README.md b/packages/permissions/README.md index 8cd9499f7..c7a916454 100644 --- a/packages/permissions/README.md +++ b/packages/permissions/README.md @@ -134,7 +134,6 @@ store.check('read', 'orders'); // true | false ## Links -- 📚 [Documentation](https://www.objectui.org/docs/packages/permissions) - 📦 [npm package](https://www.npmjs.com/package/@object-ui/permissions) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) diff --git a/packages/providers/README.md b/packages/providers/README.md index e6aa3b322..1b28a345f 100644 --- a/packages/providers/README.md +++ b/packages/providers/README.md @@ -79,7 +79,6 @@ function App() { ## Links -- 📚 [Documentation](https://www.objectui.org/docs/packages/providers) - 📦 [npm package](https://www.npmjs.com/package/@object-ui/providers) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) diff --git a/packages/react/README.md b/packages/react/README.md index 33954a839..3de10048e 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -240,7 +240,7 @@ See [full documentation](https://objectui.org/api/react) for detailed API refere ## Links -- 📚 [Documentation](https://www.objectui.org/docs/packages/react) +- 📚 [Documentation](https://www.objectui.org/docs/core/schema-renderer) - 📦 [npm package](https://www.npmjs.com/package/@object-ui/react) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues) diff --git a/packages/vscode-extension/README.md b/packages/vscode-extension/README.md index e35fe5c64..43a93171a 100644 --- a/packages/vscode-extension/README.md +++ b/packages/vscode-extension/README.md @@ -185,8 +185,8 @@ pnpm publish ## 📚 Documentation - [Object UI Documentation](https://www.objectui.org) -- [Schema Reference](https://www.objectui.org/docs/protocol/overview) -- [Component Library](https://www.objectui.org/docs/api/components) +- [Schema Reference](https://www.objectui.org/docs/api/schema-reference) +- [Component Library](https://www.objectui.org/docs/components) - [Examples](https://www.objectui.org/examples) ## 🤝 Contributing diff --git a/scripts/__tests__/check-doc-links.test.ts b/scripts/__tests__/check-doc-links.test.ts index 8cd71435c..5662e6c25 100644 --- a/scripts/__tests__/check-doc-links.test.ts +++ b/scripts/__tests__/check-doc-links.test.ts @@ -12,6 +12,7 @@ import { diskPathExists, routeExists, selfRepoPath, + siteAbsoluteRoute, siteUrlExists, stripCode, } from '../check-doc-links.mjs'; @@ -68,6 +69,22 @@ import { * the FENCE BOUNDARY — `stripCode()` hides a link inside a code fence from this * gate, dead or not, and that limit is pinned as a decision rather than left to * be discovered as coverage that was never there (#3570 is the live instance). + * + * objectui#3603 closed the second half of the by-scheme skip, and the last + * describe pins it: a URL on this project's OWN site + * (`https://www.objectui.org/docs/...`) is an internal route wearing an origin, + * and `judgeHref()` waved every one of them through. It is now stripped to an + * absolute site path and handed to the same `routeExists()` — so the tests that + * matter most there are the INHERITANCE ones: a `.md` extension is rejected + * because the `/docs` branch is strict, and a non-`/docs` path is judged by the + * `apps/site` route table. Neither behaviour is re-implemented, and these tests + * fail if either is. + * + * That card's OTHER half — adding the package READMEs to SCAN_ROOTS — was + * deliberately not bought, because measuring it first turned up 11 more dead + * links in packages the card never touched (objectui#3622). Nothing here pins + * a `packages/**` row for that reason; when its backlog is paid, the SCAN_ROOTS + * assertions below are what will need extending. */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); @@ -938,9 +955,10 @@ describe("this repo's own GitHub blob/tree URLs are resolved offline — objectu }); describe('every failure carries the reason that rejected it', () => { - it('labels all seven checks distinctly', () => { - // The #3490 test of the same name, extended with objectui#3536's three. - // Every reason here must have a HINTS entry — the next test pins that. + it('labels all eight checks distinctly', () => { + // The #3490 test of the same name, extended with objectui#3536's three and + // objectui#3603's one. Every reason here must have a HINTS entry — the next + // test pins that. const repo = repoWith({ ...SITE_FIXTURE, 'content/docs/guide/a.md': [ @@ -949,6 +967,7 @@ describe('every failure carries the reason that rejected it', () => { '[docs](/docs/guide/a.md)', '[rel](./nowhere.md)', '[self](https://github.com/objectstack-ai/objectui/blob/main/packages/gone/README.md)', + '[origin](https://www.objectui.org/docs/guide/gone)', ].join('\n\n'), 'examples/README.md': '[disk](./nowhere.md) and [abs](/packages/core)', }); @@ -959,6 +978,7 @@ describe('every failure carries the reason that rejected it', () => { 'docs-route', 'relative', 'self-repo-url', + 'site-absolute-url', 'example-relative', 'example-absolute', ]); @@ -980,7 +1000,158 @@ describe('every failure carries the reason that rejected it', () => { 'example-relative', 'relative', 'self-repo-url', + 'site-absolute-url', 'site-route', ]); }); }); + +describe("this site's own absolute URLs are resolved as internal routes — objectui#3603", () => { + it('reports the shape all 9 package-README links had: a full site URL to no page', () => { + // The assertion that dies if `if (EXTERNAL_HREF_RE.test(href)) return null;` + // is ever allowed to reach a site-absolute URL again. `/docs/packages/*` is + // the namespace 7 package READMEs invented; it has never existed. + expect( + brokenHrefs({ + 'guide/a.md': '[docs](https://www.objectui.org/docs/packages/auth)', + }), + ).toEqual(['https://www.objectui.org/docs/packages/auth']); + }); + + it('accepts the same URL once it names a real page', () => { + expect( + brokenHrefs({ + 'guide/a.md': '[renderer](https://www.objectui.org/docs/core/schema-renderer)', + 'core/schema-renderer.mdx': '# SchemaRenderer', + }), + ).toEqual([]); + }); + + it('judges both host spellings — this tree really carries www and bare', () => { + // 109 `www.objectui.org` against 9 bare `objectui.org` on main. Accepting + // only the majority spelling would leave the other 9 in the blind spot the + // whole change exists to close. + expect( + brokenHrefs({ + 'guide/a.md': [ + '[www dead](https://www.objectui.org/docs/gone)', + '[bare dead](https://objectui.org/docs/gone)', + '[www live](https://www.objectui.org/docs/guide/a)', + '[bare live](https://objectui.org/docs/guide/a)', + ].join('\n\n'), + }), + ).toEqual(['https://www.objectui.org/docs/gone', 'https://objectui.org/docs/gone']); + }); + + it('INHERITS /docs strictness — an extension 404s here exactly as it does origin-less', () => { + // The test that proves the origin is stripped and the rest handed to the + // existing `routeExists()`, rather than judged by some new parallel path. + // `guide/b.md` is materialised: only the strict `/docs` branch rejects it. + expect( + brokenHrefs({ + 'guide/a.md': '[b](https://www.objectui.org/docs/guide/b.md)', + 'guide/b.md': '# B', + }), + ).toEqual(['https://www.objectui.org/docs/guide/b.md']); + }); + + it('INHERITS the apps/site route table for non-/docs paths', () => { + // `/playground` and `/` are real routes in the site fixture; `/examples` is + // the prefix #3490 proved has never been one. Same table, same verdicts. + expect( + brokenHrefs({ + 'guide/a.md': [ + '[examples](https://www.objectui.org/examples)', + '[playground](https://www.objectui.org/playground)', + '[home](https://www.objectui.org)', + '[home slash](https://www.objectui.org/)', + ].join('\n\n'), + }), + ).toEqual(['https://www.objectui.org/examples']); + }); + + it('checks site URLs in the disk surfaces too, not only content/docs', () => { + // One rejection from EACH tree, for the reason the fence test spells out: + // an expectation naming only the content/docs one would hold just as well + // if the check never ran on the disk rule, and would prove nothing. + const repo = repoWith({ + ...SITE_FIXTURE, + 'content/docs/guide/a.md': '[gone](https://www.objectui.org/docs/gone)', + 'README.md': '[gone](https://objectui.org/docs/gone)', + 'examples/hello-world/README.md': '[live](https://www.objectui.org/docs/guide/a)', + }); + + expect(scan(repo).map((item) => [path.relative(repo, item.file), item.reason])).toEqual([ + [path.join('content', 'docs', 'guide', 'a.md'), 'site-absolute-url'], + ['README.md', 'site-absolute-url'], + ]); + }); + + it('leaves every other origin alone, a lookalike host included', () => { + // The regex is anchored at both ends precisely so `objectui.org.example.com` + // cannot borrow this repo's route table — it is somebody else's host, and + // resolving it here would invent 404s that are not ours to report. + expect( + brokenHrefs({ + 'guide/a.md': [ + '[lookalike](https://objectui.org.example.com/docs/gone)', + '[prefixed](https://notobjectui.org/docs/gone)', + '[other](https://example.com/docs/gone)', + '[docs of another](https://react.dev/docs/gone)', + ].join('\n\n'), + }), + ).toEqual([]); + }); + + it('ignores the fragment and the query on a site URL — anchors are out of scope', () => { + expect( + brokenHrefs({ + 'guide/a.md': [ + '[live](https://www.objectui.org/docs/guide/a#install)', + '[live q](https://www.objectui.org/docs/guide/a?x=1)', + '[dead](https://www.objectui.org/docs/gone#install)', + ].join('\n\n'), + }), + ).toEqual(['https://www.objectui.org/docs/gone#install']); + }); + + it('exposes siteAbsoluteRoute: the path it extracts, and the shapes it declines', () => { + expect(siteAbsoluteRoute('https://www.objectui.org/docs/guide/plugins')).toBe('/docs/guide/plugins'); + expect(siteAbsoluteRoute('https://objectui.org/api/core')).toBe('/api/core'); + expect(siteAbsoluteRoute('http://objectui.org/docs')).toBe('/docs'); + // No path at all is the site ROOT, not the empty string — `routeExists()` + // reads '' as a pure in-page anchor and would wave it through. + expect(siteAbsoluteRoute('https://www.objectui.org')).toBe('/'); + expect(siteAbsoluteRoute('https://www.objectui.org/docs/guide/a#x')).toBe('/docs/guide/a'); + expect(siteAbsoluteRoute('https://objectui.org.example.com/docs')).toBeNull(); + expect(siteAbsoluteRoute('https://example.com/docs')).toBeNull(); + expect(siteAbsoluteRoute('/docs/guide/plugins')).toBeNull(); + }); + + it('really judges the site-absolute URLs this repo carries — the floor under the green', () => { + // objectui#3603's anti-vacuous-green assertion, and the one this change + // needs most. Unlike #3536 and #3572 this card added NO scan root, so the + // repo-wide "no broken internal links" test above stays green whether or + // not the new resolution runs at all — it would pass just as well if + // `siteAbsoluteRoute()` returned null for everything. This pins that the + // URLs are really there and really reaching the check. + // + // Measured on main: 11 of them, of which 6 sit inside `content/docs` + // itself — the fact that makes this a general fix rather than a package + // README one — and 3 carry a `/docs/...` path, so the strict branch above + // is genuinely exercised by real content and not only by fixtures. + const seen: { root: string; route: string }[] = []; + for (const root of SCAN_ROOTS as { path: string }[]) { + for (const file of collectFiles(path.join(repoRoot, root.path)) as string[]) { + for (const match of stripCode(fs.readFileSync(file, 'utf8')).matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) { + const route = siteAbsoluteRoute(match[1].trim()); + if (route !== null) seen.push({ root: root.path, route }); + } + } + } + + expect(seen.length).toBeGreaterThanOrEqual(11); + expect(seen.filter((item) => item.root === 'content/docs').length).toBeGreaterThanOrEqual(6); + expect(seen.filter((item) => item.route.startsWith('/docs')).length).toBeGreaterThanOrEqual(3); + }); +}); diff --git a/scripts/check-doc-links.mjs b/scripts/check-doc-links.mjs index 63e08634c..b77ef08c0 100644 --- a/scripts/check-doc-links.mjs +++ b/scripts/check-doc-links.mjs @@ -229,6 +229,66 @@ * unscanned. Same one-row price, same caveat — measure the surface first, pay * its backlog separately, then add the row. * + * ## Why this file changed again (objectui#3603) + * + * A second scheme-bearing shape that is decidable here, and the exact mirror of + * the self-repo blob URLs above: **this site's own absolute URLs**. + * + * `judgeHref()` skipped every href carrying a scheme (`EXTERNAL_HREF_RE`), with + * `SELF_REPO_BLOB_RE` as the single exception. So + * `https://www.objectui.org/docs/guide/plugins` — a route on the very site this + * repo publishes, whose whole route table is already enumerated above to serve + * the `/docs/...` and `/...` branches — was never resolved, while the identical + * route written `/docs/guide/plugins` was checked strictly. The origin is the + * only difference between the two, and it is the one part of a URL this script + * can be certain about. + * + * **Not a package-README special case, and deliberately not written as one.** + * The blind spot lives in `judgeHref()`, so it was never confined to any + * surface: `content/docs` carries 6 site-absolute URLs of its own and the root + * `README.md` 5, and all 11 were unchecked until now. Stripping the origin and + * handing the rest to `routeExists()` fixes the class everywhere at once, which + * is also why it needs no new rule and no new scan root. + * + * Both host spellings are live in this tree (`www.objectui.org` and bare + * `objectui.org`), so both are accepted; every other origin stays external and + * remains lychee's problem. Because what survives the strip is an ordinary + * absolute site path, it inherits BOTH branches of `routeExists()` unchanged — + * `/docs` strictness included. `https://www.objectui.org/docs/guide/plugins.md` + * is rejected for precisely the reason `/docs/guide/plugins.md` is. + * + * Measured before landing: 11 site-absolute URLs across today's surfaces, zero + * dead. The #3572 shape again — the check arrives green, its backlog already + * paid. + * + * ### Measured and NOT bought: the package READMEs (objectui#3622) + * + * objectui#3603 asked for a second half: growing the scan surface by the 38 + * package READMEs, which is where the 9 dead links that prompted it lived. + * Those 9 are fixed by hand in the same PR. The scan row is **not** added, and + * the reason is this file's own rule from #3572 — measure the surface, pay its + * backlog separately, then add the row. + * + * (Note for whoever writes that row: a glob naming each package README cannot + * be spelled inside this block comment, because the star-slash in it closes the + * comment. Say it in prose here, or move the note outside.) + * + * The surface was measured, and its backlog is not the 9. With the resolution + * above in place, a `disk`-rule row covering those 38 files + * reports **11 more** dead links in five packages that card never touched: + * `/api/core`, `/api/react`, `/api/components` (no such routes — the class + * #3490 swept), `/docs/core`, `/docs/fields`, `/docs/layout` (real directories + * with no index page, so no route), `/docs/types` and `/examples` (neither + * exists), plus two disk paths that are simply absent + * (`../../docs/SHADCN_SYNC.md`, `./LICENSE`). + * + * Three of those deserve naming, because objectui#3603's own verification + * script scored them GREEN: it accepted a bare DIRECTORY as a fumadocs + * candidate. This gate does not, and is right not to — `routeCandidates()` has + * no such spelling, and the pinned test "rejects a relative link to a directory + * that has no index page" is that decision. `content/docs/core/` really has no + * index page, so `/docs/core` really does 404. The class is 20 links, not 9. + * * ## Code spans are stripped before scanning * * Required, not tidiness. Extending the scan to relative hrefs turns markdown's @@ -265,6 +325,15 @@ const ROUTE_ENTRY_RE = /^(?:page|route)\.(?:js|jsx|ts|tsx|md|mdx)$/; * treats owner and repo that way; the captured path is used as written. */ const SELF_REPO_BLOB_RE = /^https:\/\/github\.com\/objectstack-ai\/objectui\/(?:blob|tree)\/main\/(.+)$/i; +/** + * This site's own origin — the second decidable scheme-bearing shape + * (objectui#3603). Both host spellings are in use in this tree, and the site + * serves the same routes under each. Anchored at both ends so a lookalike host + * (`objectui.org.example.com`) cannot match; the captured group is the absolute + * site path, which `routeExists()` then judges exactly as if it had been + * written without the origin. + */ +const SITE_ORIGIN_RE = /^https?:\/\/(?:www\.)?objectui\.org(\/[^\s]*)?$/i; /** Never markdown sources of ours, and huge — walking them wastes the scan. */ const UNSCANNED_DIRS = new Set(['node_modules', 'dist', 'build', '.next', '.turbo', '.git']); @@ -401,6 +470,23 @@ export function selfRepoPath(href) { return match ? match[1].replace(/\/+$/, '') : null; } +/** + * The absolute site path a site-absolute URL names, or `null` if this href is + * not one (objectui#3603). The fragment and query are stripped — anchors are + * out of scope here for the same reason they are for `selfRepoPath` — but the + * path is NOT decoded: `routeExists()` does that itself, and decoding twice + * would corrupt a legitimately escaped `%25`. + * + * `https://www.objectui.org` with no path at all names the site root, so it + * resolves to `/` rather than to the empty string (which `routeExists()` reads + * as a pure in-page anchor and waves through). + */ +export function siteAbsoluteRoute(href) { + const match = SITE_ORIGIN_RE.exec(href.split('#')[0].split('?')[0].trim()); + if (!match) return null; + return match[1] || '/'; +} + /** * Does one `disk`-rule href resolve to something in this repository? * @@ -592,14 +678,26 @@ function classifyBrokenDisk(href) { * @returns {string | null} the failing check's name, or `null` when it resolves */ function judgeHref(href, context) { - // The self-repo URL check runs FIRST and in every rule: it is the one - // external-looking shape this script can decide, so it must be reached - // before the by-scheme skip below waves it through (objectui#3536). + // The two external-LOOKING shapes this script can actually decide run FIRST, + // and in every rule: both must be reached before the by-scheme skip below + // waves them through (objectui#3536, objectui#3603). const selfPath = selfRepoPath(href); if (selfPath !== null) { const target = path.resolve(context.repoRoot, selfPath); return isInside(context.repoRoot, target) && pathExists(target) ? null : 'self-repo-url'; } + // A URL on this site is an internal route wearing an origin. Strip the origin + // and judge what is left with the same `routeExists()` every absolute href + // goes through — so `/docs` strictness and the `apps/site` route table apply + // here too, on every surface, `content/docs` included. + // A URL on this site is an internal route wearing an origin. Strip the origin + // and judge what is left with the same `routeExists()` every absolute href + // goes through — so `/docs` strictness and the `apps/site` route table apply + // here too, on every surface, `content/docs` included. + const siteRoute = siteAbsoluteRoute(href); + if (siteRoute !== null) { + return routeExists(siteRoute, context) ? null : 'site-absolute-url'; + } if (EXTERNAL_HREF_RE.test(href)) return null; if (context.rule === 'disk') { @@ -678,6 +776,13 @@ const HINTS = { ' https://github.com/packages/core. Write the repo path relative to the' + ' file (`./packages/core`), or, if a github.com URL really was meant,' + ' write it in full with the scheme.', + 'site-absolute-url': + 'A `https://www.objectui.org/...` URL is a route on this project OWN site,' + + ' so the origin is stripped and the rest is checked exactly like the' + + ' equivalent absolute href — this one does not resolve. Fix the route, or' + + ' drop the link if no such page exists. Inside content/docs prefer the' + + ' origin-less form (`/docs/guide/plugins`): it survives a domain change,' + + ' and both spellings are checked identically.', 'self-repo-url': 'A `https://github.com/objectstack-ai/objectui/(blob|tree)/main/...` URL' + ' points into this repository, so its path is checked against the working' +