--- 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" - from: "CmsPageService.render" to: "PageRenderContext.components" via: "initializes components from page config before rendering" pattern: "PageRenderContext.*components.*=" --- 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 **CRITICAL: ComponentTag depends on PageRenderContext.components being pre-populated.** The components map is populated by CmsPageService.render (Task 3) BEFORE the template is rendered. ComponentTag only looks up by alias - it does NOT initialize components. 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 with COMPONENT INITIALIZATION: 1. **Initialize components from page config (CRITICAL - populates PageRenderContext.components):** - Parse component definitions from page settings (INI format) - For each `[componentClass alias]` section: a. Look up component class from ComponentRegistry (from Phase 3) b. Create ComponentInstance with properties from section c. Run component lifecycle (init, onRun) d. Store in components Map keyed by alias - This Map is passed to PageRenderContext BEFORE any template rendering 2. First pass - render page content: - Create PageRenderContext with: - `components`: the Map populated in step 1 - `placeholderContent`: empty mutable.Map - Render page.markup with Pebble - This collects {% put %} content into placeholderContent - {% component 'alias' %} tags can now resolve from pre-populated components Map 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 initializes components from page config into PageRenderContext.components Map, then 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 - CmsPageService.render initializes components from page config BEFORE template rendering - PageRenderContext.components is populated when ComponentTag executes - Two-pass rendering correctly resolves all template composition After completion, create `.planning/phases/09-content-management/09-02-SUMMARY.md`