Skip to content

Resolve content links from their source file after rendering. - #7

Merged
koculu merged 1 commit into
mainfrom
resolve-content-links
Oct 4, 2026
Merged

koculu merged 1 commit into
mainfrom
resolve-content-links

Conversation

@koculu

@koculu koculu commented Oct 4, 2026

Copy link
Copy Markdown
Member

Summary

Relative links in content now resolve from the file that wrote them, and the build fails when one points to a page that doesn't exist.

Previously only links ending in .md, .mdx or .rmdx were rewritten. Anything else, like [themes](./themes), was left for the browser to resolve against the page URL. Page URLs end in a slash, so on a page like guides/semantic-tones.mdx (/guides/semantic-tones/) that link went to /guides/semantic-tones/themes. It only worked by coincidence on folder index pages.

The rule

  • Relative links (./themes, ../, ./components/buttons) resolve from the source file, with or without an extension. They must match a page, or the build fails with the file and the link in the message.
  • Root-absolute links (/blog/) are left as written and aren't checked, so they can point to pages built separately.
  • External, hash, query and asset links are unchanged.

An extensionless path finds the file first (a/c.mdx), then the folder page (a/c/index.mdx or a/c/c.mdx). A trailing slash checks the folder first. A translated page that links to an untranslated one gets the default-locale page.

How it works

  • Links are resolved on the rendered DOM. renderApp gains an onRendered(document) hook. A single pass (build/page-urls.ts) resolves every href after Regor renders. That covers Markdown links, raw HTML, Regor components and bound values like :href="nextPage". Code samples, comments and script text are never touched, because they aren't elements.
  • Header and footer partials are wrapped in a marker that names their file, so their links resolve from the partial. The marker is removed before output.
  • The base path is added in the same pass. This replaces the regex in public-hrefs.ts; mdx/linkRewrite.ts is removed as well.
  • The route index (ContentRouteIndex) checks links against discovered pages in memory, without touching the disk.
  • Incremental builds keep the index current. Adding or removing a page marks every page dirty, now also when navigation is off.

Behavior changes

  • A broken relative link fails build. In serve, it shows on that page's error page.
  • BtnLink and FormAssistLink no longer turn ./x into /x; relative values are kept and resolved by the page pass.
  • Relative links from templates or head config follow the same rule, resolving from the page's file.
  • Content that a component moves out of the header (e.g. teleport="head") resolves from the page's file.

Docs

New Links guide in guides/, added to the nav and the guides index. The semantic tones guide now uses ./themes.

Testing

  • Unit tests for the resolver, including every source page of a folder/leaf layout plus encoding, case, Windows paths and i18n.
  • Unit tests for the DOM pass, including markers, templates, escaping and base path.
  • Page-level tests: Markdown, HTML, static and computed BtnLink, header links, and broken links.
  • Incremental tests for adding, deleting and editing pages, with navigation on and off.
  • The purestack.studio site builds with every link resolved, at the same build time as before.

@koculu
koculu merged commit 9780777 into main Oct 4, 2026
2 checks passed
@koculu
koculu deleted the resolve-content-links branch October 4, 2026 01:15
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.

1 participant