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
This commit is contained in:
275
.planning/phases/09-content-management/09-02-PLAN.md
Normal file
275
.planning/phases/09-content-management/09-02-PLAN.md
Normal file
@@ -0,0 +1,275 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user