---
phase: 09-content-management
plan: 02
type: execute
wave: 2
depends_on: ["09-01"]
files_modified:
- summercms/src/content/cms/pebble/ComponentTag.scala
- summercms/src/content/cms/pebble/PlaceholderTag.scala
- summercms/src/content/cms/pebble/PartialTag.scala
- summercms/src/content/cms/pebble/PutTag.scala
- summercms/src/content/cms/pebble/CmsPebbleExtension.scala
- summercms/src/content/cms/PageRenderContext.scala
- summercms/src/content/cms/CmsPageService.scala
autonomous: true
must_haves:
truths:
- "Components can be embedded in CMS pages via {% component 'alias' %} syntax"
- "Components receive properties from page config and inline overrides"
- "Layouts support {% placeholder 'name' %}default{% endplaceholder %} blocks"
- "Pages can fill placeholders with {% put 'name' %}content{% endput %}"
- "Partials render via {% partial 'name' %} with variable passing"
artifacts:
- path: "summercms/src/content/cms/pebble/ComponentTag.scala"
provides: "{% component %} tag implementation"
contains: "class ComponentTag extends TokenParser"
- path: "summercms/src/content/cms/pebble/PlaceholderTag.scala"
provides: "{% placeholder %} tag with defaults"
contains: "class PlaceholderTag extends TokenParser"
- path: "summercms/src/content/cms/pebble/CmsPebbleExtension.scala"
provides: "Extension registering all CMS tags"
contains: "class CmsPebbleExtension extends Extension"
key_links:
- from: "ComponentTag"
to: "PageRenderContext.components"
via: "resolves component by alias from page context"
pattern: "pageContext\\.components\\.get"
- from: "PlaceholderTag"
to: "PageRenderContext.placeholderContent"
via: "checks for put content before rendering default"
pattern: "placeholderContent\\.get"
---
Implement component embedding and placeholder system for CMS pages
Purpose: Enable pages to embed dynamic components via Twig-like syntax and use layouts with placeholders that pages can fill. This completes the template composition system per CONTEXT.md decisions.
Output: Custom Pebble tags (component, placeholder, put, partial), CmsPebbleExtension registering all tags, updated CmsPageService render pipeline.
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/09-content-management/09-RESEARCH.md
@.planning/phases/09-content-management/09-01-SUMMARY.md
@.planning/phases/03-component-system/03-RESEARCH.md (Component system patterns)
@summercms/src/content/cms/CmsPage.scala
@summercms/src/content/cms/CmsPageService.scala
Task 1: Create PageRenderContext and placeholder/put tags
summercms/src/content/cms/PageRenderContext.scala
summercms/src/content/cms/pebble/PlaceholderTag.scala
summercms/src/content/cms/pebble/PutTag.scala
**PageRenderContext.scala:**
Create render context passed through Pebble evaluation:
```scala
case class PageRenderContext(
page: CmsPage,
components: Map[String, ComponentInstance], // alias -> initialized component
placeholderContent: mutable.Map[String, String], // name -> rendered content
runtime: Runtime[Any] // for ZIO effect execution in sync context
)
```
**PlaceholderTag.scala:**
Implement Pebble TokenParser for `{% placeholder 'name' %}default{% endplaceholder %}`:
- Parse placeholder name expression
- Parse body until `endplaceholder`
- Create PlaceholderNode that:
- Evaluates name from context
- Checks PageRenderContext.placeholderContent for that name
- If content exists, write it
- Otherwise, render default body
Per CONTEXT.md: Flat layouts only, placeholders with defaults.
**PutTag.scala:**
Implement `{% put 'name' %}content{% endput %}`:
- Parse placeholder name expression
- Parse body until `endput`
- Create PutNode that:
- Renders body to string
- Stores in PageRenderContext.placeholderContent[name]
- Writes nothing to output (content stored for later use)
Two-pass rendering needed: First pass collects `put` content, second pass renders layout with placeholders resolved.
`./mill summercms.compile` succeeds with new Pebble tags
PlaceholderTag and PutTag allow layouts to define placeholders with defaults that pages can override. PageRenderContext stores render state.
Task 2: Implement component and partial tags
summercms/src/content/cms/pebble/ComponentTag.scala
summercms/src/content/cms/pebble/PartialTag.scala
**ComponentTag.scala:**
Implement `{% component 'alias' prop1='value' %}`:
- Parse required alias expression (string)
- Parse optional key=value property pairs until EXECUTE_END
- Create ComponentNode that:
- Gets PageRenderContext from evaluation context
- Looks up component by alias in pageContext.components
- If not found, throw PebbleException with line number
- Override component properties with tag-provided values
- Call component.render() using pageContext.runtime.unsafeRun
- Write rendered HTML to output
Component lookup: Page's INI section defines components like:
```
[blogPosts posts]
postsPerPage = 5
```
Where `blogPosts` is component class, `posts` is alias, properties follow.
**PartialTag.scala:**
Implement `{% partial 'name' var1='value' %}`:
- Parse partial name expression
- Parse optional variable assignments
- Create PartialNode that:
- Resolves partial from theme (partials/{name}.htm)
- Or from plugin namespace (plugin::partialName)
- Renders partial template with provided variables merged into context
- Write output
Per CONTEXT.md: `plugin::partial` for plugin partials, plain name for theme partials.
`./mill summercms.compile` succeeds
ComponentTag parses component alias and properties
PartialTag resolves partial files correctly
ComponentTag embeds components by alias with property overrides. PartialTag includes partial templates with variable passing. Both integrate with Pebble rendering.
Task 3: Create CmsPebbleExtension and update render pipeline
summercms/src/content/cms/pebble/CmsPebbleExtension.scala
summercms/src/content/cms/CmsPageService.scala
**CmsPebbleExtension.scala:**
Create Pebble Extension registering all CMS tags:
```scala
class CmsPebbleExtension extends Extension:
override def getTokenParsers: java.util.List[TokenParser] =
java.util.Arrays.asList(
new ComponentTag(),
new PlaceholderTag(),
new PutTag(),
new PartialTag()
)
override def getGlobalVariables: java.util.Map[String, Object] = null
override def getFilters: java.util.Map[String, Filter] = null
// ... other Extension methods return null/empty
```
**Update CmsPageService.scala render method:**
Implement two-pass rendering:
1. Initialize components from page config:
- Parse component definitions from page settings
- For each `[componentClass alias]` section, create ComponentInstance
- Run component lifecycle (init, onRun)
2. First pass - render page content:
- Create PageRenderContext with mutable.Map for placeholderContent
- Render page.markup with Pebble
- This collects {% put %} content into placeholderContent
3. Second pass - render layout:
- Get layout from page.config.layout
- Add pageContent variable with rendered page HTML
- Render layout.markup
- {% placeholder %} tags now have access to collected content
- {% page %} tag outputs pageContent variable
Add `{% page %}` as simple Pebble output: `{{ pageContent }}` or custom PageTag.
Note: Must configure PebbleEngine with CmsPebbleExtension when building the engine layer.
`./mill summercms.compile` succeeds
End-to-end test: Create sample page with component, partial, and placeholder
Render should produce composed HTML with all elements
CmsPebbleExtension registers all CMS tags with Pebble. CmsPageService.render uses two-pass rendering: first to collect put content, then to render layout with placeholders resolved.
After all tasks complete:
1. `./mill summercms.compile` - all code compiles
2. Create test page with all features:
```
url = "/test"
layout = "default"
[myComponent demo]
property = "value"
==
{% put 'sidebar' %}
{% endput %}
{% component 'demo' extraProp='override' %}
{% partial 'shared-block' title='Hello' %}
```
3. Create test layout:
```
{% placeholder 'sidebar' %}
{% endplaceholder %}
{% page %}
```
4. Render produces correct composed HTML with custom sidebar
- {% component 'alias' %} embeds components with property passing
- {% placeholder 'name' %}default{% endplaceholder %} in layouts with page-fillable content
- {% put 'name' %}content{% endput %} in pages fills layout placeholders
- {% partial 'name' %} includes partials with variables
- Two-pass rendering correctly resolves all template composition