QuickJS Plugins
QuickJS plugins are plain JavaScript files that run on an embedded QuickJS engine inside the Alloy process. No Node.js dependency, no build step – drop a .js file in plugins/ and it works immediately.
// plugins/reading-time.js
export default function(alloy) {
alloy.filter("readingTime", (content) => {
const words = content.split(/\s+/).filter(w => w.length > 0).length;
const minutes = Math.ceil(words / 200);
return `${minutes} min read`;
});
}
<span>{{ page.content | readingTime }}</span>
How It Works
Alloy embeds a single QuickJS instance compiled to WASM, running via wazero (pure Go, zero CGo). Your .js files are evaluated in this shared context at startup.
- Startup cost: ~10-50ms one-time
- Per-call cost: ~10-50 microseconds
- Memory: ~2-4MB
QuickJS plugins are sandboxed – they cannot access the filesystem, network, or system resources.
Registering Filters
Filters transform values in template expressions:
// plugins/filters.js
export default function(alloy) {
// Simple string filter
alloy.filter("initials", (name) => {
return name.split(' ').map(w => w[0]).join('');
});
// Filter with arguments
alloy.filter("truncateAt", (text, maxLength) => {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
});
// Numeric filter
alloy.filter("percentage", (value, total) => {
return Math.round((value / total) * 100) + '%';
});
}
Use in templates:
{{ page.author | initials }}
{{ page.description | truncateAt: 120 }}
{{ page.score | percentage: page.maxScore }}
Filter arguments are passed as additional parameters after the input value.
Registering Shortcodes
Shortcodes embed rich HTML snippets in content files:
// plugins/shortcodes.js
export default function(alloy) {
// Inline shortcode (self-closing)
alloy.shortcode("youtube", (args) => {
const id = args[0];
return `<iframe src="https://www.youtube.com/embed/${id}"
frameborder="0" allowfullscreen></iframe>`;
});
// Block shortcode (wraps content)
alloy.shortcode("callout", (args, content) => {
const type = args[0] || "info";
return `<div class="callout callout--${type}">${content}</div>`;
});
// Shortcode using site data
alloy.shortcode("componentDemo", (args) => {
const tagName = args[0];
const elements = alloy.data.elements || [];
const el = elements.find(e => e.tagName === tagName);
if (!el) return `<!-- unknown component: ${tagName} -->`;
return `<div class="demo">
<h3>${el.name}</h3>
<${el.tagName}></${el.tagName}>
</div>`;
});
}
Use in content:
{% youtube "dQw4w9WgXcQ" %}
{% callout "warning" %}
Do not use this in production without testing first.
{% endcallout %}
{% componentDemo "rh-button" %}
Registering Hooks
Hooks let plugins run code at specific points in the build pipeline:
// plugins/transforms.js
export default function(alloy) {
// Add lazy loading to all images
alloy.hook("onContentTransformed", {}, (page) => {
page.html = page.html.replace(/<img /g, '<img loading="lazy" ');
return page;
});
// Minify final HTML output
alloy.hook("onPageRendered", {}, (html) => {
return html.replace(/\s+/g, ' ').trim();
});
}
Hook Options
The second argument to alloy.hook() is a required options object:
// Control execution order with priority (lower runs first, default 50)
alloy.hook("onPageRendered", { priority: 10 }, earlyTransformFn);
alloy.hook("onPageRendered", { priority: 100 }, lateTransformFn);
// Declare what data the hook needs (reduces serialization cost)
alloy.hook("onContentLoaded", {
data: ["navigation"], // only serialize these site.data keys
pages: "/blog/**", // only receive blog pages
pageFields: ["frontMatter", "url"] // only these fields per page
}, fn);
See Lifecycle Events for all available hooks and Hook Scoping for the full scoping API.
Accessing Site Data
alloy.data provides read-only access to the same data available as site.data in templates:
export default function(alloy) {
alloy.filter("teamMember", (slug) => {
const team = alloy.data.team || [];
const member = team.find(m => m.slug === slug);
return member ? member.name : slug;
});
}
Access alloy.data inside filter, shortcode, and hook functions – not at the top level of your plugin. During plugin evaluation, alloy.data is undefined.
To modify data, use hooks like onDataFetched:
alloy.hook("onDataFetched", { data: ["team"] }, (data) => {
data.teamCount = data.team.length;
return data;
});
QuickJS vs Node
A .js file in plugins/ runs on QuickJS by default. If your plugin needs Node APIs or npm packages, add runtime: "node":
// This runs on QuickJS (default)
export default function(alloy) { /* ... */ }
// This runs on Node (Tier 3)
export const runtime = "node";
export default function(alloy) { /* ... */ }
See Node Plugins for details on Tier 3 plugins.
Limitations
- No filesystem access (
fs,path) - No network access (
fetch,http) - No Node.js APIs or npm packages
- No
require()orimportof external modules alloy.datais read-only – mutations do not propagate to templates
For any of these capabilities, use a Node plugin instead.
Related
- Plugin System – overview and tier comparison
- WASM Plugins – compiled plugins for maximum performance
- Node Plugins – full Node.js access
- Lifecycle Events – all hook events and payloads