v0.6.0 (2026-07-25)
Minor Changes
-
structure.componentscontrols where Alloy looks for SSR component source files. Defaults tocomponents/when omitted.# alloy.config.yaml structure: components: "elements"
During
alloy serve, Alloy watches this directory for changes and re-renders pages that use the affected components. Projects that keep component source outsidecomponents/previously got no SSR invalidation on file changes.Alloy's built-in SSR integration is experimental. This config option and its behavior may change in future releases.
-
onFormatRenderedfires once per non-HTML format body after layout rendering with{ format, content, url, path, frontMatter }. Return an object with acontentkey to replace the rendered body. The build ignores all other keys in the return value.alloy.hook("onFormatRendered", {}, (payload) => { if (payload.format === "json") { return { content: JSON.stringify(JSON.parse(payload.content)) }; } });
onPageRenderedskips pages whoseoutputscontains only non-HTML formats. A page withoutputs: ["json"]routes throughonFormatRenderedinstead. Both hooks fire independently when a page declares HTML and non-HTML outputs together.Return
null,undefined, or an object without acontentkey to keep the original format body. -
Breaking:
onPageRenderedsends a page object{ html, frontMatter, url, path }instead of a raw HTML string. Onlyhtmlin the return is applied back —frontMatter,url, andpathare read-only context.alloy.hook("onPageRendered", {}, (page) => { if (page.frontMatter.layout === "demo") return page; page.html = page.html.replace(/<h2/g, '<h2 class="styled"'); return page; });
Plugins that conditionally process pages can read
page.frontMatterto decide whether to transform. BothBuild()andBuildIncremental()send the same payload shape.Previously, the hook received a raw HTML string. Plugins that needed to skip certain pages had to embed
<meta>markers in layout HTML and strip them downstream. -
Plugins declare external file dependencies via
addDependenciesinonPageRenderedandonContentTransformedreturn values. Alloy tracks a reverse index in the build cache and rebuilds only the pages that declared a dependency when that file changes duringalloy dev.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", ], }; });
onFileChangedhooks return{ invalidateByDependency: [...] }to trigger targeted rebuilds via the reverse index, or{ restart: true }to restart Node bridge subprocesses before the rebuild. Restart clears Node's ESM module cache so the plugin re-imports fresh component definitions.alloy.hook("onFileChanged", {}, (events) => { const changed = events .filter(ev => ev.path.startsWith("elements/") && ev.path.endsWith(".js")) .map(ev => ev.path); if (changed.length > 0) { return { invalidateByDependency: changed, restart: true }; } });
Dependencies from both hooks accumulate per page per build. Removing a component tag from a page drops that dependency on the next rebuild. Non-array
addDependenciesvalues produce a warning. Alloy normalizes dependency paths withfilepath.Clean, so./data.jsonanddata/../data.jsonmatch the canonicaldata.jsoncache key.Previously, changes to files outside
content/andlayouts/either triggered no rebuild or forced a full rebuild of every page. A site with 720 pages and an SSR plugin that reads component definitions would rebuild all 720 pages when a single component file changed. -
alloy devandalloy servewrite a lockfile at.alloy/server.lockon startup. If another alloy process is already watching the same project directory, a warning prints to stderr with the conflicting PID, port, mode, and akillcommand. Startup continues without blocking.warning: another alloy process (PID 4659, alloy serve on port 3003, started 2026-07-14T13:00:00-04:00) is watching this directory warning: concurrent instances writing to _site/ will cause missing pages and 404s warning: kill the other process with: kill 4659Stale lockfiles from crashed processes (dead PID or corrupt JSON) are removed on the next startup. The lockfile is removed on clean shutdown via signal handler.
Previously, a backgrounded
alloy serveand a newalloy devsession would silently fight over_site/, withclean: truefull rebuilds wiping incremental output. Pages vanished with no errors in either console.
Patch Changes
-
Free each page's rendered HTML after writing it to disk. Alloy held every page's
RenderedBody, cached HTML string, and alternate format bodies in memory for the full build lifetime. A 3,000-page site averaging 500KB of output carried ~1.5GB in page bodies. Now only the largest single page sits in memory at once: O(total site HTML) becomes O(largest page).CaptureRenderedContent(used byBuildWithContenttests) snapshots the HTML map before the output writing loop, so release does not affect test infrastructure. Sitemap generation and cache building read page metadata, not rendered bodies. -
Reduce peak memory on large sites. Alloy kept a duplicate of every page's rendered HTML in memory after writing it to disk. A 3,000-page site averaging 500KB of output carried ~1.5GB in the duplicate alone. Production builds and
alloy devskip the duplicate.Trim the
onBuildCompletehook payload to{ pageCount, duration, errors, outputDir }. Alloy previously piped the full rendered HTML of every page to plugins over IPC on each build. Plugins that need output content can read_site/from disk.