Shortcodes
Shortcodes are reusable content snippets that accept arguments and output HTML. They let you embed rich elements in Markdown content without writing raw HTML.
Shortcodes are registered via plugins and used in content files as custom Liquid tags (or Go template functions).
Using shortcodes in content
Inline shortcodes
Inline shortcodes take positional arguments and produce self-contained output:
Block shortcodes
Block shortcodes wrap inner content, letting you add markup around authored text:
Engine differences
| Liquid | Go templates | |
|---|---|---|
| Inline | {% tag "arg" %} |
{{ tag "arg" }} |
| Block open | {% tag "arg" %} |
{{% tag "arg" %}} |
| Block close | {% endtag %} |
{{% /tag %}} |
| Inner content | Rendered HTML | Rendered HTML |
| Plugin callback | (args, content) |
(args, content) |
Both engines pass rendered HTML to the plugin callback — the same alloy.shortcode() plugin works for both engines with no engine-specific code.
Code block escaping
{{% %}} delimiters inside fenced code blocks and inline <code> elements are treated as literal text, not shortcode invocations.
Variable arguments (Liquid)
In Liquid templates, unquoted shortcode arguments resolve from the template context instead of being passed as literal strings:
Dotted paths like page.videoId traverse nested maps in the template context.
Mixed arguments
Quoted and unquoted arguments work in the same tag:
The first argument is the literal string "primary". The second resolves page.size from the context.
Fallback behavior
When an unquoted argument does not match any context variable, it falls back to its literal token string. {% youtube nonexistent %} passes "nonexistent" to the shortcode callback. This preserves backward compatibility — existing shortcodes that used unquoted literal strings continue to work.
Empty return
Shortcodes that return an empty string produce no output. Previous versions emitted an <alloy-shortcode> placeholder element — this is no longer the case.
Registering shortcodes
Shortcodes are defined in plugin files placed in the plugins/ directory. No configuration is needed – drop a file in plugins/ and its shortcodes are immediately available in all content files.
JS plugin (Tier 2 – in-process)
The simplest way to define shortcodes. JS plugins run on embedded QuickJS with no build step:
The first argument to alloy.shortcode() is the tag name. The callback receives an args array of positional arguments. Block shortcodes receive a second content parameter containing the inner content.
Node plugin (Tier 3 – full Node.js access)
Use Tier 3 when your shortcode needs npm packages, filesystem access, or network calls:
Tier 3 plugins must have "type": "module" in the project’s package.json.
WASM plugin (Tier 2 – compiled)
For maximum performance, compile shortcodes to WASM from Rust, TinyGo, or AssemblyScript:
Rust:
TinyGo:
JS plugins run at ~10-50 microseconds per call. Compiled WASM runs at ~1-10 microseconds per call. Choose based on whether you need the simplicity of plain JS or the performance of compiled code.
Accessing site data
Shortcode plugins can access global data from data/ files via alloy.data:
alloy.data is a read-only snapshot of site.data injected after data files are loaded. Access it inside shortcode functions, not at the top level of the plugin file – top-level access during evaluation returns undefined.
Practical examples
Responsive image shortcode
Admonition block shortcode
Name conflicts
If two plugins register the same shortcode name, the last one loaded wins. Plugins load in alphabetical filename order within plugins/: built-in Go functions first, then Tier 2 (.js and .wasm), then Tier 3 (.js with runtime: "node").
Alloy logs a warning when a name collision occurs so you know which plugin took precedence.
Related
- Filters – transform values in template expressions
- Plugins – plugin system overview and tiers
- Templates Overview – template engine basics and context