# Phase 3: Component System - Research **Researched:** 2026-02-05 **Domain:** Reusable UI components with YAML-defined properties, lifecycle hooks, HTMX handlers, and Twig-compatible templating **Confidence:** HIGH ## Summary This phase implements the component system that enables reusable UI components responding to HTMX interactions. Components are defined with YAML property schemas (similar to WinterCMS Inspector configuration), registered per-plugin, and render via Twig-compatible templates. HTMX handlers (methods prefixed with `on`) respond to client-side triggers and return HTML fragments for swapping. The recommended stack uses: ZIO HTTP 3.3.0+ with the official `zio-http-htmx` module for HTMX response headers, **Pebble 4.1.1** for Twig/Jinja2-compatible templating (chosen over ScalaTags per CONTEXT.md decision for Twig-compatible templates), circe-yaml for YAML property schema parsing (reusing Phase 2 infrastructure), and ZIO for component lifecycle management. The htmx4s library provides type-safe HTMX constants and ScalaTags integration for any programmatic HTML needs. **Primary recommendation:** Define components as Scala traits with YAML property schemas parsed at plugin registration time. Use Pebble template engine for Twig-compatible rendering with `default.htm` convention. Implement HTMX handlers as `on*` methods returning `ZIO[ComponentEnv, ComponentError, Html]`. Integrate CSRF protection via request middleware injecting tokens, with validation on all HTMX POST requests. ## Standard Stack The established libraries/tools for this domain: ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | zio-http-htmx | 3.3.0 | HTMX response headers | Official ZIO HTTP module for HTMX integration | | Pebble | 4.1.1 | Twig-compatible templates | Java templating with Jinja2/Twig syntax, i18n, autoescaping | | circe-yaml | 0.16.1 | YAML property parsing | Already in stack from Phase 2, proven for YAML parsing | | htmx4s-constants | latest | HTMX vocabulary | Type-safe HTMX attribute/header constants | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | htmx4s-scalatags | latest | ScalaTags HTMX attrs | When generating HTML programmatically (not templates) | | ScalaTags | 0.13.x | Programmatic HTML | Component helper methods, fragment generation | | zio-http | 3.3.0+ | HTTP server + SSE | Base HTTP server, Server-Sent Events streaming | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | Pebble | ScalaTags only | ScalaTags is type-safe but not Twig-compatible; Pebble enables WinterCMS migration | | Pebble | Twirl | Twirl has Scala syntax, not Jinja2/Twig-compatible | | Pebble | Jinjava | Jinjava is also Jinja2-compatible; Pebble has better Twig inheritance | | htmx4s | Hand-roll constants | htmx4s provides IDE docs from htmx markdown, type safety | **Installation (Mill build.mill):** ```scala def mvnDeps = Seq( // Existing deps from Phase 1/2... // HTMX integration mvn"dev.zio::zio-http-htmx:3.3.0", // Twig-compatible templating mvn"io.pebbletemplates:pebble:4.1.1", // HTMX constants (optional, for type-safe attributes) mvn"com.github.eikek::htmx4s-constants:latest", // ScalaTags for programmatic HTML fragments mvn"com.lihaoyi::scalatags:0.13.1" ) ``` ## Architecture Patterns ### Recommended Component Directory Structure ``` plugins/ ├── golem15/ │ └── blog/ │ ├── plugin.yaml │ ├── Plugin.scala # registerComponents method │ └── components/ │ ├── Posts.scala # Component class │ └── posts/ │ ├── component.yaml # Property schema │ ├── default.htm # Main template (required) │ ├── pagination.htm # Partial template │ └── item.htm # Item template ``` ### Pattern 1: Component Trait with Lifecycle Hooks **What:** Define components as Scala traits with `init`, `onRun`, and HTMX handler methods **When to use:** All components must implement this trait **Example:** ```scala // Source: WinterCMS ComponentBase pattern adapted to ZIO trait SummerComponent: /** Component identifier (alias used in pages) */ def alias: String /** Component metadata from YAML */ def details: ComponentDetails /** Property schema from component.yaml */ def propertySchema: List[PropertyDef] /** Current property values (set from page/layout) */ def properties: Ref[Map[String, PropertyValue]] /** Access to page context for rendering */ def pageContext: PageContext /** Called once when component is initialized (before HTMX handlers) */ def init: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit /** Called when component is bound to page/layout, part of page lifecycle */ def onRun: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit /** Render the default template with current context */ def render: ZIO[ComponentEnv, ComponentError, Html] /** Render a specific partial template */ def renderPartial(name: String): ZIO[ComponentEnv, ComponentError, Html] case class ComponentDetails( name: String, description: String, icon: Option[String] = None ) ``` ### Pattern 2: YAML Property Schema (WinterCMS Inspector Compatible) **What:** Define component properties in YAML with explicit types and validation **When to use:** Every component with configurable properties **Example:** ```yaml # plugins/golem15/blog/components/posts/component.yaml # Source: WinterCMS Inspector configuration format name: Blog Posts description: Displays a list of blog posts with pagination icon: pencil properties: postsPerPage: type: string title: Posts per page description: Number of posts to display per page default: "10" validation: integer: message: Posts per page must be a positive integer min: value: 1 message: Must show at least 1 post max: value: 100 message: Maximum 100 posts per page categoryFilter: type: dropdown title: Category Filter description: Filter posts by category default: "" showExternalParam: true # Allow {{ :category }} binding # Options loaded dynamically from getCategoryFilterOptions() sortOrder: type: dropdown title: Sort Order options: published_at desc: Published date (newest first) published_at asc: Published date (oldest first) title asc: Title (A-Z) default: published_at desc noPostsMessage: type: string title: No posts message default: No posts found. group: Messages throwNotFound: type: checkbox title: Throw 404 on empty description: Show 404 page when no posts match filter default: false group: Advanced ``` ```scala // Source: circe-yaml parsing + WinterCMS property types import io.circe.generic.auto.* import io.circe.yaml.parser case class ComponentSchema( name: String, description: String, icon: Option[String], properties: Map[String, PropertyDef] ) case class PropertyDef( `type`: PropertyType, title: String, description: Option[String] = None, default: Option[PropertyValue] = None, options: Option[Map[String, String]] = None, // For dropdown/set group: Option[String] = None, showExternalParam: Boolean = false, validation: Option[ValidationRules] = None ) enum PropertyType: case String, Text, Checkbox, Dropdown, Set, Dictionary case Object, ObjectList, StringList, Autocomplete sealed trait PropertyValue object PropertyValue: case class StringVal(value: String) extends PropertyValue case class BoolVal(value: Boolean) extends PropertyValue case class IntVal(value: Int) extends PropertyValue case class ListVal(values: List[PropertyValue]) extends PropertyValue case class ObjectVal(fields: Map[String, PropertyValue]) extends PropertyValue ``` ### Pattern 3: HTMX Handler Convention (on* Methods) **What:** Methods starting with `on` become HTMX handlers, routed via component alias **When to use:** All interactive component behaviors **Example:** ```scala // Source: WinterCMS AJAX handler pattern + CONTEXT.md decisions trait BlogPostsComponent extends SummerComponent: // HTMX handler: POST /summer/component/blogPosts/onLoadMore def onLoadMore: ZIO[ComponentEnv, ComponentError, HtmxResponse] = for page <- getFormValue("page").map(_.toIntOption.getOrElse(1)) posts <- loadPosts(page) html <- renderPartial("items", Map("posts" -> posts)) hasMore <- ZIO.succeed(posts.nonEmpty) yield HtmxResponse( html = html, triggers = if hasMore then Map.empty else Map("postsExhausted" -> "true"), retarget = None // Use original hx-target ) // HTMX handler: POST /summer/component/blogPosts/onFilter def onFilter: ZIO[ComponentEnv, ComponentError, HtmxResponse] = for category <- getFormValue("category") posts <- loadPosts(1, category) html <- renderPartial("default") yield HtmxResponse(html) case class HtmxResponse( html: Html, triggers: Map[String, String] = Map.empty, // HX-Trigger header retarget: Option[String] = None, // HX-Retarget header reswap: Option[String] = None, // HX-Reswap header oobSwaps: List[OobSwap] = List.empty // Out-of-band swaps ) case class OobSwap( targetId: String, html: Html, swapStrategy: String = "outerHTML" ) ``` ### Pattern 4: Component Registration in Plugin **What:** Plugins register components via `registerComponents` method returning alias -> class mapping **When to use:** Plugin boot phase, collected by ComponentManager **Example:** ```scala // Source: WinterCMS Plugin.registerComponents pattern class BlogPlugin extends SummerPlugin: override def id = PluginId("golem15", "blog") override def registerComponents: Map[String, ComponentFactory] = Map( "blogPost" -> ComponentFactory[BlogPostComponent], "blogPosts" -> ComponentFactory[BlogPostsComponent], "blogCategories" -> ComponentFactory[BlogCategoriesComponent], "blogRssFeed" -> ComponentFactory[BlogRssFeedComponent] ) trait ComponentFactory[C <: SummerComponent]: def create(alias: String, properties: Map[String, PropertyValue], ctx: PageContext): ZIO[Any, Nothing, C] // ComponentManager collects from all plugins trait ComponentManager: def listComponents: UIO[Map[String, ComponentFactory[?]]] def resolve(aliasOrClass: String): UIO[Option[ComponentFactory[?]]] def make[C <: SummerComponent]( alias: String, properties: Map[String, PropertyValue], ctx: PageContext ): IO[ComponentError, C] ``` ### Pattern 5: Pebble Template Integration **What:** Use Pebble engine for Twig-compatible templates with component context **When to use:** All component template rendering **Example:** ```java // Source: Pebble Templates documentation // Pebble template syntax (Twig-compatible) // plugins/golem15/blog/components/posts/default.htm {% extends "layout" %} {% block content %}
{% if posts is empty %}

{{ noPostsMessage }}

{% else %} {% for post in posts %} {% include "posts/item" %} {% endfor %} {% if hasMorePosts %} {% endif %} {% endif %}
{% endblock %} ``` ```scala // Source: Pebble Java API wrapped for ZIO import io.pebbletemplates.pebble.PebbleEngine import io.pebbletemplates.pebble.loader.FileLoader trait TemplateService: def render(template: String, context: Map[String, Any]): Task[Html] def renderString(templateContent: String, context: Map[String, Any]): Task[Html] object TemplateService: val live: ZLayer[PluginManager, Nothing, TemplateService] = ZLayer.fromFunction { (plugins: PluginManager) => new TemplateService: private val engine = new PebbleEngine.Builder() .loader(new ComponentTemplateLoader(plugins)) .autoEscaping(true) .defaultLocale(Locale.ENGLISH) .build() def render(template: String, context: Map[String, Any]): Task[Html] = ZIO.attemptBlocking { val writer = new StringWriter() val ctx = context.asJava engine.getTemplate(template).evaluate(writer, ctx) Html(writer.toString) } } // Custom Twig functions/filters // {{ asset('img/logo.png') }} -> ThemeService.asset() // {{ componentHandler('onRefresh') }} -> component URL builder // {{ 'text'|_ }} -> Translation filter ``` ### Pattern 6: CSRF Protection for HTMX Requests **What:** Inject CSRF token via middleware, validate on all HTMX POST/PUT/DELETE **When to use:** All HTMX handlers (per CONTEXT.md: CSRF always required) **Example:** ```scala // Source: HTMX CSRF pattern + OWASP recommendations // Middleware injects CSRF token into response headers and validates requests val csrfMiddleware: HandlerAspect[Any, Unit] = HandlerAspect.interceptHandler { handler => Handler.fromFunctionZIO[Request] { request => for session <- SessionService.get token <- session.csrfToken _ <- ZIO.when(request.method.isUnsafe) { val headerToken = request.header("X-CSRF-Token").map(_.value) val formToken = request.bodyAsMultipartForm.flatMap(_.get("_token")) val requestToken = headerToken.orElse(formToken) ZIO.when(requestToken != Some(token)) { ZIO.fail(CsrfViolation("Invalid CSRF token")) } } response <- handler(request) yield response.addHeader("X-CSRF-Token", token) } } // Template helper for forms // // HTMX global header injection (in base layout) // ``` ### Pattern 7: Server-Sent Events for Real-Time Updates **What:** Components can push updates to clients via SSE stream **When to use:** Live updates (chat, notifications, progress bars) **Example:** ```scala // Source: ZIO HTTP SSE documentation // Component declares SSE capability trait LiveNotifications extends SummerComponent: def onSubscribe: ZIO[ComponentEnv, ComponentError, Response] = for userId <- getCurrentUserId stream <- NotificationService.subscribeUser(userId) sse <- ZIO.succeed( stream.map { notification => ServerSentEvent( data = renderNotification(notification), event = Some("notification"), id = Some(notification.id.toString) ) } ) yield Response( status = Status.Ok, headers = Headers( Header.ContentType(MediaType.text.`event-stream`), Header.CacheControl.NoCache ), body = Body.fromStreamSSE(sse) ) // Template usage //
//
//
``` ### Pattern 8: Out-of-Band Swaps for Multiple Updates **What:** Single HTMX response updates multiple DOM elements **When to use:** Handler needs to update content outside its target **Example:** ```scala // Source: htmx hx-swap-oob documentation def onAddToCart: ZIO[ComponentEnv, ComponentError, HtmxResponse] = for productId <- getFormValue("product_id") _ <- CartService.addItem(productId) cart <- CartService.getCart itemHtml <- renderPartial("cart-item", Map("item" -> cart.lastItem)) countHtml <- renderFragment(s"""${cart.itemCount}""") yield HtmxResponse( html = itemHtml, oobSwaps = List( OobSwap("cart-count", countHtml), OobSwap("cart-total", renderCartTotal(cart)) ) ) // Response includes primary HTML plus OOB fragments: //
... item HTML ...
// 5 // $99.00 ``` ### Anti-Patterns to Avoid - **Blocking in handlers:** Always return `ZIO`, never block threads - **Direct database access in components:** Use services, not repositories directly - **Mutable component state:** Use `Ref` for any state, properties are immutable per request - **Missing CSRF on unsafe methods:** All POST/PUT/DELETE require validation - **String concatenation for HTML:** Use templates or ScalaTags, never raw strings - **Synchronous template rendering:** Wrap Pebble in `ZIO.attemptBlocking` - **Hard-coded component aliases:** Use `__SELF__` macro in templates for uniqueness ## Don't Hand-Roll Problems that look simple but have existing solutions: | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | Twig-compatible templates | Custom parser | Pebble | Complex inheritance, filters, autoescaping | | HTMX response headers | Manual string headers | zio-http-htmx | Type-safe headers, correct formatting | | HTMX attribute constants | String literals | htmx4s-constants | IDE docs, typo prevention, refactoring | | HTML escaping | Manual escaping | Pebble autoescaping | XSS vulnerabilities, edge cases | | CSRF token generation | Random strings | Secure random + HMAC | Timing attacks, entropy issues | | Property validation | Custom validators | Use WinterCMS patterns | Complex rules (regex, ranges, dependencies) | | Component discovery | Manual registration | Reflective scan or explicit registration | Miss components, ordering issues | **Key insight:** Component systems appear simple but involve complex concerns: property validation, template inheritance, security (XSS, CSRF), lifecycle management, and state isolation. Use proven patterns from WinterCMS adapted to ZIO. ## Common Pitfalls ### Pitfall 1: Template Escaping Disabled Accidentally **What goes wrong:** XSS vulnerabilities when rendering user content **Why it happens:** Using `|raw` filter or disabling autoescaping for convenience **How to avoid:** Keep autoescaping enabled, use `|raw` only for known-safe HTML fragments **Warning signs:** HTML entities appearing in output, or HTML rendering unexpectedly ### Pitfall 2: Component State Leaking Between Requests **What goes wrong:** User A sees User B's data **Why it happens:** Storing state in class variables instead of request-scoped Ref **How to avoid:** All component state via `Ref` created per-request, never `var` **Warning signs:** Inconsistent behavior under load, data appearing randomly ### Pitfall 3: HTMX Handler Not Found (404) **What goes wrong:** HTMX POST returns 404, component method exists **Why it happens:** Handler method doesn't start with `on`, or component not registered **How to avoid:** Naming convention enforcement, handler discovery at boot time **Warning signs:** Console shows POST to correct URL, but 404 returned ### Pitfall 4: CSRF Token Mismatch After Cache **What goes wrong:** Form submissions fail with CSRF error after page was cached **Why it happens:** CDN/browser cached page with old CSRF token **How to avoid:** Use `Vary: Cookie` header, or refresh token via HTMX header on each response **Warning signs:** Works in development, fails in production with caching ### Pitfall 5: SSE Connection Exhaustion **What goes wrong:** Server runs out of connections, new SSE subscriptions fail **Why it happens:** Clients not closing SSE connections, no timeout **How to avoid:** Set connection timeout, use heartbeat to detect dead connections **Warning signs:** Gradual slowdown, connection refused errors ### Pitfall 6: Template Path Traversal **What goes wrong:** Attacker reads arbitrary files via template include **Why it happens:** User input in template path without validation **How to avoid:** Whitelist allowed templates, never use user input directly in paths **Warning signs:** Error messages revealing file paths, unexpected template content ### Pitfall 7: Blocking Pebble Rendering on ZIO Runtime **What goes wrong:** Thread pool exhaustion, timeout errors under load **Why it happens:** Pebble is synchronous, blocking ZIO fibers **How to avoid:** Wrap Pebble calls in `ZIO.attemptBlocking`, use blocking thread pool **Warning signs:** High latency spikes, fiber count explosion ## Code Examples Verified patterns from official sources: ### Complete Component Implementation ```scala // Source: WinterCMS Account component pattern adapted to Scala/ZIO package golem15.blog.components import zio.* import io.circe.yaml.parser import io.pebbletemplates.pebble.PebbleEngine class BlogPostsComponent( val alias: String, val propertySchema: List[PropertyDef], val properties: Ref[Map[String, PropertyValue]], val pageContext: PageContext, templateService: TemplateService, postService: PostService ) extends SummerComponent: override val details = ComponentDetails( name = "Blog Posts", description = "Displays a list of blog posts", icon = Some("pencil") ) // Page variables set in onRun, available in template private var posts: List[Post] = List.empty private var category: Option[Category] = None private var pageParam: String = "page" override def onRun: ZIO[ComponentEnv, ComponentError, Unit] = for props <- properties.get catSlug = props.get("categoryFilter").collect { case StringVal(s) => s } cat <- catSlug.fold(ZIO.none)(slug => categoryService.findBySlug(slug).map(Some(_)) ) perPage = props.get("postsPerPage") .collect { case StringVal(s) => s.toIntOption } .flatten.getOrElse(10) page = pageContext.param("page").flatMap(_.toIntOption).getOrElse(1) result <- postService.listFrontend( page = page, perPage = perPage, category = cat.map(_.id), published = !pageContext.isBackendEditor ) yield this.posts = result.items this.category = cat this.pageParam = paramName("pageNumber").getOrElse("page") override def render: ZIO[ComponentEnv, ComponentError, Html] = for props <- properties.get noPostsMsg = props.get("noPostsMessage") .collect { case StringVal(s) => s } .getOrElse("No posts found.") html <- templateService.render( s"$alias/default", Map( "__SELF__" -> alias, "posts" -> posts.asJava, "category" -> category.orNull, "noPostsMessage" -> noPostsMsg, "pageParam" -> pageParam ) ) yield html // HTMX Handler: Load more posts def onLoadMore: ZIO[ComponentEnv, ComponentError, HtmxResponse] = for page <- ZIO.serviceWithZIO[Request](_.formField("page")) .map(_.flatMap(_.toIntOption).getOrElse(1)) props <- properties.get perPage = props.get("postsPerPage") .collect { case StringVal(s) => s.toIntOption } .flatten.getOrElse(10) result <- postService.listFrontend(page = page, perPage = perPage) html <- templateService.render(s"$alias/items", Map("posts" -> result.items.asJava)) yield HtmxResponse( html = html, triggers = if result.hasMore then Map.empty else Map("postsExhausted" -> "true") ) // Dynamic options for categoryFilter dropdown def getCategoryFilterOptions: UIO[Map[String, String]] = categoryService.listAll.map { cats => Map("" -> "- All Categories -") ++ cats.map(c => c.slug -> c.name).toMap } ``` ### HTMX Handler Router ```scala // Source: ZIO HTTP routing + WinterCMS component handler pattern object ComponentRoutes: val routes: Routes[ComponentManager & TemplateService & SessionService, Response] = Routes( // Component HTMX handler endpoint Method.POST / "summer" / "component" / string("alias") / string("handler") -> handler { (alias: String, handlerName: String, req: Request) => for _ <- CsrfService.validate(req) manager <- ZIO.service[ComponentManager] factory <- manager.resolve(alias).someOrFail(ComponentNotFound(alias)) ctx <- PageContext.fromRequest(req) props <- extractProperties(req) component <- factory.create(alias, props, ctx) result <- invokeHandler(component, handlerName) response <- buildHtmxResponse(result) yield response } ) private def invokeHandler( component: SummerComponent, handlerName: String ): IO[ComponentError, HtmxResponse] = // Validate handler name starts with "on" if !handlerName.startsWith("on") then ZIO.fail(InvalidHandler(s"Handler must start with 'on': $handlerName")) else // Use reflection or macro to invoke handler method component.invokeHandler(handlerName) ``` ### YAML Property Schema Parser ```scala // Source: circe-yaml + WinterCMS Inspector format import io.circe.* import io.circe.generic.auto.* import io.circe.yaml.parser case class ComponentYaml( name: String, description: String, icon: Option[String] = None, properties: Map[String, PropertyDefYaml] = Map.empty ) case class PropertyDefYaml( `type`: String, title: String, description: Option[String] = None, default: Option[Json] = None, options: Option[Map[String, String]] = None, group: Option[String] = None, showExternalParam: Option[Boolean] = None, validation: Option[ValidationYaml] = None ) def parseComponentYaml(path: Path): IO[YamlError, ComponentSchema] = for content <- ZIO.attemptBlocking(Files.readString(path)) .mapError(e => YamlError.ReadError(path, e)) yaml <- ZIO.fromEither(parser.parse(content)) .mapError(e => YamlError.ParseError(path, e)) schema <- ZIO.fromEither(yaml.as[ComponentYaml]) .mapError(e => YamlError.DecodeError(path, e)) yield toComponentSchema(schema) def toComponentSchema(yaml: ComponentYaml): ComponentSchema = ComponentSchema( name = yaml.name, description = yaml.description, icon = yaml.icon, properties = yaml.properties.map { case (name, prop) => name -> PropertyDef( `type` = PropertyType.valueOf(prop.`type`.capitalize), title = prop.title, description = prop.description, default = prop.default.flatMap(parsePropertyValue), options = prop.options, group = prop.group, showExternalParam = prop.showExternalParam.getOrElse(false), validation = prop.validation.map(parseValidation) ) } ) ``` ### Pebble Custom Functions for Components ```scala // Source: Pebble extension API import io.pebbletemplates.pebble.extension.* import io.pebbletemplates.pebble.template.EvaluationContext class SummerPebbleExtension( themeService: ThemeService, componentContext: ComponentContext ) extends AbstractExtension: override def getFunctions: java.util.Map[String, Function] = Map( "asset" -> new AssetFunction(themeService), "componentHandler" -> new ComponentHandlerFunction(componentContext), "csrfToken" -> new CsrfTokenFunction(componentContext) ).asJava override def getFilters: java.util.Map[String, Filter] = Map( "_" -> new TranslateFilter(componentContext) // {{ 'text'|_ }} ).asJava class ComponentHandlerFunction(ctx: ComponentContext) extends Function: override def getArgumentNames = List("handler").asJava override def execute( args: java.util.Map[String, Object], self: PebbleTemplate, context: EvaluationContext, lineNumber: Int ): Object = val handler = args.get("handler").toString val alias = context.getVariable("__SELF__").toString s"/summer/component/$alias/$handler" ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | AJAX with JSON responses | HTMX with HTML fragments | 2020+ | Simpler, less JS, server-rendered | | SPA with React/Vue | Server-side + HTMX | 2023+ | Lower complexity for CMS-style apps | | Custom templating | Pebble (Twig-compatible) | Established | Migration from WinterCMS themes | | Callback-based handlers | ZIO effect-based handlers | ZIO 2.0 | Type-safe async, resource safety | | Session-based CSRF | Per-request tokens + SameSite | 2024+ | Better security, stateless option | | Polling for updates | Server-Sent Events | 2020+ | Efficient real-time, browser-native | **Deprecated/outdated:** - jQuery AJAX: Use HTMX data attributes instead - PHP component classes: Scala traits with ZIO - `defineProperties()` method: YAML file at `component.yaml` - Synchronous handlers: All handlers return `ZIO` ## Open Questions Things that couldn't be fully resolved: 1. **Template Hot-Reload in Development** - What we know: Pebble caches compiled templates - What's unclear: Best pattern for dev-mode reloading without restart - Recommendation: Use Pebble's cache.invalidate() on file change, integrate with Mill -w 2. **Component Inheritance Depth** - What we know: CONTEXT.md allows component inheritance - What's unclear: How deep inheritance should go, multiple inheritance - Recommendation: Single-level inheritance only (extend one base), use composition for reuse 3. **Property Type Extensibility** - What we know: WinterCMS has fixed property types (string, dropdown, etc.) - What's unclear: How plugins can define custom property types - Recommendation: Start with WinterCMS types, add extension mechanism in v2 if needed 4. **Pebble vs ScalaTags for Fragments** - What we know: Pebble for templates, ScalaTags for programmatic HTML - What's unclear: When to use which, can they interop? - Recommendation: Pebble for component templates, ScalaTags for helper methods returning `Html` 5. **HTMX Extension Loading** - What we know: HTMX has extensions (sse, json-enc, etc.) - What's unclear: How to manage extension loading per-component - Recommendation: Load common extensions in base layout, document per-component needs ## Sources ### Primary (HIGH confidence) - [ZIO HTTP HTMX Module](https://index.scala-lang.org/zio/zio-http/artifacts/zio-http-htmx/3.3.0) - Official module, version 3.3.0 - [ZIO HTTP SSE Examples](https://ziohttp.com/examples/server-sent-events-in-endpoints/) - Server-Sent Events pattern - [Pebble Templates](https://pebbletemplates.io/) - Twig-compatible templating, version 4.1.1 - [htmx4s Library](https://github.com/eikek/htmx4s) - Type-safe HTMX constants for Scala - [ScalaTags Documentation](https://com-lihaoyi.github.io/scalatags/) - Programmatic HTML generation - [HTMX OOB Swaps](https://htmx.org/attributes/hx-swap-oob/) - Out-of-band swap patterns - [HTMX CSRF Patterns](https://htmx.org/essays/web-security-basics-with-htmx/) - Security best practices ### Secondary (MEDIUM confidence) - [rockthejvm/scalatags-htmx-demo](https://github.com/rockthejvm/scalatags-htmx-demo) - ZIO + ScalaTags + HTMX architecture - WinterCMS ComponentBase.php - Reference implementation (local file) - WinterCMS Inspector documentation - Property schema format (local file) - [OWASP CSRF Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) - Token patterns ### Tertiary (LOW confidence) - Phase 2 Research - circe-yaml patterns (reuse) - Phase 1 Research - ZIO service patterns (reuse) ## Metadata **Confidence breakdown:** - Standard stack: HIGH - zio-http-htmx and Pebble verified via official sources - Architecture patterns: HIGH - Based on WinterCMS reference + ZIO official patterns - HTMX integration: HIGH - Official HTMX docs and zio-http module - Template engine: MEDIUM - Pebble is established but interop with ZIO needs validation - Property system: MEDIUM - Adapted from WinterCMS, Scala-specific validation untested **Research date:** 2026-02-05 **Valid until:** 2026-03-05 (30 days - stable patterns, Pebble/HTMX established)