Skip to content

spec(template): layout chaining via front matter layout: directive (#276)#277

Merged
zeroedin merged 2 commits into
mainfrom
spec/issue-276-layout-chaining
Apr 25, 2026
Merged

spec(template): layout chaining via front matter layout: directive (#276)#277
zeroedin merged 2 commits into
mainfrom
spec/issue-276-layout-chaining

Conversation

@zeroedin

Copy link
Copy Markdown
Owner

Summary

Layouts can reference a parent layout via front matter layout: directive (e.g., layout: "base" in has-toc.liquid). The pipeline renders inside-out: page content → innermost layout → parent layout → root layout. This enables multi-level composition like page → has-toc → base.

  • Front matter in layout files is stripped before rendering (not output as literal text)
  • Circular references detected by DetectCircularLayouts() during Phase 0
  • Max chain depth: 10 levels (prevents infinite loops)
  • Exports ExtractLayoutParent, adds StripLayoutFrontMatter and ResolveLayoutChain stubs
  • Updates PLAN.md and IMPLEMENTATION.md with chaining spec

Test plan

  • ExtractLayoutParent reads layout: from layout front matter
  • ExtractLayoutParent returns empty for root layouts (no parent)
  • StripLayoutFrontMatter removes front matter from layout content
  • ResolveLayoutChain follows parent references (2-level chain)
  • Chain depth exceeding 10 levels returns error
  • Pipeline: content rendered through chained layouts, front matter stripped
  • Pipeline: layout front matter not output as literal text (the Layout chaining: layout front matter in layout files not parsed #276 bug)

Spec work for #276

🤖 Generated with Claude Code

#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 zeroedin left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

  1. ExtractLayoutParent reads layout: from front matter → returns "base"
  2. ExtractLayoutParent returns empty for layout without front matter ✓
  3. StripLayoutFrontMatter removes --- and layout: from content → will fail (stub)
  4. ResolveLayoutChain resolves 2-level chain → will fail (stub)
  5. Chain exceeding 10 levels returns error → will fail (stub)

build_test.go (2 tests):

  1. Full pipeline renders through chain: page → has-toc → base → output has <html>, <div class="toc">, content, no --- or layout: in output
  2. 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ExtractLayoutParent and introduces StripLayoutFrontMatter / 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

  • DetectCircularLayouts stores keys as relative filenames (e.g. base.liquid) but ExtractLayoutParent returns the raw layout: value (often just base per the spec/tests). That means current = parent in cycle detection won’t match any key, so cycles like a -> b -> a can 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.

Comment thread internal/template/layout.go
Comment thread internal/pipeline/build_test.go
Comment thread internal/template/layout_test.go Outdated
Comment thread internal/template/layout.go
Comment thread internal/template/layout.go
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants