spec(template): layout chaining via front matter layout: directive (#276)#277
Conversation
#276) Layout files can reference a parent layout via front matter (e.g., layout: "base" in has-toc.liquid). The pipeline renders inside-out: page → innermost layout → parent → root. Front matter is stripped before rendering (not output as literal text). Updates PLAN.md §4: adds layout chaining spec with max depth (10), circular detection via DetectCircularLayouts, front matter stripping. Updates IMPLEMENTATION.md: adds step 12a for layout chaining in the build orchestration, notes for cache invalidation tracking all levels. Exports ExtractLayoutParent, adds StripLayoutFrontMatter and ResolveLayoutChain stubs to layout.go so tests compile. 6 failing tests: - ExtractLayoutParent reads layout: from layout front matter - ExtractLayoutParent returns empty for root layouts - StripLayoutFrontMatter removes front matter from layout content - ResolveLayoutChain follows parent references (2-level chain) - Layout chain depth exceeding 10 levels returns error - Pipeline: renders content through chained layouts, strips front matter Spec work for #276 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
zeroedin
left a comment
There was a problem hiding this comment.
PR #277 Review — Layout Chaining via Front Matter layout: Directive (#276)
Scope: layout.go (+30/-4), layout_test.go (+96), build_test.go (+57), PLAN.md (+21/-2), IMPLEMENTATION.md (+11/-1). Spec, tests, and scaffolding for multi-level layout composition.
Feature
Layouts can reference a parent layout via front matter:
---
layout: "base"
---
<div class="toc">{{ content }}</div>Pipeline renders inside-out: content → child layout → parent layout → root layout. Each level injects {{ content }} from the level below.
Code Changes
ExtractLayoutParent — Improved from the original extractLayoutParent:
- Now properly parses YAML front matter delimiters (
---) - Only reads
layout:within front matter (not from template body) - Old code matched
layout:anywhere in the file, including inside HTML/Liquid
StripLayoutFrontMatter — Stub (return content). TDD — the test for front matter stripping will fail until implemented.
ResolveLayoutChain — Stub (return nil, fmt.Errorf("not implemented")). Takes a starting layout path and follows layout: directives to build an ordered chain. Capped at 10 levels.
extractLayoutParent wrapper — Unexported function calls the exported ExtractLayoutParent. Keeps backward compatibility for DetectCircularLayouts which calls the unexported version.
Tests (7)
layout_test.go (5 tests):
ExtractLayoutParentreadslayout:from front matter → returns"base"✓ExtractLayoutParentreturns empty for layout without front matter ✓StripLayoutFrontMatterremoves---andlayout:from content → will fail (stub)ResolveLayoutChainresolves 2-level chain → will fail (stub)- Chain exceeding 10 levels returns error → will fail (stub)
build_test.go (2 tests):
- Full pipeline renders through chain:
page → has-toc → base→ output has<html>,<div class="toc">, content, no---orlayout:in output - Layout front matter not rendered as literal text — the specific bug from #276
Spec Changes
PLAN.md: Replaces "layout composition is delegated to each engine" with explicit layout chaining spec. Documents: inside-out rendering, front matter stripping, circular detection, max depth 10. Separates partials/includes (engine-native) from layout chaining (Alloy-owned).
IMPLEMENTATION.md: Step 12a added between layout resolution and cache tracking. Documents the chaining loop, front matter stripping, DetectCircularLayouts placement in Phase 0, and multi-layout cache tracking.
Observations
A. extractLayoutParent wrapper is unnecessary
func extractLayoutParent(path string) string {
return ExtractLayoutParent(path)
}DetectCircularLayouts calls extractLayoutParent (unexported). Since the logic was moved to the exported version, the unexported wrapper is a pure passthrough. DetectCircularLayouts could call ExtractLayoutParent directly. Not a bug — just dead indirection.
B. Front matter parsing improvement is correct
The old extractLayoutParent matched layout: anywhere in the file — if a layout contained <p>layout: something</p> in its body, it would incorrectly detect a parent. The new version only matches within --- delimiters. Good fix.
C. Test 6 (pipeline) provides BuildWithContent with layouts
content := map[string]string{
"content/page.md": "...",
"layouts/has-toc.liquid": "---\nlayout: \"base\"\n---\n...",
"layouts/base.liquid": "<html>...",
}BuildWithContent must write layout files to the temp directory so ResolveLayout can find them. This works if BuildWithContent writes all contentMap entries (not just content/ prefixed ones). If it only writes content/ entries, the layout files won't be found. Worth verifying in the implementation.
D. Cache tracking for chained layouts
IMPLEMENTATION.md notes: "Track ALL layouts in the chain (not just the innermost)." This means if page.md → has-toc → base, changing base.liquid must invalidate page.md — not just pages that directly use base. The TrackTemplateUsage call must iterate the full chain. Good spec note.
Verdict
Well-structured spec and tests for layout chaining. The ExtractLayoutParent improvement (front matter parsing) is a bugfix itself. Two stubs (StripLayoutFrontMatter, ResolveLayoutChain) drive the implementation. The 10-level max depth test prevents infinite loops. The build_test verifies the specific bug from #276 (front matter appearing as literal text in output). No blocking issues.
There was a problem hiding this comment.
Pull request overview
This PR specifies and begins implementing Liquid layout chaining via a layout file’s front matter layout: directive (issue #276), aiming to render content inside-out through multiple layouts while stripping layout front matter from output.
Changes:
- Updates PLAN/IMPLEMENTATION docs to describe layout chaining, cycle detection, and max chain depth (10).
- Exports
ExtractLayoutParentand introducesStripLayoutFrontMatter/ResolveLayoutChain(currently stubs). - Adds unit/integration tests covering layout parent extraction, front matter stripping, chain depth errors, and pipeline chaining behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| plans/PLAN.md | Documents the intended layout chaining behavior and constraints. |
| plans/IMPLEMENTATION.md | Adds missing implementation notes for chaining, cycle detection, and cache tracking. |
| internal/template/layout.go | Exports parent extraction; adds (stubbed) front matter stripping and chain resolution APIs. |
| internal/template/layout_test.go | Adds tests for parent extraction, front matter stripping, and chain resolution/depth. |
| internal/pipeline/build_test.go | Adds pipeline-level tests asserting chained layout wrapping and front matter stripping. |
Comments suppressed due to low confidence (1)
internal/template/layout.go:196
DetectCircularLayoutsstores keys as relative filenames (e.g.base.liquid) butExtractLayoutParentreturns the rawlayout:value (often justbaseper the spec/tests). That meanscurrent = parentin cycle detection won’t match any key, so cycles likea -> b -> acan be missed unless the front matter includes the extension. Consider normalizing parent references (e.g., append the engine extension and/or canonicalize to a consistent relative path) so cycle detection and chain resolution agree on identifiers.
if inFrontMatter && strings.HasPrefix(line, "layout:") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
return strings.TrimSpace(strings.Trim(parts[1], `"' `))
}
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Update testdata a.liquid/b.liquid to use proper front matter delimiters so ExtractLayoutParent (which now requires --- delimiters) can detect the circular reference (Copilot comment on line 190) - Rename test "RenderLayoutChain" → "ResolveLayoutChain" to match what is actually tested (Copilot comment on line 339) - Remove unnecessary extractLayoutParent wrapper, call ExtractLayoutParent directly from DetectCircularLayouts (reviewer observation A) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Layouts can reference a parent layout via front matter
layout:directive (e.g.,layout: "base"inhas-toc.liquid). The pipeline renders inside-out: page content → innermost layout → parent layout → root layout. This enables multi-level composition likepage → has-toc → base.DetectCircularLayouts()during Phase 0ExtractLayoutParent, addsStripLayoutFrontMatterandResolveLayoutChainstubsTest plan
ExtractLayoutParentreadslayout:from layout front matterExtractLayoutParentreturns empty for root layouts (no parent)StripLayoutFrontMatterremoves front matter from layout contentResolveLayoutChainfollows parent references (2-level chain)Spec work for #276
🤖 Generated with Claude Code