v0.6.0

Minor Changes

  • structure.components controls where Alloy looks for SSR component source files. Defaults to "components". During alloy serve, Alloy watches this directory and re-renders pages that use affected components. Experimental.

    structure:
      components: "elements"
  • onFormatRendered fires once per non-HTML format body after layout rendering. The payload contains { format, content, url, path, frontMatter }. Return { content: "..." } to replace the rendered body.

    alloy.hook("onFormatRendered", {}, (payload) => {
      if (payload.format === "json") {
        return { content: JSON.stringify(JSON.parse(payload.content)) };
      }
    });

    onPageRendered skips pages whose outputs contains only non-HTML formats. Pages with mixed outputs fire both hooks: onPageRendered for HTML, onFormatRendered for each non-HTML format.

  • Breaking: onPageRendered receives { html, frontMatter, url, path } instead of a raw HTML string. Only html is mutable. Plugins can read frontMatter to decide whether to transform.

    alloy.hook("onPageRendered", {}, (page) => {
      if (page.frontMatter.layout === "demo") return page;
      page.html = page.html.replace(/<h2/g, '<h2 class="styled"');
      return page;
    });

    See migration notes for the before/after.

  • Return addDependencies from onPageRendered or onContentTransformed to declare external file dependencies. Alloy tracks a reverse index and rebuilds only affected pages when those files change during alloy dev. onFileChanged hooks can return { invalidateByDependency: [...] } for targeted rebuilds, or { restart: true } to restart Node bridge subprocesses.

    alloy.hook("onPageRendered", {}, (page) => {
      const result = renderSSR(page.html);
      return {
        html: result,
        addDependencies: [
          "elements/rh-card/rh-card.js",
          "elements/rh-icon/rh-icon.js",
        ],
      };
    });

    Dependencies accumulate per page per build. Removing a component tag drops that dependency on the next rebuild. Alloy normalizes paths with filepath.Clean.

    Previously, changes to files outside content/ and layouts/ either triggered no rebuild or forced a full rebuild of every page.

  • alloy dev and alloy serve write .alloy/server.lock on startup. If another Alloy process is watching the same directory, Alloy prints a warning with the conflicting PID, port, and a kill command. Startup continues without blocking. Alloy removes stale lockfiles from crashed processes on the next startup.

    Previously, concurrent instances fought over _site/, with clean: true full rebuilds wiping incremental output and no error in either console.

Patch Changes

  • Alloy frees each page’s rendered HTML after writing it to disk. Previously, the build held every page body in memory until completion. A 3,000-page site averaging 500KB of output carried ~1.5GB in page bodies alone.

  • onBuildComplete payload is now { pageCount, duration, errors, outputDir } with camelCase keys. duration is a pre-formatted string, not nanoseconds. PagesSkipped removed, errors added. Plugins that need output content should read from _site/ on disk.

    Alloy previously piped the full rendered HTML of every page to plugins over IPC on each build.

Migration notes

onPageRendered plugins — update from (html) => string to (page) => { html }:

// Before (v0.5.x)
alloy.hook("onPageRendered", {}, (html) => {
  return html.replace(/<h2/g, '<h2 class="styled"');
});

// After (v0.6.0)
alloy.hook("onPageRendered", {}, (page) => {
  page.html = page.html.replace(/<h2/g, '<h2 class="styled"');
  return page;
});

onBuildComplete consumers — field names are now camelCase, and duration is a string:

// Before (v0.5.x)
const ms = (result.Duration / 1e6).toFixed(0);
console.log(`Built ${result.PageCount} pages in ${ms}ms`);

// After (v0.6.0)
console.log(`Built ${result.pageCount} pages in ${result.duration}`);