for-ais-only/render_hook_docs/AI_RENDER_HOOK_LESSONS.md
This document captures key lessons learned while implementing the checklist render hook for Hugo. These insights should help guide future render hook implementations.
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:
.html.md URLs)Lesson: Always preserve the source content in a way that's accessible without JavaScript.
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.
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/.
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:
textContent instead of innerHTML for text contentdocument.createTextNode() for text nodesdocument.createDocumentFragment() for efficient DOM buildingLesson: Always use safe DOM methods. Even if the data source is controlled, defense-in-depth is important.
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.
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.
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.
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:
Lesson: localStorage is useful for persisting user state, but always validate and handle edge cases.
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.
Pattern: Always test with multiple instances of the component on the same page.
Why:
Lesson: Test with at least 2-3 instances of your component on the same page before considering it complete.
Pattern: When implementing similar components (like checklists for multiple client libraries), use the same Markdown format and render hook.
Benefits:
Lesson: Design render hooks to be reusable across similar content. Use consistent naming conventions and ID patterns.
Implemented:
<label> elements with htmlFor attributes<form>, <select>, <output>)Lesson: Build accessibility in from the start. Use semantic HTML and proper ARIA relationships.
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:
Lesson: A simple custom parser can be more efficient than adding a library dependency, especially for domain-specific formats.
Pattern: Use SVG for rendering complex visual structures (trees, hierarchies, etc.) rather than HTML/CSS.
Benefits:
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.
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.
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.
Pattern: Calculate SVG/container dimensions based on content, accounting for all visual elements.
Considerations:
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.
Pattern: Use the type attribute to enable type-specific rendering features while maintaining a generic base.
Example:
const showIcons = hierarchyType === 'filesystem' && !noIcons;
Benefits:
Lesson: Use type attributes to conditionally enable features. This allows one render hook to serve multiple purposes.
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.
Pattern: Create a separate documentation file specifying the format for your render hook.
Should Include:
Benefits:
Lesson: Invest time in clear format documentation. It pays dividends in usability and maintainability.
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:
strings.Replace instead of strings.TrimPrefix for more reliable extractiondata-* attributes to mark metadata elements for AI agentsWhy This Matters:
Lesson: Always provide metadata in static HTML for AI agents. Use server-side extraction to ensure accuracy and avoid relying on JavaScript parsing.
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:
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.
Pattern: Combine progressive enhancement with metadata embedding to serve both humans and AI agents from a single source.
Architecture:
Server-side (Hugo):
<pre> elementClient-side (JavaScript):
AI agents:
<pre> elementBenefits:
Lesson: Design render hooks to serve multiple audiences simultaneously. Metadata should be available in static HTML, not just in JavaScript.
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:
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.
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:
Use Cases:
Lesson: Add semantic metadata (scope, category, type) to components. This helps AI agents understand purpose and applicability, enabling better recommendations and filtering.
<pre> or similar elementstatic/js/, not assets/js/innerHTML with dynamic content; use safe DOM methodsdata-* attributes to pass server data to JavaScriptstrings.Replace for reliable string manipulation in templatesscope or category metadata for component discoverydata-* attributes to mark metadata elements