Decision Tree Implementation Notes
January 23, 2026 · View on GitHub
Key Learnings from Implementation
This document captures insights from implementing the decision tree render hook that go beyond the standard lessons.
1. Metadata Extraction Challenges
Problem: Extracting metadata from nested YAML structures in Hugo templates is tricky because simple field matching returns nested occurrences.
Example: When extracting id: from a decision tree YAML, you might get:
- Top-level:
id: documents-tree✅ - Nested:
id: jsonOutcome❌ (from outcome objects)
Solution: Use indentation detection to distinguish levels:
{{- if and (gt (len .) 0) (ne (index . 0) 32) (ne (index . 0) 9) -}}
{{- /* Process only top-level lines (no leading space/tab) */ -}}
{{- end -}}
Key Insight: Character codes matter - 32 = space, 9 = tab. This is more reliable than string operations.
2. String Manipulation in Hugo Templates
Problem: strings.TrimPrefix doesn't always work as expected in Hugo templates.
Solution: Use strings.Replace with a count parameter instead:
{{- $afterPrefix := strings.Replace $trimmed "id:" "" 1 -}}
{{- $id = strings.TrimSpace $afterPrefix -}}
Why: Replace is more predictable and handles edge cases better than TrimPrefix.
3. Server-Side vs Client-Side Metadata
Key Principle: Metadata for AI agents MUST be in static HTML, not JavaScript.
Why: AI agents typically don't execute JavaScript. If metadata is only created by JavaScript, AI agents won't see it.
Implementation:
- Extract metadata in Hugo render hook
- Embed as
<script type="application/json">in HTML - JavaScript can use the same metadata if needed
- AI agents read static JSON without executing JS
4. Scope Metadata for Discovery
Pattern: Add scope field to help AI agents understand component purpose.
Benefits:
- Enables filtering (e.g., "show me all documents-related trees")
- Prevents misapplication (e.g., using collections tree for documents)
- Improves search and discovery
- Provides semantic context
Example:
id: documents-tree
scope: documents
5. Progressive Enhancement Architecture
Three-Layer Design:
- Server-side (Hugo): Extract metadata, preserve source, set page store flag
- Static HTML: Metadata in JSON, source in
<pre>, ready for AI agents - Client-side (JavaScript): Parse source, render diagrams, enhance with interactivity
Benefit: Each layer works independently. AI agents get metadata without JS. Humans get interactive diagrams. Non-JS users get raw content.
6. SVG Dimension Calculation
Lesson: Never hardcode SVG dimensions. Calculate dynamically based on content.
Common Mistake:
const svgWidth = leftMargin + (maxDepth + 1) * indentWidth + 320; // ❌ Hardcoded
Better:
const svgWidth = leftMargin + (maxDepth + 1) * indentWidth + maxBoxWidth + 40;
Why: Content varies. Hardcoded values cause cutoff issues when content is larger than expected.
7. Text Wrapping in Boxes
Pattern: Calculate max characters per line based on font metrics:
const charWidth = 8; // Space Mono at 14px
const maxCharsPerLine = Math.floor(maxBoxWidth / charWidth);
Considerations:
- Different fonts have different character widths
- Account for padding when calculating available width
- Test with various content lengths
8. Code Block Attributes
Pattern: Use info string attributes to pass metadata to render hook:
```decision-tree {id="documents-tree"}
Access in render hook:
{{- $id := .Attributes.id -}}
Lesson: Keep fence attributes simple. Complex data should go in the YAML body.
9. Testing Multiple Instances
Critical: Always test with 3+ instances on the same page.
Why:
- ID conflicts
- Event handler interference
- localStorage key collisions
- Performance issues
- SVG rendering issues
Result: Discovered that multiple trees on one page work correctly with proper ID handling.
10. Documentation is Part of Implementation
Lesson: Comprehensive documentation is not optional—it's part of making the component usable.
Should Document:
- Format specification (YAML structure)
- Code block attributes
- Metadata fields and their purposes
- Real-world examples
- Best practices
- AI agent compatibility
Benefit: Helps future implementers and enables AI agents to understand the format.
11. Sentiment-Based Styling for Suitability Trees
Discovery: Not all decision trees are "selection trees" (choose between options). Some are "suitability trees" (determine if something is appropriate).
Problem: Selection trees and suitability trees have fundamentally different semantics:
- Selection trees: All outcomes are valid recommendations (e.g., "Use JSON" vs. "Use Hash" vs. "Use String")
- Suitability trees: Outcomes are binary (suitable vs. unsuitable) (e.g., "RDI is a good fit" vs. "RDI won't work")
Solution: Add optional sentiment field to outcomes:
outcome:
label: "✅ RDI is a good fit for your use case"
id: goodFit
sentiment: "positive" # Green styling
Implementation Details:
- Extract
sentimentfield during YAML parsing in JavaScript - Apply conditional styling in SVG rendering:
sentiment: "positive"→ Green background (#0fa869) and bordersentiment: "negative"→ Red background (#d9534f) and bordersentiment: "indeterminate"→ Yellow/amber background (#f0ad4e) and border- No sentiment → Red (default, maintains backward compatibility)
Key Insight: Explicit metadata is better than heuristics. Don't try to infer sentiment from emoji (✅/❌) or label text. Use explicit fields for reliability and AI agent compatibility.
Backward Compatibility: Existing trees without sentiment fields continue to work with default red styling. This allows gradual adoption.
12. Answer Order Respects YAML Structure
Discovery: The JavaScript had two issues preventing YAML answer order from being respected:
- The
flattenDecisionTree()function was hardcoded to process "yes" first, then "no" - The tree line drawing code was deriving Yes/No labels from position (first child = Yes, others = No) instead of using the actual answer value
Problem: This prevented authors from controlling the visual layout of the tree. If you wanted "no" outcomes to appear first (for early rejection patterns), the diagram would still show "yes" first.
Solution:
- Modified
flattenDecisionTree()to iterate through answer keys in the order they appear in the YAML - Modified
drawTreeLines()to use the actualanswervalue stored in each item instead of deriving it from position
// In flattenDecisionTree():
const answerKeys = Object.keys(question.answers);
answerKeys.forEach(answerKey => {
const answer = question.answers[answerKey];
// Process in order, storing answer.value in the item
});
// In drawTreeLines():
answerLabel = item.answer || 'Yes'; // Use stored value, not position
Benefit: Authors can now control tree layout by ordering answers in the YAML:
- Put
nofirst for early rejection patterns (negative outcomes appear left) - Put
yesfirst for positive-path-first patterns (positive outcomes appear left)
Key Insight: YAML object key order is preserved in JavaScript (since ES2015), and we now respect both the order AND the actual answer values, making the layout fully author-controlled.
13. Configurable Indent Width for Deeply Nested Trees
Problem: Deeply nested decision trees (with many levels of questions) can become too wide to fit on the page, requiring horizontal scrolling.
Solution: Added optional indentWidth parameter to the YAML root object that controls the horizontal spacing between parent and child nodes:
id: when-to-use-rdi
scope: rdi
indentWidth: 25 # Reduce from default 40 to make tree narrower
rootQuestion: cacheTarget
questions:
# ...
Implementation:
In renderDecisionTree(), the indent width is read from treeData.indentWidth with a sensible default:
const indentWidth = treeData.indentWidth ? parseInt(treeData.indentWidth) : 40;
Design Rationale: While indentWidth is a rendering preference, it's included in the YAML because:
- Hugo's Goldmark attribute parsing doesn't reliably expose custom attributes from the code block info string to the render hook
- Including it in the YAML keeps all tree configuration in one place
- AI agents can still access the semantic metadata (id, scope, questions) separately from rendering preferences
Benefit: Authors can now control tree width by adjusting indentWidth:
- Default (40): Comfortable spacing for shallow trees
- Reduced (20-30): Compact layout for deeply nested trees
- The SVG width is calculated as:
leftMargin + (maxDepth + 1) * indentWidth + maxBoxWidth + 40
Recommendation: For trees with 8+ levels of nesting, try indentWidth: 25 or lower to keep the diagram readable without horizontal scrolling.
14. Improved Label Visibility with Reduced Indent Width
Problem: When using reduced indentWidth values, the Yes/No labels on the connecting lines were being covered by the node boxes they referred to.
Solution:
- Increased the vertical offset of labels from
y + 10toy + 16pixels - Added a white background rectangle behind each label to ensure visibility even when overlapping with boxes
Implementation:
const labelY = y + 16; // Increased offset
// Add white background rectangle behind label
const labelBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
labelBg.setAttribute('x', labelX - 12);
labelBg.setAttribute('y', labelY - 9);
labelBg.setAttribute('width', '24');
labelBg.setAttribute('height', '12');
labelBg.setAttribute('fill', 'white');
svg.appendChild(labelBg);
Benefit: Labels remain readable regardless of indent width or tree density.