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
276 lines
9.4 KiB
Markdown
276 lines
9.4 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create PageRenderContext and placeholder/put tags</name>
|
|
<files>
|
|
summercms/src/content/cms/PageRenderContext.scala
|
|
summercms/src/content/cms/pebble/PlaceholderTag.scala
|
|
summercms/src/content/cms/pebble/PutTag.scala
|
|
</files>
|
|
<action>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
`./mill summercms.compile` succeeds with new Pebble tags
|
|
</verify>
|
|
<done>
|
|
PlaceholderTag and PutTag allow layouts to define placeholders with defaults that pages can override. PageRenderContext stores render state.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Implement component and partial tags</name>
|
|
<files>
|
|
summercms/src/content/cms/pebble/ComponentTag.scala
|
|
summercms/src/content/cms/pebble/PartialTag.scala
|
|
</files>
|
|
<action>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
`./mill summercms.compile` succeeds
|
|
ComponentTag parses component alias and properties
|
|
PartialTag resolves partial files correctly
|
|
</verify>
|
|
<done>
|
|
ComponentTag embeds components by alias with property overrides. PartialTag includes partial templates with variable passing. Both integrate with Pebble rendering.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Create CmsPebbleExtension and update render pipeline</name>
|
|
<files>
|
|
summercms/src/content/cms/pebble/CmsPebbleExtension.scala
|
|
summercms/src/content/cms/CmsPageService.scala
|
|
</files>
|
|
<action>
|
|
**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' %}
|
|
<aside>Custom sidebar</aside>
|
|
{% endput %}
|
|
|
|
<main>
|
|
{% component 'demo' extraProp='override' %}
|
|
{% partial 'shared-block' title='Hello' %}
|
|
</main>
|
|
```
|
|
3. Create test layout:
|
|
```
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body>
|
|
{% placeholder 'sidebar' %}
|
|
<aside>Default sidebar</aside>
|
|
{% endplaceholder %}
|
|
<div class="content">
|
|
{% page %}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
```
|
|
4. Render produces correct composed HTML with custom sidebar
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/09-content-management/09-02-SUMMARY.md`
|
|
</output>
|