Skip to content

Resolve relative image and file URLs from their source file and resolve config paths from the content root. - #8

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

koculu merged 1 commit into
mainfrom
resolve-asset-links

Conversation

@koculu

@koculu koculu commented Oct 4, 2026

Copy link
Copy Markdown
Member

Summary

Relative image and file URLs in content now resolve from the file that wrote them, just like page links. ![Diagram](./images/palette.svg) in guides/semantic-tones.mdx becomes /guides/images/palette.svg, and a missing file fails the build.

Previously only page links were resolved. Relative src, poster and srcset values were left for the browser, which resolved them against the page URL. Page URLs end in a slash, so the image above loaded from /guides/semantic-tones/images/palette.svg, which doesn't exist.

The rule

  • Relative URLs in href, src, poster and srcset resolve from the source file. This covers Markdown images, <img>, <picture>, <video>/<source>/<track>, downloads, SVG <use> and iframes, plus shared header and footer files, which resolve from their own folder.
  • Files are named exactly, with extension and the same capitals. The lookup order is unambiguous: a page extension or folder path means a page; an exact file match means an asset; then the page by name. A page can't have an exact extensionless path, so pages and files never collide.
  • Not checked: root-absolute URLs (/assets/site.css, generated previews), special schemes such as data: and cid:, and form action, which names an endpoint.
  • .ts sources are never matched, because they're bundled into hashed scripts.
  • Translated pages fall back to the default locale's file, as they do for pages.

Config URLs start at the content root

siteConfig.json lives at the content root, so its relative URLs now resolve from there: assets/logo.svg becomes /assets/logo.svg on every page. This covers logo.href, logo.imageSrc, logo.imageSrcDark, style.href, consent.privacyPolicyUrl and consent script src. Paths are normalized, and any query or hash is kept.

Without this, a relative logo or stylesheet path would resolve against each page and fail every nested page. It also fixes consent scripts, which load in the browser and were already broken on nested pages.

Implementation

  • content-hrefs.ts is renamed to content-urls.ts. ContentRouteIndex now holds pages and assets, and one resolveContentUrl serves every URL attribute.
  • The DOM pass in page-urls.ts resolves href, src and poster. srcset gets a parser that follows the HTML spec: each URL runs to the next whitespace, so commas inside data: URLs stay intact.
  • Incremental builds: the asset index starts from the manifest, so there's no extra directory walk, and full builds take it from the asset copy step. Adding or deleting an asset marks pages dirty; editing one in place doesn't.

Behavior changes

  • A relative src, poster or srcset must name a real file, or the build fails. In serve, the page shows the error.
  • Relative URLs from templates or head config resolve from the page's file.
  • A relative asset path that leaves the content folder now fails instead of passing through.
  • Missing-target messages now read "does not match any page or file" or "points to a missing file".

Docs

The links guide gains an Images and files section and the updated error message. The site configuration guide explains that config paths start at the content folder.

Testing

  • Resolver: asset paths, encoding, capitals, .ts sources, paths that leave the content folder, i18n fallback, and index comparison.
  • DOM pass: every asset attribute, srcset parsing, partials, base path, and missing files.
  • Page level: a Markdown image; config URL resolution.
  • Incremental: asset add, delete and in-place edit, with navigation on and off.
  • ts-ssg suite: 370 tests pass. Biome and TypeScript 7 are clean.

@koculu
koculu merged commit 5adb85e into main Oct 4, 2026
2 checks passed
@koculu
koculu deleted the resolve-asset-links branch October 4, 2026 01:22
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