v0.5.0

Minor Changes

  • onConfig applies the return value to config for a mutable allowlist: build.output, build.clean, structure.*, passthrough, plugins.workers, and plugins.timeout. Fields outside the allowlist are ignored without warning. Returning a non-object is a build error.

    alloy.hook("onConfig", {}, (config) => {
      config.build.output = "dist";
      config.structure.content = "pages";
      return config;
    });

    Multiple onConfig hooks chain in priority order. Alloy validates path fields before applying any of them. Absolute paths, .. traversals above the project root, and . are rejected. Passthrough entries follow the same rules, with to: "." allowed (root of the output directory).

    Previously the return value was discarded.

  • onAssetProcess fires once per asset file with {path, content} instead of once per build with directory paths. Return {content: "..."} to replace the file in output. Return null or omit the content key to keep the original. Multiple hooks chain.

    alloy.hook("onAssetProcess", {}, (asset) => {
      if (asset.path.endsWith('.css')) {
        return { content: minifyCSS(asset.content) };
      }
    });

    Previously the hook received {assetsDir, outputDir} and discarded the return value.

  • onBeforeValidation receives {outputPaths: [...]} and accepts {addOutputs: {path: source}} to register additional output paths before conflict detection. onAfterValidation receives {outputPaths: [...], cascade: {...}} and accepts {cascade: {...}} merged into site data for templates. Both reject unrecognized return keys.

    Previously both hooks fired before content discovery with a stub payload and threw away the return value.

  • onPagesReady accepts a second return shape, {addPages: [...]}, for injecting virtual pages without round-tripping the full pages array through the plugin bridge. With pages: false, Alloy skips serialization of existing pages.

    alloy.hook("onPagesReady", { data: ["elements"], pages: false }, (payload) => {
      return {
        addPages: payload.siteData.elements.map(el => ({
          path: `demos/${el.slug}.md`,
          url: `/demos/${el.slug}/`,
          frontMatter: { title: el.name, layout: "demo" },
          content: `# ${el.name}`
        }))
      };
    });

    The pages and addPages return shapes are mutually exclusive. Returning both is a build error. Returning an unrecognized key errors instead of dropping pages without warning.

  • onPagesReady virtual pages accept a dependencies array of project-root-relative file paths. During alloy dev, Alloy re-renders only the virtual pages whose listed files changed.

    dependencies: ['elements/button/demo/index.html']

    dependencies: [] skips re-rendering (no file deps to invalidate). Omitting dependencies re-renders on every rebuild (safe default, pre-0.5.0 behavior).

  • alloy.source(name, fn) registers a data source handler in Node plugins. Configure type: "plugin" in sources: to route data acquisition through the handler instead of the built-in rest or graphql fetchers.

    alloy.source("cms-posts", async () => {
      const resp = await fetch("https://api.example.com/posts", {
        headers: { Authorization: `Bearer ${process.env.TOKEN}` }
      });
      return resp.json();
    });

    Alloy caches results to .alloy/fetch-cache/ with the same TTL and --refetch semantics as built-in sources.

  • Alloy loads data files from subdirectories of data/ into nested template namespaces. data/nav/main.yaml becomes site.data.nav.main. Nesting goes to any depth. A file and non-empty directory sharing the same stem (e.g., nav.yaml alongside nav/) produces a build error. Alloy skips empty subdirectories.

    Previously, Alloy ignored subdirectories of data/ without warning.

  • Go template block shortcodes use {{% tag "args" %}}...{{% /tag %}} delimiters. A preprocessor runs after Goldmark and before Go template rendering, pairing tags and passing inner HTML to the shortcode callback. Nesting resolves innermost-first. Delimiters inside <pre> and <code> are literal text.

    Previously, Go template block shortcodes received empty content because the engine hard-coded it to "".

  • Liquid shortcode arguments resolve variables from the template context. Unquoted arguments like {% youtube page.videoId %} look up page.videoId and pass the resolved value to the shortcode callback. Quoted arguments remain literal strings. Unresolved variables fall back to their token string. Shortcodes returning "" produce no output instead of emitting a placeholder element.

  • alloy version --check queries GitHub Releases for newer versions. Set updateCheck: true in config for a one-line notification when alloy dev or alloy serve starts. The check runs in the background, caches for 24 hours, and defaults to off.

Patch Changes

  • Fix onContentLoaded dropping html mutations without warning. Alloy now applies page.html changes via SetRenderedBody. Both html and frontMatter mutations work independently or together.
  • Fix omitted pages scope defaulting to “all pages” instead of “none.” Hooks registered with {} now skip page serialization. Plugins that want pages must declare pages: true.
  • Fix batch hooks firing a spurious timeout warning with 0 items. Alloy skips the hook when scope filtering leaves no payloads.
  • Fix alloy dev skipping virtual pages on incremental rebuilds. Plugin-generated pages re-render when source files change instead of serving stale content.
  • Fix Node bridge protocol corruption when plugins call process.stdout.write(). The bridge captures the real process.stdout.write at startup and redirects subsequent calls to stderr. The error diagnostic names stdout pollution as the cause instead of reporting a generic framing error.
  • Fix QuickJS runtime panic on build teardown when a plugin hook exceeds its timeout. Close() waits for in-flight calls to finish before releasing the runtime.