Render Hooks

Render hooks let you replace Goldmark’s default HTML output for specific Markdown elements with your own Liquid templates. Place a template in layouts/_markup/ and Alloy uses it instead of the default rendering for that element type.

layouts/_markup/
├── render-blockquote.liquid
├── render-codeblock.liquid
├── render-codeblock-mermaid.liquid
├── render-heading.liquid
├── render-image.liquid
├── render-link.liquid
└── render-table.liquid

If no render hook template exists for an element type, Goldmark’s default rendering applies. Render hooks run during Markdown rendering — before template tag processing and layout rendering.

Supported hooks

Template file Markdown element
render-blockquote.liquid Blockquotes (>)
render-codeblock.liquid Fenced code blocks (```)
render-codeblock-{lang}.liquid Language-specific code blocks
render-heading.liquid Headings (#, ##, ###, etc.)
render-image.liquid Images (![alt](src))
render-link.liquid Links ([text](url))
render-table.liquid Tables (`

Template context

Each render hook template receives a markup object with properties specific to the element type:

Template markup.* properties
render-blockquote inner (rendered inner HTML), attributes
render-codeblock inner (raw code text), language, attributes
render-heading inner (rendered HTML), text (plain text), level (1–6), id (auto-generated slug or {#custom-id} override), attributes
render-image src, alt, title
render-link destination, text (plain link text), title (from [text](url "title")), is_external (boolean)
render-table inner (rendered inner HTML), attributes

markup.attributes is a map of key-value pairs from block-level attribute syntax. It is an empty map (not nil) when no attributes are present on the element. render-image does not receive attributes – images are inline elements and do not support attribute syntax.

The markup object is the only context available inside a render hook – page.* and site.* are not passed to hook templates. Hooks are pure element transformers.

Engine selection

Render hook templates follow the configured template engine:

Engine Config value Hook file extension Syntax
Liquid "liquid" (default) .liquid {{ markup.language }}
Go templates "gotemplate" or "go" .html {{ .markup.language }}

Language-specific code blocks

render-codeblock-{language}.liquid overrides rendering for a specific fenced code block language. For example, render-codeblock-mermaid.liquid handles only mermaid blocks.

Lookup order:

  1. render-codeblock-{language}.liquid — language-specific match
  2. render-codeblock.liquid — generic fallback
  3. Default Goldmark rendering — no hook exists

Examples

Custom code block rendering

Output a web component instead of the default <pre><code>:

<!-- layouts/_markup/render-codeblock.liquid -->
<alloy-code lang=&#34;{{ markup.language }}&#34;>{{ markup.inner }}</alloy-code>

Pair this with a build plugin that applies syntax highlighting to <alloy-code> elements during onPageRendered, and the component handles copy-to-clipboard and filename display on the client.

Add target="_blank" and an icon to external links:

Liquid Go templates
<!-- layouts/_markup/render-link.liquid -->
{% if markup.is_external %}
  <a href="{{ markup.destination }}" target="_blank" rel="noopener">{{ markup.text }} ↗</a>
{% else %}
  <a href="{{ markup.destination }}">{{ markup.text }}</a>
{% endif %}
<!-- layouts/_markup/render-link.html -->
{{ if .markup.is_external }}
  <a href="{{ .markup.destination }}" target="_blank" rel="noopener">{{ .markup.text }} ↗</a>
{{ else }}
  <a href="{{ .markup.destination }}">{{ .markup.text }}</a>
{{ end }}

Mermaid diagrams

Render mermaid code blocks as diagram containers instead of code:

<!-- layouts/_markup/render-codeblock-mermaid.liquid -->
<div class=&#34;mermaid&#34;>{{ markup.inner }}</div>

Standard code blocks still use the generic render-codeblock.liquid (or default rendering). Only blocks tagged with ```mermaid are affected.

Auto-linked headings

Add anchor links to headings. markup.inner contains the rendered HTML (so ## Hello **world** gives Hello <strong>world</strong>), while markup.text contains the plain text (Hello world):

<!-- layouts/_markup/render-heading.liquid -->
<h{{ markup.level }} id=&#34;{{ markup.id }}&#34;>
  <a href=&#34;#{{ markup.id }}&#34; class=&#34;anchor&#34;>#</a>
  {{ markup.inner }}
</h{{ markup.level }}>

The markup.id is auto-generated from the heading text using Alloy’s slugify function (e.g., “My Section” becomes my-section). A {#custom-id} attribute overrides the auto-generated slug.

Styled code blocks with attributes

Use markup.attributes to pass classes and IDs from Markdown to your hook output:

<!-- layouts/_markup/render-codeblock.liquid -->
<pre class=&#34;{{ markup.attributes.class }}&#34;{% if markup.attributes.id %} id=&#34;{{ markup.attributes.id }}&#34;{% endif %}>
  <code data-lang=&#34;{{ markup.language }}&#34;>{{ markup.inner }}</code>
</pre>

Block-level attributes

Alloy supports {.class #id key=value} attribute syntax on block elements. Attributes are automatically enabled when autoHeadingID is true (the default).

Supported elements:

  • Headings – on the same line: ## My Heading {.subtitle #intro}
  • Fenced code blocks – on the opening fence: ```go {.highlight #example}
  • Blockquotes – trailing line after the quote: {.callout}
  • Tables – trailing line after the table: {.striped}
```go {.highlight #example}
fmt.Println(&#34;hello&#34;)
```

> This is important
{.callout}

| Name  | Role     |
| ----- | -------- |
| Alice | Engineer |
{.striped}

Attributes are exposed to render hooks via markup.attributes. When no attributes are present, markup.attributes is an empty map.

Images and links are inline elements and do not support attribute syntax.

Template tag escaping

The markup.inner content in code block hooks is already escaped by Goldmark — {{ }} and {% %} inside code fences are protected from Liquid processing. No additional escaping is needed in your render hook template.