Skip to content

feat(lectio-docs): doc collector utilities - #43

Merged
losolio merged 1 commit into
mainfrom
feat/doc-helpers
Aug 7, 2026
Merged

feat(lectio-docs): doc collector utilities#43
losolio merged 1 commit into
mainfrom
feat/doc-helpers

Conversation

@losolio

@losolio losolio commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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.

Copilot AI lite review requested due to automatic review settings August 7, 2026 23:10
@losolio losolio changed the title feat(lectio-docs): a manifest without collect(), links resolved by so… feat(lectio-docs): doc collector utilities Aug 7, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/resolveRelativePath as pure string utilities and exports them from the content entrypoint.
  • Extends ContentSource with resolveLink() and implements it in createContentSource() with a new ResolvedLink type.
  • Adds slug: frontmatter override support in collect() 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.

Comment on lines +135 to +139
for (const segment of toPosix(relative).replace(/^\//, '').split('/')) {
if (segment === '' || segment === '.') continue;
if (segment === '..') segments.pop();
else segments.push(segment);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@losolio
losolio merged commit 453347d into main Aug 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants