feat(lectio-docs): doc collector utilities - #43
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extracts and exports path/locale/slug derivation utilities so hosts can build manifests at runtime using the same logic as the build-time collector, and adds a ContentSource.resolveLink() API to resolve relative *.md links against the source path of the linking document.
Changes:
- Introduces
pathToPage/pathToSlug/pathToLocale/normalizeSlug/resolveRelativePathas pure string utilities and exports them from the content entrypoint. - Extends
ContentSourcewithresolveLink()and implements it increateContentSource()with a newResolvedLinktype. - Adds
slug:frontmatter override support incollect()and warns when translations disagree on their declared slug; updates README + changeset accordingly.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/lectio-docs/src/content/types.ts | Adds ResolvedLink type and ContentSource.resolveLink() API contract. |
| packages/lectio-docs/src/content/paths.ts | Adds pure path→slug/locale/page utilities and relative path resolver. |
| packages/lectio-docs/src/content/index.ts | Re-exports new path utilities and types from the public content API. |
| packages/lectio-docs/src/content/content-source.ts | Implements resolveLink() for manifest-backed sources. |
| packages/lectio-docs/src/collector/collect.ts | Uses shared path utilities; adds frontmatter slug: override + slug disagreement warning. |
| packages/lectio-docs/README.md | Documents runtime manifest building and link resolution. |
| .changeset/runtime-manifests-and-links.md | Declares a minor release and summarizes new APIs/behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| for (const segment of toPosix(relative).replace(/^\//, '').split('/')) { | ||
| if (segment === '' || segment === '.') continue; | ||
| if (segment === '..') segments.pop(); | ||
| else segments.push(segment); | ||
| } |
There was a problem hiding this comment.
Valid, and fixed in 697fe8c. This contradicted the thing resolveLink exists to guarantee, so thanks for catching it.
The clamp made two links that mean different things indistinguishable:
a/b.md + ../c.md → "c.md" ← legitimate
a/b.md + ../../../c.md → "c.md" ← out of bounds, same answer
resolveRelativePath now keeps the .. when it climbs past the start, the way path.posix.normalize does, so the second resolves to ../../c.md — which no manifest source can match, and bySource.get() returns null. An absolute path still clamps at its root, since POSIX does.
Verified against node:path's posix.join/posix.normalize across eleven cases (plain, ./, .. within bounds, .. past the root, interior .. like x/../../y.md, and absolute) — all identical. End to end through resolveLink:
docs/privacy.md → terms.md /terms [en]
docs/nb/privacy.md → terms.md /terms [nb]
docs/guides/a.md → ../apps/config.md /apps/config
docs/a.md → ../../../../terms.md null
docs/a.md → ../nope.md null
The locale-follows-the-link behaviour and the two-config.md disambiguation both still hold.
…urce The collector held the only copy of two things a host needs. Both are now reachable on their own, and `collect()` is built from the same pieces rather than a second implementation of them. `pathToPage` maps a file path to the slug it gets and the language it is written in — the logic behind `collect()`, exported. Content that changes without a rebuild (a directory mounted into a container, a CMS export) can assemble a manifest at runtime and still get the navigation tree, the locale fallback and the search index; a manifest was always just data, but building one meant reimplementing this. `pathToSlug`, `pathToLocale`, `normalizeSlug` and `resolveRelativePath` come with it, all pure and free of `node:path`. `source.resolveLink(href, fromSource)` maps a relative `*.md` link to the page it means, resolved against the path of the document containing it rather than against a bare filename. Documentation is written to read on disk and on a forge as well as in a host, so documents link to each other by path — and a filename is not unique. Two sections can each hold a `config.md` without `[overview](../guides/config.md)` becoming a coin flip, and language settles itself: a link from `nb/privacy.md` to `terms.md` lands on the Norwegian version of that page. Off-site, root-relative and anchor-only hrefs return null, as do files the manifest doesn't hold; a typo should read as a broken link rather than point somewhere unintended. Alongside: - `slug:` in frontmatter overrides the path-derived slug, so a document can keep a short, stable URL while its filename stays descriptive — `terms-of-use.md` with `slug: terms` is `/terms`. Translations that disagree on the slug they declare are warned about while collecting, since they would otherwise quietly stop being one page.
7d908fa to
697fe8c
Compare
The collector held the only copy of two things a host needs. Both are now reachable on their own, and
collect()is built from the same pieces rather than a second implementation of them.pathToPagemaps a file path to the slug it gets and the language it is written in — the logic behindcollect(), exported. Content that changes without a rebuild (a directory mounted into a container, a CMS export) can assemble a manifest at runtime and still get the navigation tree, the locale fallback and the search index; a manifest was always just data, but building one meant reimplementing this.pathToSlug,pathToLocale,normalizeSlugandresolveRelativePathcome with it, all pure and free ofnode:path.source.resolveLink(href, fromSource)maps a relative*.mdlink to the page it means, resolved against the path of the document containing it rather than against a bare filename. Documentation is written to read on disk and on a forge as well as in a host, so documents link to each other by path — and a filename is not unique. Two sections can each hold aconfig.mdwithout[overview](../guides/config.md)becoming a coin flip, and language settles itself: a link fromnb/privacy.mdtoterms.mdlands on the Norwegian version of that page. Off-site, root-relative and anchor-only hrefs return null, as do files the manifest doesn't hold; a typo should read as a broken link rather than point somewhere unintended.Alongside:
slug:in frontmatter overrides the path-derived slug, so a document can keep a short, stable URL while its filename stays descriptive —terms-of-use.mdwithslug: termsis/terms. Translations that disagree on the slug they declare are warned about while collecting, since they would otherwise quietly stop being one page.