{"_id":"@domandigital/seo","_rev":"5-5626c63b36da13dfcec104cb8c0d1c28","name":"@domandigital/seo","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.1":{"name":"@domandigital/seo","version":"0.1.1","license":"Apache-2.0","_id":"@domandigital/seo@0.1.1","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"homepage":"https://github.com/Doman-Digital/dd-seo#readme","bugs":{"url":"https://github.com/Doman-Digital/dd-seo/issues"},"dist":{"shasum":"44343939fe9d89b9aca1ce49b9cc740e230c2ad7","tarball":"https://registry.npmjs.org/@domandigital/seo/-/seo-0.1.1.tgz","fileCount":7,"integrity":"sha512-H56+PwIEOzqB2W0HtPvE70E3me+xRhhukl6MzHLeAhIFSiEOvPTmTXT4qJBAFghACV4qaXbrTD8ySCnF2bDt3Q==","signatures":[{"sig":"MEUCIQDDglfaTbKiACqM88GKciJYbSw8s+xr+TAY5Bj3DmikygIgV1W4K7931B1wyi6AX/KWqoxa2cLia2uq5ETntSZ4nXg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":39217},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"43ae57af634e647e02a52a44b74c19e0b0e3ea8f","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"domandigital","email":"admin@domandigital.co.uk"},"repository":{"url":"git+https://github.com/Doman-Digital/dd-seo.git","type":"git"},"_npmVersion":"11.8.0","description":"Route-fact primitives shared across Doman Digital and client properties: sitemap/indexability policy, per-page keyword targets, the internal-link graph, breadcrumb trail derivation, and a filesystem-vs-policy coverage validator.","directories":{},"_nodeVersion":"24.13.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@9.15.9","devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3"},"_npmOperationalInternal":{"tmp":"tmp/seo_0.1.1_1786897806499_0.8675401650226757","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@domandigital/seo","version":"0.1.2","license":"Apache-2.0","_id":"@domandigital/seo@0.1.2","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"homepage":"https://github.com/Doman-Digital/dd-seo#readme","bugs":{"url":"https://github.com/Doman-Digital/dd-seo/issues"},"dist":{"shasum":"4599bce2c957aeccd967ac46490c356e05780746","tarball":"https://registry.npmjs.org/@domandigital/seo/-/seo-0.1.2.tgz","fileCount":8,"integrity":"sha512-q0iX4xjgC3gAHSSBibzh06LwBCcU4RXP/d3SluHRBE4QO+Om+4uyc+qq1Hptt4XU7zQx9TqVDYC+lwVdu+TgDw==","signatures":[{"sig":"MEQCIG/3ySbU/rm9seu0aJwH7IrKS13oNbS1I+Fg96sxg+Q8AiBPwLLM8AocrlGOUaUdYSzQ6ZL86A0Rw2ikvHEGb6WZVg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domandigital%2fseo@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":43319},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"943a8296c4c2b5158d3fc9f836f4dcaa0f6d8fc0","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:6efe5b19-3a03-429b-9c06-0f433ab37c28"}},"repository":{"url":"git+https://github.com/Doman-Digital/dd-seo.git","type":"git"},"_npmVersion":"12.0.2","description":"Route-fact primitives shared across Doman Digital and client properties: sitemap/indexability policy, per-page keyword targets, the internal-link graph, breadcrumb trail derivation, and a filesystem-vs-policy coverage validator.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@9.15.9","devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3"},"_npmOperationalInternal":{"tmp":"tmp/seo_0.1.2_1786908781451_0.2281273333687417","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@domandigital/seo","version":"0.1.3","license":"Apache-2.0","_id":"@domandigital/seo@0.1.3","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"homepage":"https://github.com/Doman-Digital/dd-seo#readme","bugs":{"url":"https://github.com/Doman-Digital/dd-seo/issues"},"dist":{"shasum":"14922ae8ada80dde245b36f2c9a86c4f4c85c641","tarball":"https://registry.npmjs.org/@domandigital/seo/-/seo-0.1.3.tgz","fileCount":8,"integrity":"sha512-mj/WSUZe50fu9B9BvcPnH+lg8ECxUvm6ZSGIKJRXzpguntlyl9wAvGZBy1rlD2ogVQzWjzS/0TcxJOCCkfv++g==","signatures":[{"sig":"MEUCIQDv1XkbmtUfCWu+JE/qxhvKZb0N+G1lHBjAbsRwCX30nQIgSbph2N6YR+Ye0+UefJLPFmkbT9sTsAGy1hf6R9FtonQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domandigital%2fseo@0.1.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":45513},"main":"dist/index.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"bfd79c961754788ea4bc5826679a3a7f9f6c191b","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:6efe5b19-3a03-429b-9c06-0f433ab37c28"}},"repository":{"url":"git+https://github.com/Doman-Digital/dd-seo.git","type":"git"},"_npmVersion":"12.0.2","description":"Route-fact primitives shared across Doman Digital and client properties: sitemap/indexability policy, per-page keyword targets, the internal-link graph, breadcrumb trail derivation, and a filesystem-vs-policy coverage validator.","directories":{},"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"packageManager":"pnpm@9.15.9","devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3"},"_npmOperationalInternal":{"tmp":"tmp/seo_0.1.3_1786911380002_0.01220183677128861","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@domandigital/seo","version":"0.3.0","license":"Apache-2.0","_id":"@domandigital/seo@0.3.0","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"homepage":"https://github.com/Doman-Digital/dd-packages/tree/main/packages/seo#readme","bugs":{"url":"https://github.com/Doman-Digital/dd-packages/issues"},"dist":{"shasum":"ac979a359e37f88bfe4d0413fa6a83f07969bd36","tarball":"https://registry.npmjs.org/@domandigital/seo/-/seo-0.3.0.tgz","fileCount":10,"integrity":"sha512-ecgCb8wl5aTtzJd/P3Kz+8FwG+G7s85MI661oDYTSHHB9QdkOddX+FtgJCwdtd0AfTkTb7frhIQixAImRDxE4A==","signatures":[{"sig":"MEUCIDJhekrxvHMeMYse/O7xogw/F27PONehvAi1HWOY4IVcAiEAzen4K4FhKKDeOSMdLNgPxkdZn7TifKxQ4P0hJzWW4TE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQDBkAzGdgCnbMLUNDBcNQMs1B9IE8okh1xj8M6LIayaUQIgFiek0FwHpK3BY6CQ6zwYpo1jprl5US+jccDbTERkYt8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domandigital%2fseo@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":84644},"main":"dist/index.cjs","type":"module","_from":"file:domandigital-seo-0.3.0.tgz","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=22.12"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json","./links.schema.json":"./links.schema.json","./redirects.schema.json":"./redirects.schema.json"},"private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:fd3a220b-21f5-4b5c-9516-ce5dd9b1bf64"}},"_resolved":"/tmp/0fe93fbd2bb6eabfc10cd079bd9b7825/domandigital-seo-0.3.0.tgz","_integrity":"sha512-ecgCb8wl5aTtzJd/P3Kz+8FwG+G7s85MI661oDYTSHHB9QdkOddX+FtgJCwdtd0AfTkTb7frhIQixAImRDxE4A==","repository":{"url":"git+https://github.com/Doman-Digital/dd-packages.git","type":"git","directory":"packages/seo"},"_npmVersion":"11.20.0","description":"Route-fact primitives shared across Doman Digital and client properties: sitemap/indexability policy, per-page keyword targets, the internal-link graph, breadcrumb trail derivation, and a filesystem-vs-policy coverage validator.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3"},"_npmOperationalInternal":{"tmp":"tmp/seo_0.3.0_1790214917827_0.2672640067653522","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"_id":"@domandigital/seo@0.3.1","bugs":{"url":"https://github.com/Doman-Digital/dd-packages/issues"},"dist":{"shasum":"25254d41341c20911000c9386f46a81b9a43363a","tarball":"https://registry.npmjs.org/@domandigital/seo/-/seo-0.3.1.tgz","fileCount":10,"integrity":"sha512-T8h6q7IYr++kvfsXQ0H/LHr2y1PqPtQZ3EQSTt4W1lNRPihnXpRKssiEjeGNU3kaaxRXz2Pu96bFiD9X50xlXg==","signatures":[{"sig":"MEYCIQD6zuhpuIezcDD2M5ltPptq/CI3a76teKfAFoz+iDNo5AIhAJsqL++tVZ8/V0OISVv2d/BLvGgp5yG4TJFLRLicSF9u","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDPxd/7TVFWP5WZ0MMFia6mblJzFp6QcuBnr0j97FmdywIgBjzCyGk/WnyY5CDdQtwx7oZYkk/v6gKIFL++BsgyHV0="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@domandigital%2fseo@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":84706},"main":"dist/index.cjs","name":"@domandigital/seo","type":"module","_from":"file:domandigital-seo-0.3.1.tgz","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=22.12"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json","./links.schema.json":"./links.schema.json","./redirects.schema.json":"./redirects.schema.json"},"license":"Apache-2.0","private":false,"scripts":{"test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","typecheck":"tsc --noEmit"},"version":"0.3.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:fd3a220b-21f5-4b5c-9516-ce5dd9b1bf64"}},"homepage":"https://github.com/Doman-Digital/dd-packages/tree/main/packages/seo#readme","_resolved":"/tmp/28c5753a92e8191219bc0c9c217d6647/domandigital-seo-0.3.1.tgz","_integrity":"sha512-T8h6q7IYr++kvfsXQ0H/LHr2y1PqPtQZ3EQSTt4W1lNRPihnXpRKssiEjeGNU3kaaxRXz2Pu96bFiD9X50xlXg==","repository":{"url":"git+https://github.com/Doman-Digital/dd-packages.git","type":"git","directory":"packages/seo"},"_npmVersion":"11.20.0","description":"Route-fact primitives shared across Doman Digital and client properties: sitemap/indexability policy, per-page keyword targets, the internal-link graph, breadcrumb trail derivation, and a filesystem-vs-policy coverage validator.","directories":{},"maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.9","typescript":"^5.6.3"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/seo_0.3.1_1790229148781_0.3828444906459283"}}},"time":{"created":"2026-08-16T16:30:06.292Z","modified":"2026-09-24T05:52:29.202Z","0.1.1":"2026-08-16T16:30:06.647Z","0.1.2":"2026-08-16T19:33:01.606Z","0.1.3":"2026-08-16T20:16:20.154Z","0.3.0":"2026-09-24T01:55:17.930Z","0.3.1":"2026-09-24T05:52:28.859Z"},"bugs":{"url":"https://github.com/Doman-Digital/dd-packages/issues"},"license":"Apache-2.0","homepage":"https://github.com/Doman-Digital/dd-packages/tree/main/packages/seo#readme","repository":{"url":"git+https://github.com/Doman-Digital/dd-packages.git","type":"git","directory":"packages/seo"},"description":"Route-fact primitives shared across Doman Digital and client properties: sitemap/indexability policy, per-page keyword targets, the internal-link graph, breadcrumb trail derivation, and a filesystem-vs-policy coverage validator.","maintainers":[{"name":"domandigital","email":"admin@domandigital.co.uk"}],"readme":"# @domandigital/seo\n\nRoute-fact primitives shared across Doman Digital and client properties:\nsitemap and indexability policy, per-page keyword targets, the\ninternal-link graph, breadcrumb trail derivation, and a filesystem-vs-policy\ncoverage validator.\n\n**Read [PRINCIPLES.md](PRINCIPLES.md) before wiring this into a new repo.**\nThis README is the API reference.\n\n## Why this exists\n\nFour properties (Doman Digital, RMP Electrical, MMM Beauty, Sensphere) each\ngrew their own version of the same route bookkeeping: which pages are\nindexable, which belong in the sitemap, what a page is trying to rank for,\nwhich pages should link to which. The recurring defect: nothing checked that\nevery page on disk had a policy entry, so six live case studies (`/work/*` on\n`domandigital.co.uk`, including a page already earning its own search\ntraffic) shipped indexable and simply never got submitted to the sitemap.\nNobody noticed for weeks.\n\nThis package is the fix: pure functions over route data the consuming repo\nstill owns. It does not know your CMS, your framework, or your route data.\nIt only knows how to look entries up, derive edges between them, and flag\nwhen the two don't line up.\n\n**What's per-repo, on purpose:** the actual `RoutePolicyEntry[]` array, the\nactual `PageTarget[]` list, the actual link declarations, Sanity queries, and\nevery component. This package ships none of that. See `## Deliberately not\nin v0.2` below for what it also leaves out.\n\n## Install\n\n```bash\npnpm add @domandigital/seo\n```\n\nPublic on npm, Apache-2.0, published with provenance from a tagged release.\nESM and CJS builds ship together, each with its own types. Requires Node 22.12 or\nnewer.\n\nUpgrading a repo that still pins `github:Doman-Digital/dd-seo#vX.Y.Z`? Swap it\nfor a semver range, and drop `resolve.preserveSymlinks: true` from\n`vitest.config.ts` if it was added for this package. See\n`@domandigital/graph`'s README for why that workaround existed and why registry\ninstalls don't need it.\n\n## API stability\n\n`policy.ts`, `links.ts` and `validate.ts` are exercised by a real consumer and\nare treated as stable within a `0.x` minor: a breaking change to them comes only in a minor bump, with a changeset saying so.\n\n`targets.ts`, `trail.ts`, `redirects.ts` and `backlinks.ts` are provisional. They're built and tested, but no\nrepo has adopted them in production yet, so their shapes may change once a\nsecond consumer shows what they actually need. Pin exactly if you depend on\nthem today.\n\n## API\n\n### `policy.ts`\n\n```ts\ngetSitemapRoutes(policy: RoutePolicyEntry[]): RoutePolicyEntry[]\ngetRoutePolicy(policy: RoutePolicyEntry[], path: string): RoutePolicyEntry | undefined\nisRouteIndexable(policy: RoutePolicyEntry[], path: string): boolean\n```\n\n`isRouteIndexable` defaults to `true` for an unknown route, deliberately.\nSee the comment in `src/policy.ts`. The corresponding failure belongs to\n`validateCoverage`, which runs in CI, not at request time.\n\nLookups normalise the path first, so `/pricing/`, `/pricing?ref=x` and\n`/pricing#faq` all find the `/pricing` entry. Before 0.2 they fell through\nto the permissive default, so a noindex page reached with a query string\nreported as indexable.\n\n`sitemapPriority` and `sitemapChangeFrequency` are deprecated. Google says it\nignores `<priority>` and `<changefreq>`\n([Build and submit a sitemap](https://developers.google.com/search/docs/crawling-indexing/sitemaps/build-sitemap)).\nThey still compile and still reach your sitemap; they just don't do anything\nin Google.\n\n**Don't combine noindex with a robots.txt disallow.** For `noindex` to work,\nGoogle has to crawl the page and see it; a `Disallow` for the same path\nstops that, and the URL can stay indexed from links alone\n([Block indexing](https://developers.google.com/search/docs/crawling-indexing/block-indexing)).\nA route with `indexable: false` belongs out of the sitemap, not out of\nrobots.txt.\n\n### `normalize.ts`\n\n```ts\nnormalizeRoutePath(path: string, opts?: { trailingSlash?: \"never\" | \"always\" }): string\ntoPolicyPatterns(nextPath: string): string[]\n```\n\n`normalizeRoutePath` strips `?query` and `#hash` and applies one trailing-slash\nrule (default `\"never\"`, Next.js's default; pass `\"always\"` for a site built\nwith `trailingSlash: true`).\n\n`toPolicyPatterns` turns a Next.js App Router route into the policy paths it\nneeds, in this package's `/prefix/*` convention:\n\n| Route on disk | Policy paths |\n|---|---|\n| `/blog/[slug]` | `/blog/*` |\n| `/docs/[...slug]` | `/docs/*` |\n| `/help/[[...slug]]` | `/help` and `/help/*` (an optional catch-all also serves its parent) |\n| `/(marketing)/pricing` | `/pricing` (route groups are dropped) |\n\nIt throws on parallel-route slots (`@modal`) and intercepting routes\n(`(.)photo`), which don't have URLs of their own. Filter those out first.\n\n### `targets.ts`\n\n```ts\ngetTargetForRoute(targets: PageTarget[], routeKey: string): PageTarget | undefined\nfindKeywordCannibalization(targets: PageTarget[], allowlist?: string[]): KeywordCannibalization[]\n```\n\n`findKeywordCannibalization` groups primary keywords case-insensitively with\nwhitespace collapsed, and matches the allowlist the same way. It checks your\nown targeting. Google documents no cannibalisation penalty, so read a hit as\n\"two pages are written for one query: merge them or retarget one\", not as\nevidence that either page is being demoted.\n\n### `links.ts`: the internal-link graph\n\n```ts\ngetRelatedLinks(declarations: LinkDeclaration[], routeKey: string, opts?: GetRelatedLinksOptions): RelatedLinks\n```\n\nA page declares `supports: string[]` (which money pages it should send\nauthority to) and optionally a `pillar` for same-topic fallback linking.\nCalling `getRelatedLinks` on the money page returns every declaration that\nnamed it: the reverse edge, derived once, not declared twice. Calling it on\nthe content page returns its own `supports`, topped up with pillar siblings\nif under `opts.limit`.\n\n`opts.limit` caps `linksTo` only. `opts.linkedFromLimit` caps `linkedFrom`,\nindependently: the two are unrelated lists (a page's own outbound picks versus\nevery page that named it), so one option was never meant to cap both. Omit\neither for no cap.\n\n### `trail.ts`\n\n```ts\ngetBreadcrumbTrail(labels: TrailLabel[], path: string): TrailEntry[]\n```\n\nWalks path segments, picking up the registered label at each level that has\none. Feed the result to `@domandigital/graph`'s `buildBreadcrumbs`. That's\nthe one touchpoint between the two packages (see PRINCIPLES.md in dd-graph).\n\n### `validate.ts`\n\n```ts\nvalidateCoverage(input: ValidateCoverageInput): CoverageIssue[]\n```\n\nPure: the caller does the filesystem enumeration (a per-router concern) and\npasses `routesOnDisk` in. Flags: a route on disk with no policy entry (the\nsix-missing-case-studies bug), a sitemap-eligible policy entry with no\ncorresponding page (an orphan), and, if `moneyRoutes`/`targets` are\nsupplied, a money route with no keyword target declared.\n\nPass `dynamicRoutesOnDisk` (bracket paths such as `/blog/[slug]`) to check\ndynamic routes too. That adds two issues:\n\n- `dynamic-route-missing-policy`: a dynamic route whose `toPolicyPatterns`\n  output has no matching policy entry. A new `app/case-studies/[slug]` page\n  with no policy used to pass CI; this is the same bug as the six case\n  studies, one level up.\n- `policy-pattern-missing-route`: a `/prefix/*` entry that no dynamic route\n  produces (a retired section still carrying policy).\n\nCheck patterns, not the URLs they generate. With `dynamicParams` on, pages\nare generated at request time and never appear in a build-time list.\n\n```ts\n// e.g. with fast-glob, from the app directory\nconst pages = glob.sync(\"**/page.tsx\", { cwd: \"app\" })\n  .map((f) => \"/\" + f.replace(/\\/?page\\.tsx$/, \"\"))\n  .filter((p) => !/\\/(@|\\(\\.)/.test(p)); // drop parallel slots and intercepting routes\nconst routesOnDisk = pages.filter((p) => !p.includes(\"[\")).flatMap(toPolicyPatterns);\nconst dynamicRoutesOnDisk = pages.filter((p) => p.includes(\"[\"));\nvalidateCoverage({ routesOnDisk, dynamicRoutesOnDisk, policy });\n```\n\n### `redirects.ts`: the migration gate\n\n```ts\nvalidateRedirects(input: ValidateRedirectsInput): RedirectIssue[]\n```\n\nEvery URL other sites link to must still land on a page, in one hop. Pass\nthe linked URLs (`liveLinkedUrls(links)`, or a backlink export), the routes\non disk, the redirect list, the route policy and the hostnames the site\nanswers on (old domains included). Flags: a linked URL with neither a page\nnor a redirect, an unparseable linked URL, a redirect to a missing page, a\nchain (with every hop named), a loop (once per cycle), a redirect that hides\na live page, a redirect to a `noindex` page, and two redirects from one path.\n\nPaths are compared after stripping origin, query, fragment and a trailing\nslash. Case is kept. Dynamic-pattern policy entries count as pages. External\ndestinations are not followed. Pure, like `validateCoverage`.\n\n`redirects.schema.json` ships with the package for the site's\n`redirects.json`.\n\n### `backlinks.ts`: the backlink register\n\n```ts\nliveLinkedUrls(register: BacklinkRegister): string[]\n```\n\nThe shape of a site's `links.json` (`links.schema.json` ships with the\npackage), and the live targets out of it. There is no status for asks: the\nregister records links that exist or existed, because Doman Digital does not\nrun cold outreach for links.\n\n## Deliberately not in v0.2\n\nPattern redirects (`/blog/:slug`). `validateRedirects` checks exact paths;\na site that needs patterns lists the linked URLs they cover explicitly until\na second consumer shows the shape.\n\nThe metadata builder (title/description/OG/hreflang generation). Doman\nDigital's `lib/metadata.ts` is mature and shaped around its own CMS overlay;\nRMP Electrical has its own version tuned to location pages. Extracting a\nshared metadata builder now would mean designing for two consumers whose\nneeds haven't converged yet. Revisit once a second real consumer is on this\npackage and the shape is clearer.\n","readmeFilename":"README.md"}