•3 min read

Merkle Asset Digesting in the Astro 5 Content Layer

Astro 5’s built-in glob() content loader computes entry digests exclusively from raw Markdown text, leaving co-located vector schematics and raster cover images untracked in the build cache [MMJ18]. We engineered assetAwareGlob to fold sorted sibling asset hashes into each entry’s Merkle digest and prune build-time inlined SVGs from redundant image optimization [Mer88].

Table
Architectural DimensionBuilt-In glob() LoaderAd-Hoc Dev Cache-Wipe WorkaroundComposite Merkle assetAwareGlob (SYSTEM-012)
Digest Input ScopeRaw .md/.mdx string onlyRaw .md/.mdx string onlyMarkdown body + co-located .svg/raster hashes + pipeline plugins [Mer88]
In-Place Asset Edit (hero.jpg / .svg)Misses mutation; serves stale .astro/data-store.jsonDeletes entire .astro/data-store.json + server.restart()Re-renders only the affected entry in < 200ms [MMJ18]
Vite Watcher IntegrationWatches .md/.mdx globs onlyExternal Chokidar hooks in astro.config.tsBound Proxy on context.watcher + 100ms debounced asset mapping
Build-Time Inlined SVGsProcessed redundantly by astro:assetsLeft in localImagePathsPruned from vfile.data.astro.localImagePaths in rehype-inline-svg.ts

Why Does Astro’s Built-In Glob Loader Miss Co-Located Asset Mutations?

Astro’s default glob() loader misses co-located asset mutations because it evaluates generateDigest strictly over the Markdown string and frontmatter metadata [MMJ18]. When an author edits a sibling .svg diagram or overwrites hero.jpg in place, the entry digest in .astro/data-store.json remains unchanged and serves stale build artifacts until a manual cache wipe.

In the formal taxonomy of build systems established by Mokhov, Mitchell, and Peyton Jones [MMJ18], Astro 5’s Content Layer acts as a constructive verifying trace store backed by .astro/data-store.json. A constructive trace cache is only sound when its verification key captures the complete transitive closure of inputs consumed during compilation [DJV04]. As documented in our architectural specification SYSTEM-012, two compilation paths violated this invariant: first, rehype-inline-svg.ts reads co-located .svg files synchronously via fs.readFileSync outside Vite’s module graph; second, replacing hero.jpg or preview.jpg with an identical filename leaves the frontmatter path string ./hero.jpg untouched.

SVG
TEXT-ONLY DIGEST VS. MERKLE TREE 1. Built-In glob() Loader Blind to Sibling Assets index.md Unchanged Text diagram.svg Edited on Disk Text Digest Same Hash Stale Cache Skipped 2. Composite Merkle Tree Full Input Closure index.md baseDigest Sibling Assets .svg + .jpg MD5 Pipeline Files Plugins + CSS Merkle Root base : assets New Digest Fresh Store Re-Rendered Constructive Traces Require Complete Input Fingerprints

Previously, astro.config.ts worked around these stale cache hits by watching .svg files and deleting the entire .astro/data-store.json cache or triggering a full server.restart(), which invalidated every collection simultaneously and added multi-second rebuild latency.

How Does Our Custom Content Loader Compute Combined Merkle Asset Digests?

Our custom assetAwareGlob wrapper intercepts generateDigest to combine the base Markdown digest with a sorted cryptographic fingerprint of all co-located directory assets and pipeline plugins [Mer88] [DJV04]. Simultaneously, it proxies Vite’s file watcher with bound this context to map sibling asset edits directly back to the affected Markdown entry for sub-second updates.

Wired across all content collections in src/content.config.ts, computeEntryAssetFingerprint scans the entry directory (plus parent presentation directories for slides/ and explicit ./ or ../ references) for .svg, .png, .jpg, .webp, .avif, and .gif files, alongside global pipeline dependencies including src/styles/graphics.css. Using getFileContentHash from src/commons/server/content-hash.ts—which caches 16-character MD5 digests keyed by mtimeMs and byte size—the loader constructs a deterministic Merkle-style fingerprint [Mer88]:

digest(entry)=H(H(markdown)  ∥  ⨁a∈Assets(entry)(relPath(a)∥H(a)))\text{digest}(\text{entry}) = H\Big(H(\text{markdown}) \;\Vert\; \bigoplus_{a \in \text{Assets}(\text{entry})} \big(\text{relPath}(a) \mathbin{\Vert} H(a)\big)\Big)

SVG
ASSETAWAREGLOB LOADER PIPELINE 1. Composite Digest Hook generateDigest() MD Base Digest Map hash → mdFilePath Asset Fingerprint Sorted relKey:md5 2. Bound FSWatcher Proxy Reflect.get().bind() PX Watcher Proxy Captures Callback 100ms Debounce change / add / unlink 3. Surgical Entry Update Zero Store Wipe Affected Lookup Matches Asset Name Single-Entry HMR globChangeHandler() Sub-200ms HMR Without Touching Markdown Mtimes

To trigger surgical single-entry rebuilds during astro dev without mutating Markdown file timestamps (fs.utimesSync), src/commons/server/content-loader.ts wraps context.watcher in an ES Proxy that captures Astro’s internal globChangeHandler while binding native Vite FSWatcher methods (Reflect.get(target, prop, target).bind(target)):

TypeScript
const wrappedWatcher: FSWatcher | undefined = context.watcher
  ? new Proxy(context.watcher, {
      get(target, prop) {
        if (prop === 'on') {
          return (event: string, listener: (changedPath: string) => void) => {
            if (event === 'change' && !globChangeHandler) globChangeHandler = listener;
            return target.on(event, listener);
          };
        }
        const value = Reflect.get(target, prop, target);
        return typeof value === 'function' ? value.bind(target) : value;
      },
    })
  : undefined;

const enhancedContext: LoaderContext = {
  ...context,
  watcher: wrappedWatcher,
  generateDigest: (data) => {
    const baseDigest = String(originalGenerateDigest(data));
    const mdFilePath = typeof data === 'string' ? mdFileByBaseDigest.get(baseDigest) : undefined;
    const assetFp = mdFilePath ? computeEntryAssetFingerprint(mdFilePath, data as string) : '';
    return assetFp ? String(originalGenerateDigest(`${baseDigest}:${assetFp}`)) : baseDigest;
  },
};

When a co-located .svg or raster image changes on disk, findAffectedMarkdownFiles resolves the owning .md/.mdx entry within a 100ms debounce window, refreshes mdFileByBaseDigest, and invokes globChangeHandler(mdFile) so Astro re-renders only that entry in under 200ms.

Why Must Build-Time Inlined SVGs Be Pruned From Astro’s Image Pipeline?

Build-time inlined SVGs must be pruned from vfile.data.astro.localImagePaths because Astro’s Remark pass registers every Markdown image syntax node for asset optimization before Rehype plugins execute. Filtering out inlined .svg paths in rehype-inline-svg.ts prevents redundant astro:assets processing while keeping theme-adaptive vector diagrams inside the document DOM.

Because our diagrams rely on CSS custom properties (var(--svg-*) from src/styles/graphics.css) to switch seamlessly between Soft Nordic Pastel light mode and dark mode, src/plugins/rehype-inline-svg.ts replaces <p><img src="./diagram.svg"></p> with an inline <figure class="svg-diagram"> tree. During this transformation, the plugin computes an 8-character SHA-256 scope prefix (svg-${hash}-) over both the normalized path and SVG content (data-svg-hash) so internal <marker> and <clipPath> IDs never collide across multiple diagrams on a page.

SVG
BUILD-TIME SVG INLINING & PRUNING 1. Remark Image Scan vfile.data.astro ![alt](./diagram.svg) localImagePaths [hero.jpg, diagram.svg] 2. rehype-inline-svg.ts DOM + VFile Filter Inline <figure> Scoped svg-hash IDs Prune Inlined .svg Remove from Astro List 3. Decoupled Build Output Zero Redundant Work Theme-Aware DOM Live var(--svg-*) CSS Raster Pipeline Only [hero.jpg] Optimized Eliminates Duplicate SVG Optimization & Dev Store Resets

However, Astro’s internal Remark image plugin runs before Rehype and pushes every ![alt](./diagram.svg) path into vfile.data.astro.localImagePaths. By filtering inlinedSrcs and .svg entries out of astroData.localImagePaths at the end of rehype-inline-svg.ts and recording inlineSvgDependencies, Astro’s image pipeline only optimizes genuine raster assets like hero.jpg, while astro.config.ts remains completely free of manual .astro/data-store.json deletion hacks—unifying our Schema.org ImageObject Metadata Pipeline, Zero-Artifact Content-Addressable Build Caching, Single-Root JSON-LD 1.1 Knowledge Graph Validator, and Three-Band Adaptive SVG Color Governance.

References