Files
summercms-initial-research/.planning/phases/09-content-management/09-02-PLAN.md
Jakub Zych dca89e10cd docs(09): create phase plan
Phase 09: Content Management
- 6 plan(s) in 3 wave(s)
- Wave 1: 09-01 (CMS pages/layouts), 09-03 (media library) - parallel
- Wave 2: 09-02 (component embedding), 09-04 (revisions), 09-05 (menus)
- Wave 3: 09-06 (hot reload)
- Ready for execution
2026-02-05 15:33:51 +01:00

9.4 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
09-content-management 02 execute 2
09-01
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
true
truths artifacts key_links
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
path provides contains
summercms/src/content/cms/pebble/ComponentTag.scala {% component %} tag implementation class ComponentTag extends TokenParser
path provides contains
summercms/src/content/cms/pebble/PlaceholderTag.scala {% placeholder %} tag with defaults class PlaceholderTag extends TokenParser
path provides contains
summercms/src/content/cms/pebble/CmsPebbleExtension.scala Extension registering all CMS tags class CmsPebbleExtension extends Extension
from to via pattern
ComponentTag PageRenderContext.components resolves component by alias from page context pageContext.components.get
from to via pattern
PlaceholderTag PageRenderContext.placeholderContent checks for put content before rendering default 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.

<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>

@.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.
  </action>
  <verify>
`./mill summercms.compile` succeeds
End-to-end test: Create sample page with component, partial, and placeholder
Render should produce composed HTML with all elements
  </verify>
  <done>
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.
  </done>
</task>

</tasks>

<verification>
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: ``` <html> {% placeholder 'sidebar' %} {% endplaceholder %}
{% page %}
</html> ``` 4. Render produces correct composed HTML with custom sidebar

<success_criteria>

  • {% 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 </success_criteria>
After completion, create `.planning/phases/09-content-management/09-02-SUMMARY.md`