Render Hook Implementation Lessons
January 9, 2026 ยท View on GitHub
Overview
This document captures key lessons learned while implementing the checklist render hook for Hugo. These insights should help guide future render hook implementations.
1. Progressive Enhancement Pattern
Key Principle: Preserve the original Markdown source in a <pre> element for non-JS viewers and AI agents, then progressively enhance it with JavaScript for users with JS enabled.
Implementation:
<pre class="checklist-source" data-checklist-id="{{ $id }}">{{ .Inner | htmlEscape | safeHTML }}</pre>
{{ .Page.Store.Set "hasChecklist" true }}
Benefits:
- AI agents can easily parse the raw Markdown source (e.g., via
.html.mdURLs) - Graceful degradation for users without JavaScript
- SEO-friendly (content is in the HTML)
- Easier to debug and maintain
Lesson: Always preserve the source content in a way that's accessible without JavaScript.
2. Avoiding Duplicate JavaScript on Multi-Component Pages
Problem: If a page has multiple checklists, the JavaScript would be loaded multiple times, causing inefficiency and potential conflicts.
Solution: Use Hugo's page store pattern (like Mermaid does):
-
Render hook sets a flag:
{{ .Page.Store.Set "hasChecklist" true }} -
Base template conditionally loads the script:
{{ if .Page.Store.Get "hasChecklist" }} <script src="{{ "js/checklist.js" | relURL }}"></script> {{ end }} -
JavaScript finds all instances and processes them:
const checklists = document.querySelectorAll('pre.checklist-source'); checklists.forEach(pre => { /* process each */ });
Lesson: Use page store flags to conditionally load resources only when needed, and design JavaScript to handle multiple instances on a single page.
3. Static Files vs Asset Pipeline
Problem: Initially placed checklist.js in assets/js/ but got 404 errors.
Solution: Hugo's static JavaScript files must go in static/js/, not assets/js/.
Explanation:
assets/directory: For CSS, images, and files processed by Hugo's asset pipelinestatic/directory: For files served directly as-is (JavaScript, fonts, etc.)
Lesson: Know the difference between Hugo's asset pipeline and static files. JavaScript typically goes in static/js/.
4. Security: Avoid innerHTML with Dynamic Content
Problem: Using innerHTML with template literals containing dynamic IDs is an XSS vulnerability vector.
Bad:
countersDiv.innerHTML = `<label for="${formId}-count">...</label>`;
Good:
const label = document.createElement('label');
label.htmlFor = formId + '-count';
label.textContent = 'text';
countersDiv.appendChild(label);
Additional Tips:
- Use
textContentinstead ofinnerHTMLfor text content - Use
document.createTextNode()for text nodes - Use
document.createDocumentFragment()for efficient DOM building - Use actual emoji characters instead of HTML entities (safer and cleaner)
Lesson: Always use safe DOM methods. Even if the data source is controlled, defense-in-depth is important.
5. Code Block Attributes and Extraction
Pattern: Hugo render hooks receive code block attributes via .Attributes.
Example:
```checklist {id="pyprodlist"}
- [ ] Item 1
- [ ] Item 2
**Extraction in render hook**:
```html
{{- $id := .Attributes.id | default "checklist" -}}
Lesson: Use .Attributes to extract custom parameters from code block headers. Provide sensible defaults.
6. Data Attributes for JavaScript Communication
Pattern: Use data-* attributes to pass information from HTML to JavaScript.
Example:
<pre class="checklist-source" data-checklist-id="{{ $id }}">...</pre>
JavaScript access:
const checklistId = pre.getAttribute('data-checklist-id');
Lesson: Data attributes are the clean way to pass server-side data to client-side JavaScript. Avoid embedding data in class names or other hacks.
7. Markdown Parsing in JavaScript
Pattern: Parse Markdown in JavaScript using regex patterns.
Example (checklist items):
const linkMatch = item.match(/\[([^\]]+)\]\(([^\)]+)\)/);
if (linkMatch) {
const a = document.createElement('a');
a.href = linkMatch[2];
a.textContent = linkMatch[1];
}
Lesson: For simple Markdown patterns, regex is sufficient. For complex parsing, consider a lightweight Markdown parser library.
8. State Persistence with localStorage
Pattern: Use localStorage to persist user interactions across page reloads.
Example:
// Save state
localStorage.setItem(formId, itemChoices);
// Load state
let itemString = localStorage.getItem(formId);
if (itemString) {
setCLItemsFromString(formId, itemString);
}
Considerations:
- Use unique keys (e.g., based on checklist ID) to avoid conflicts
- Handle missing/corrupted data gracefully
- Consider privacy implications
Lesson: localStorage is useful for persisting user state, but always validate and handle edge cases.
9. Template Context Limitations
Problem: Render hook context doesn't have access to .Page.Store directly in some Hugo versions.
Solution: Use .Page.Store in the render hook template, not .Store.
Lesson: Understand the context available in render hooks vs other templates. Test with your Hugo version.
10. Testing Multiple Instances
Pattern: Always test with multiple instances of the component on the same page.
Why:
- ID conflicts can occur
- Event handlers might interfere
- localStorage keys might collide
- Performance issues might only appear with multiple instances
Lesson: Test with at least 2-3 instances of your component on the same page before considering it complete.
11. Consistency Across Similar Components
Pattern: When implementing similar components (like checklists for multiple client libraries), use the same Markdown format and render hook.
Benefits:
- Easier maintenance
- Consistent user experience
- Easier for AI agents to parse
- Reduces code duplication
Lesson: Design render hooks to be reusable across similar content. Use consistent naming conventions and ID patterns.
12. Accessibility Considerations
Implemented:
- Proper
<label>elements withhtmlForattributes - Semantic HTML (
<form>,<select>,<output>) - Text content accessible without JavaScript
Lesson: Build accessibility in from the start. Use semantic HTML and proper ARIA relationships.
13. Custom Parsing for Structured Data Formats
Pattern: When working with structured data (YAML, JSON, etc.), implement a custom parser in JavaScript rather than relying on external libraries.
Example (YAML parser for hierarchies):
function parseYAML(yamlText) {
const lines = yamlText.split('\n');
const root = {};
const stack = [{ node: root, indent: -1 }];
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
const line = lines[lineIndex];
const indent = line.search(/\S/);
// ... parse logic
}
return root;
}
Considerations:
- Custom parsers are lightweight and don't require external dependencies
- They can be tailored to your specific format needs
- For complex formats, consider a lightweight library instead
- Always handle edge cases (empty lines, comments, special characters)
Lesson: A simple custom parser can be more efficient than adding a library dependency, especially for domain-specific formats.
14. SVG Generation for Complex Diagrams
Pattern: Use SVG for rendering complex visual structures (trees, hierarchies, etc.) rather than HTML/CSS.
Benefits:
- Precise control over positioning and lines
- Efficient rendering of complex structures
- Scalable without quality loss
- Can draw arbitrary shapes and connectors
Example (drawing tree lines):
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
line.setAttribute('stroke', '#999');
svg.appendChild(line);
Lesson: For hierarchical or graph-like structures, SVG is more appropriate than HTML/CSS. Use createElementNS with the SVG namespace.
15. Attribute Case Sensitivity in Hugo
Problem: Hugo code block attributes are case-sensitive and converted to lowercase.
Example:
```hierarchy {type="filesystem" noIcons="true"}
In the render hook, access as:
```html
{{- $noIcons := .Attributes.noicons -}}
Not:
{{- $noIcons := .Attributes.noIcons -}}
Lesson: Hugo converts attribute names to lowercase. Always use lowercase when accessing .Attributes in render hooks.
16. Metadata Parsing with Line Tracking
Pattern: When parsing nested structures with metadata, track which lines have been processed to avoid duplicate processing.
Example:
const skipLines = new Set();
// When processing metadata
while (i < lines.length) {
// ... parse metadata
skipLines.add(i);
i++;
}
// In main loop
if (skipLines.has(lineIndex)) continue;
Lesson: Use a Set to track processed lines when parsing complex nested structures. This prevents metadata properties from being treated as separate items.
17. Dimension Calculation for Dynamic Content
Pattern: Calculate SVG/container dimensions based on content, accounting for all visual elements.
Considerations:
- Text width varies by font and character
- Icons, decorations, and spacing add to dimensions
- Comments or descriptions may extend width
- Nesting depth affects height
Example:
const charWidth = 8; // Space Mono at 14px
const iconSize = 16;
const iconGap = 6;
const commentGap = 40;
const iconOffset = showIcons ? iconSize + iconGap : 0;
const svgWidth = leftMargin + (maxDepth + 1) * indentWidth + iconOffset +
maxTextWidth * charWidth + commentGap + maxCommentWidth * charWidth + 20;
Lesson: Account for all visual elements when calculating dimensions. Test with various content sizes to ensure proper layout.
18. Progressive Enhancement with Type-Specific Features
Pattern: Use the type attribute to enable type-specific rendering features while maintaining a generic base.
Example:
const showIcons = hierarchyType === 'filesystem' && !noIcons;
Benefits:
- Single render hook handles multiple hierarchy types
- Type-specific features don't interfere with other types
- Easy to add new types in the future
- Keeps code organized and maintainable
Lesson: Use type attributes to conditionally enable features. This allows one render hook to serve multiple purposes.
19. String Unescaping in Parsed Data
Pattern: When parsing quoted strings from YAML/JSON, properly unescape special characters.
Example:
if ((metaValue.startsWith('"') && metaValue.endsWith('"'))) {
metaValue = metaValue.slice(1, -1);
metaValue = metaValue.replace(/\\"/g, '"');
metaValue = metaValue.replace(/\\\\/g, '\\');
}
Lesson: Handle quote removal and character unescaping during parsing, not during rendering. This keeps rendering logic clean.
20. Documentation Format Specification
Pattern: Create a separate documentation file specifying the format for your render hook.
Should Include:
- Basic syntax with examples
- Metadata fields and their purposes
- Type-specific features
- Code block attributes
- Real-world examples
- Best practices
Benefits:
- Helps users understand the format
- Guides future implementations
- Serves as reference for AI agents
- Reduces support questions
Lesson: Invest time in clear format documentation. It pays dividends in usability and maintainability.
21. Server-Side Metadata Extraction for AI Agents
Pattern: Extract metadata from structured content (YAML, JSON) in the Hugo render hook and embed it as JSON in the HTML output for AI agents that don't execute JavaScript.
Implementation:
{{- /* Extract top-level fields only (no indentation) */ -}}
{{- $lines := split .Inner "\n" -}}
{{- $id := "" -}}
{{- range $lines -}}
{{- /* Check if line starts without whitespace (32=space, 9=tab) */ -}}
{{- if and (gt (len .) 0) (ne (index . 0) 32) (ne (index . 0) 9) -}}
{{- $trimmed := strings.TrimSpace . -}}
{{- if strings.HasPrefix $trimmed "id:" -}}
{{- $afterPrefix := strings.Replace $trimmed "id:" "" 1 -}}
{{- $id = strings.TrimSpace $afterPrefix -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* Embed as JSON for AI agents */ -}}
{{- $metadata := dict "type" "decision-tree" "id" $id -}}
{{ $jsonMetadata := $metadata | jsonify (dict "indent" " ") }}
{{ printf "<script type=\"application/json\" data-redis-metadata=\"decision-tree\">\n%s\n</script>" $jsonMetadata | safeHTML }}
Key Considerations:
- Indentation detection: When parsing nested structures, only extract top-level fields by checking if the line starts with whitespace
- String manipulation: Use
strings.Replaceinstead ofstrings.TrimPrefixfor more reliable extraction - Character codes: Use ASCII codes (32 for space, 9 for tab) to detect indentation reliably
- Metadata format: Use simple JSON (not JSON-LD) for clarity and ease of parsing
- Data attributes: Use
data-*attributes to mark metadata elements for AI agents
Why This Matters:
- AI agents typically don't execute JavaScript, so metadata must be in static HTML
- Server-side extraction ensures metadata is available even if JavaScript fails
- Structured metadata helps AI agents understand the purpose and scope of components
- Separating metadata from content improves maintainability
Lesson: Always provide metadata in static HTML for AI agents. Use server-side extraction to ensure accuracy and avoid relying on JavaScript parsing.
22. Handling Nested Structures in Hugo Templates
Pattern: When extracting data from nested YAML/JSON structures, distinguish between top-level and nested fields using indentation detection.
Problem: If you extract all occurrences of a field (e.g., id:), you'll get nested occurrences too, leading to incorrect values.
Solution: Check indentation before processing:
{{- if and (gt (len .) 0) (ne (index . 0) 32) (ne (index . 0) 9) -}}
{{- /* Process only top-level lines */ -}}
{{- end -}}
Why This Works:
- YAML indentation is significant and indicates nesting level
- Top-level fields have no leading whitespace
- Nested fields have leading spaces or tabs
- Character code 32 = space, 9 = tab
Lesson: When parsing nested structures in Hugo templates, use indentation detection to distinguish between levels. This prevents extracting nested values when you only want top-level ones.
23. Progressive Enhancement with Metadata
Pattern: Combine progressive enhancement with metadata embedding to serve both humans and AI agents from a single source.
Architecture:
-
Server-side (Hugo):
- Extract metadata from source content
- Embed metadata as JSON in HTML
- Preserve raw source in
<pre>element
-
Client-side (JavaScript):
- Parse raw source for rendering
- Use metadata for context/identification
- Enhance with interactivity
-
AI agents:
- Read static JSON metadata
- Parse raw source from
<pre>element - No JavaScript execution needed
Benefits:
- Single source of truth (the YAML/JSON in the Markdown)
- Metadata available to all consumers (humans, AI agents, JavaScript)
- Graceful degradation if JavaScript fails
- AI-friendly without extra work
Lesson: Design render hooks to serve multiple audiences simultaneously. Metadata should be available in static HTML, not just in JavaScript.
24. Text Wrapping and Box Sizing in SVG Diagrams
Pattern: When rendering text in SVG boxes, calculate dimensions based on character width and implement text wrapping to fit within maximum width.
Implementation:
const charWidth = 8; // Space Mono at 14px
const maxBoxWidth = 420;
const maxCharsPerLine = Math.floor(maxBoxWidth / charWidth);
function wrapText(text, maxChars) {
const words = text.split(' ');
const lines = [];
let currentLine = '';
for (const word of words) {
if ((currentLine + ' ' + word).length > maxChars) {
if (currentLine) lines.push(currentLine);
currentLine = word;
} else {
currentLine = currentLine ? currentLine + ' ' + word : word;
}
}
if (currentLine) lines.push(currentLine);
return lines;
}
Considerations:
- Font metrics: Different fonts have different character widths
- Padding: Account for box padding when calculating available width
- Line height: Multiply number of lines by line height for total box height
- Dynamic sizing: Calculate SVG dimensions based on content, not fixed values
Common Pitfall: Hardcoding SVG width can cause content to be cut off. Instead:
const svgWidth = leftMargin + (maxDepth + 1) * indentWidth + maxBoxWidth + 40;
Lesson: Calculate SVG dimensions dynamically based on content. Account for all visual elements (padding, margins, decorations) when sizing boxes and containers.
25. Scope and Context Metadata for Component Discovery
Pattern: Add scope or category metadata to components to help AI agents understand their purpose and applicability.
Implementation:
id: documents-tree
scope: documents
rootQuestion: root
questions:
# ...
Benefits:
- Discoverability: AI agents can filter components by scope
- Context awareness: Agents know which problem domain each component addresses
- Prevents misapplication: Agents won't use a "collections" tree to recommend document storage
- Relationship mapping: Enables linking related components
Use Cases:
- Filtering decision trees by data type category
- Finding all components related to a specific feature
- Organizing components hierarchically
- Providing context in search results
Lesson: Add semantic metadata (scope, category, type) to components. This helps AI agents understand purpose and applicability, enabling better recommendations and filtering.
Quick Checklist for Future Render Hooks
Core Patterns
- Preserve source content in a
<pre>or similar element - Use page store pattern to avoid duplicate resource loading
- Place static JavaScript in
static/js/, notassets/js/ - Avoid
innerHTMLwith dynamic content; use safe DOM methods - Use
data-*attributes to pass server data to JavaScript
Testing & Accessibility
- Test with multiple instances on the same page
- Consider state persistence if needed
- Use semantic HTML and proper accessibility attributes
- Document the Markdown format clearly
- Provide sensible defaults for optional parameters
Hugo-Specific
- Remember Hugo converts attribute names to lowercase
- Use indentation detection when parsing nested structures
- Extract top-level metadata in render hook (not JavaScript)
- Use
strings.Replacefor reliable string manipulation in templates
Advanced Features
- Use SVG for complex visual structures
- Track processed lines when parsing nested structures
- Account for all visual elements in dimension calculations
- Use type attributes for conditional features
- Handle string unescaping during parsing, not rendering
- Create comprehensive format documentation
AI Agent Compatibility
- Embed metadata as JSON in static HTML (not just JavaScript)
- Add
scopeorcategorymetadata for component discovery - Use
data-*attributes to mark metadata elements - Ensure metadata is available without JavaScript execution
- Preserve raw source content for AI parsing