How Hugo's Shortcode Rendering Pipeline Works: A Deep Dive into the Internals

Hugo's shortcode rendering pipeline parses raw content to detect shortcode calls, analyzes templates for inner content usage, and renders them either immediately or via placeholders that get replaced after Markdown processing completes.

The gohugoio/hugo repository implements a sophisticated multi-phase pipeline for handling shortcodes. When Hugo builds a page, it processes shortcode calls through distinct stages of parsing, template analysis, and deferred rendering. Understanding this Hugo shortcode rendering pipeline is essential for developers building complex shortcodes or debugging rendering issues.

The Three Phases of the Shortcode Rendering Pipeline

Phase 1: Parsing and Shortcode Detection

The pipeline begins in hugolib/page__content.go where the page parser tokenizes the raw source. When the parser encounters {{< … >}} or {{% … %}} syntax, it creates a *shortcode struct for each call.

This struct captures:

  • Name and parameters parsed from the shortcode call
  • Position data (byte offsets) used later for placeholder substitution
  • Inner content (raw text or nested shortcodes) for block-level shortcodes
  • Inline flag indicating whether the shortcode appears inline with text

The parser stores these *shortcode instances in content.pi.itemsStep2, making them available for the rendering phase.

Phase 2: Template Analysis and Inner Content Flagging

Before rendering begins, Hugo analyzes shortcode templates to detect inner content usage. In tpl/tplimpl/templatetransform.go, the function collectInnerInShortcode inspects the template AST during compilation.

If the template accesses .Inner or .InnerDeindent, the function sets ParseInfo.IsInner = true. This flag is critical because it instructs the renderer to:

  • Capture the rendered inner markup
  • Pass it back to the shortcode template during execution
  • Handle the content appropriately based on whether the shortcode uses % (Markdown) or < (HTML) delimiters

Phase 3: Rendering and Placeholder Handling

The final phase occurs in hugolib/shortcode.go through prepareShortcode, which constructs a shortcodeRenderFunc closure. This function determines whether to render immediately or defer via placeholders.

Immediate rendering occurs when doRenderShortcode executes the template with a ShortcodeWithPage context, returning rendered bytes and a "more" flag indicating if additional processing is needed.

Deferred rendering uses placeholder substitution. When insertPlaceholder returns true, Hugo generates a unique placeholder string (HAHAHUGOSHORTCODE…HBHB) and writes it into the output buffer instead of the rendered content. After Goldmark finishes processing the Markdown, Hugo scans for these placeholders and replaces them with the final rendered shortcode HTML.

If the page uses Markdown and the shortcode sets doMarkup, the context flag tpl.Context.IsInGoldmark enables Goldmark to re-parse the rendered result.

Step-by-Step Walkthrough of the Rendering Process

Content Parsing Details

The pageparser (indirectly referenced through parser/pageparser/pageparser.go) tokenizes the source file and populates content.pi.itemsStep2 with *shortcode items. Each item tracks its byte position to enable precise placeholder replacement later.

Template Preparation Details

In tpl/tplimpl/templatestore.go, Hugo loads shortcode templates into the TemplateStore. During this process, collectInnerInShortcode analyzes whether templates use .Inner, setting ParseInfo.IsInner accordingly.

Render Loop Execution

The RenderShortcodes method in hugolib/page__content.go (lines 993-1020) iterates over itemsStep2. For each shortcode:

  1. It checks whether to insert a placeholder or render immediately
  2. If rendering immediately, it calls the shortcodeRenderFunc returned by prepareShortcode
  3. The function executes doRenderShortcode, which loads the TemplInfo, executes the template with ShortcodeWithPage context, and returns rendered bytes

Placeholder Replacement Mechanics

After Goldmark processes the Markdown content, Hugo scans for the placeholder pattern HAHAHUGOSHORTCODE…HBHB. It looks up the corresponding rendered shortcode in contentPlaceholders and substitutes the placeholder with the final HTML. This enables shortcodes to output raw HTML that should not be escaped by the Markdown processor.

Final Output Generation

The fully-rendered byte slice is wrapped as template.HTML or, when Goldmark is still active, wrapped with hugocontext.Wrap to preserve the current page stack context.

Key Source Files and Their Roles

File Purpose
hugolib/page__content.go Contains RenderShortcodes (lines 993-1020), the core loop that processes shortcode items and manages placeholder substitution
hugolib/shortcode.go Defines the shortcode struct, prepareShortcode, and doRenderShortcode; handles placeholder generation (HAHAHUGOSHORTCODE…HBHB) and template execution
tpl/tplimpl/templatetransform.go Implements collectInnerInShortcode (lines 20-44) to detect .Inner usage in templates and set ParseInfo.IsInner
tpl/tplimpl/templatestore.go Stores and retrieves compiled shortcode templates via the TemplateStore
parser/pageparser/pageparser.go Tokenizes source content and creates the initial *shortcode items that feed the pipeline

Practical Code Examples

Rendering Shortcodes Programmatically

To render a page's shortcodes from Go code:

page := site.GetPage("mysection/mypage.md")
html, err := page.RenderShortcodes(context.Background())
if err != nil {
    log.Fatalf("render failed: %s", err)
}
fmt.Println(html) // fully-rendered content with all shortcodes resolved

Creating an Inline Shortcode Template

Create layouts/shortcodes/hello.html:

{{ $_hugo_config := `{ "version": 1 }` }}
<h1>Hello {{ .Get 0 }}</h1>

When Hugo processes this template:

  1. collectInnerInShortcode detects no .Inner usage, leaving ParseInfo.IsInner as false
  2. During rendering, doRenderShortcode executes the template with ShortcodeWithPage context
  3. If the shortcode uses {{% %}} delimiters and doMarkup is true, tpl.Context.IsInGoldmark enables Goldmark to process the rendered <h1> tag

Summary

  • Hugo's shortcode rendering pipeline operates in three distinct phases: parsing and detection, template analysis, and rendering with placeholder handling
  • The *shortcode struct in hugolib/shortcode.go serves as the lightweight representation of shortcode calls, tracking parameters, inner content, and position data
  • collectInnerInShortcode in tpl/tplimpl/templatetransform.go analyzes templates during compilation to detect .Inner usage, setting ParseInfo.IsInner to control content flow
  • Placeholder substitution (using strings like `HAHAHUGOSHORTCODE…

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →