Status: Draft
Updated: 2026-05-25
Owner: Core
Purpose: Draft for package-scoped theme resolution, template overrides, asset loading, and local customization behavior.
- Defines how the system chooses template files and assets from core, scoped packages, and local override sources.
- Covers fallback order, override safety, cache behavior, and rendering integration.
- Depends on core architecture and static/dynamic content decisions.
- Keeps presentation replaceable without hiding Twig, AssetMapper, or Symfony conventions.
The theme engine should resolve presentation files without turning rendering into a separate framework. Twig should remain the template layer, AssetMapper/importmap should remain the default asset layer, and packages with theme scopes should provide a documented fallback and override structure. Templates use logical Twig namespaces instead of relative cross-area imports: @frontend/..., @backend/..., and @root/....
The core should ship safe default templates for both backend UI and frontend content rendering. This native baseline behaves as immutable virtual frontend-theme, backend-theme, and system-template packages, so the CMS can render content and backend UI even when no installable package is active. A selected package with frontend-theme scope may override frontend-facing templates and provide assets. A selected package with backend-theme scope may override backend templates. Packages without the matching scope must not override that area. Packages may reference the root namespace for shared fallbacks, but only packages with system-template scope may override root-level shared templates.
Theme-capable packages may also ship PHP classes below a package-owned namespace for Twig functions, Twig extensions, event subscribers, or other documented extension hooks. Package PHP integration must still be explicit, manifest-driven, and validated before activation. package.php must never be included during discovery and may only be included after validation and activation.
Dynamic schema Twig stored in the database is not part of theme template resolution. The theme provides the outer page layout and generic fieldset fallback renderer; schema rendering may replace or extend only the inner content block for that schema. Because database-backed Twig is not visible to Tailwind's filesystem scan, schema rendering needs a later build input layer that aggregates CSS class usage from active custom schema Twig and exposes it to tailwind:build.
Theme-capable packages are intentionally partial. A package may override only the templates and assets allowed by its scopes, while field templates, form fragments, pagination, menus, error pages, and other missing files fall back to the native baseline. Reusable Twig macros should therefore be aggregated and namespaced instead of overwritten globally. Native fallback templates must depend only on native macro namespaces; packages may add their own macro namespaces, but they must not replace the macro files that fallback templates require.
Theme resolution should be deterministic. When several sources provide a template or asset, the resolver should explain which source won and why. This is important for debugging, updates, self-update safety, and LLM-assisted review.
Package activation, deactivation, update, or override changes may change Tailwind class usage and AssetMapper inputs. The production-safe workflow should run through php bin/console assets:rebuild: mirror active package assets into the AssetMapper-visible package directory, rewrite package-authored CSS/JS asset references, rewrite the generated CSS/JS registries, install assets, install importmap assets, run Tailwind, compile the asset map only for APP_ENV=prod, and clear the cache as the finalizer. A local file watcher may still be useful during development, but administrative package changes should use a deterministic rebuild action with clear logs and failure handling.
Discovered packages should not become active automatically. Discovery may validate metadata and show the package as available, but activation must be an explicit admin action. This guarantees that manifest validation, compatibility checks, asset rebuilds, cache invalidation, diagnostics, and action logs happen in the right order before the package affects rendering.
Package activation should fail safely. If validation, cache invalidation, Tailwind build, or AssetMapper compilation fails, the system should restore the previous active package option where practical and show the failure through the operational action log. This keeps the lifecycle simple without introducing a separate staged asset compiler.
Package dependency resolution is deferred to the package installer and activation workflow. The shared core package planner should not infer dependencies or compatibility maps. A later resolver should define how package manifests express required core versions, package dependencies, conflicts, optional suggestions, provided capabilities, and update compatibility before an administrator can activate or update a package.
- Use Twig for templates and keep template names readable.
- Use AssetMapper and importmap entrypoints for theme and page assets where possible.
- Use
symfonycasts/tailwind-bundlefor Tailwind builds already present in the project. - Define package manifests using the shared
.manifeststyle withPACKAGE_*keys. - Use the package layout
packages/<package-slug>/with.manifest, optionalpackage.php,src/,templates/,assets/,config/,migrations/, and optionallanguages/. - Let packages define their own PHP namespace for classes in
src/, constrained to a package-owned root namespace. - Allow package classes to register with documented event hooks or tagged services only after manifest validation and activation.
- Use the current public theme hooks for extension work that belongs outside template fallback:
ViewContextEvent,ContentRenderContextEvent,ContentRenderedEvent,NavigationBuilderEvent,ResponseHeadersEvent, andOutputGeneratedEvent. - Use
navigation()for theme menus. The helper returns normalized parent/child trees, defaults to three levels, and supports explicit depth, absolute start-level, and root-item split-menu slices. - Themes with fully custom menu-building rules may consume the flat
NavigationBuilder::collectItems()result from PHP and render their own structure instead of using the corenavigation()tree. - Do not add template-path or asset-collection hooks while namespace discovery and AssetSync already provide deterministic package contribution paths.
- Template namespace contract:
@frontend/...maps totemplates/frontend/**; activefrontend-themepackage paths are searched before native templates, while active module/provider package paths are searched after native templates for additive package views.@backend/...maps totemplates/backend/**; activebackend-themepackage paths are searched before native templates, while active module/provider package paths are searched after native templates for additive package views.@root/...maps to shared project templates such astemplates/base.html.twigandtemplates/macros/**. Active packages may reference this namespace for fallbacks, but only packages withsystem-templatescope may override root-level shared templates.@provider/...maps to active provider packagetemplates/provider/**directories before nativetemplates/provider/**. A package withPACKAGE_SCOPE=captcha-providercontributestemplates/provider/captcha/**; a package withPACKAGE_SCOPE=editor-providercontributestemplates/provider/editor/**.- The repository template root should contain only
base.html.twigplus shared directories such asmacros/**; routable or area-specific templates belong infrontend/**orbackend/**.
- Suggested frontend template resolution order:
- Local project override.
- Active package with
frontend-themescope. - Active package contribution where an extension point allows it.
- Native frontend fallback.
- Suggested backend template resolution order:
- Local project override.
- Active package with
backend-themescope. - Active package contribution through explicit backend UI extension points.
- Native backend fallback.
- Suggested root/shared template resolution order:
- Local project override.
- Active package with
system-templatescope. - Native root fallback.
- Define whether plugin contributions are allowed per template area, for example public page, editor widget, form field, navigation item, or content block.
- Keep admin templates more restrictive than public-facing templates.
- Let package overrides follow the original folder structure. If a file exists in the active scoped package at the corresponding path, it replaces the matching base file for allowed template areas.
- Use
templates/frontend/**for frontend content, user, and error-page templates. Usetemplates/backend/**for admin, editor, setup, and operation templates. Keep the root clean:templates/base.html.twigis the global wrapper, and sharedtemplates/macros/**may be aggregated but must stay namespaced. Do not add generic root-level page templates. - Mirror the same ownership model in assets: package files below
assets/frontend/**feed frontend-theme registries only when the package hasfrontend-theme, files belowassets/backend/**feed backend-theme registries only when the package hasbackend-theme, and package assets outside those area folders are shared/global extension assets only when the package has a global runtime scope. - Because native and package CSS are currently compiled into one
app.css, theme styles that are not intentionally global should scope selectors to their rendered area root, for example.system-frontendor.system-backend. - CSS classes follow the template namespace they belong to: root/shared templates use
{system|package-slug}-{class}, provider templates use{system|package-slug}-{provider-scope}-{class}, and frontend/backend templates use{system|package-slug}-{frontend|backend}-{class}. A template may use root classes plus classes from its own namespace, but not classes from a different rendered area. - Package translation sources use
languages/<locale>/*.yaml; packages that ship translations must include a configured fallback catalogue, and package-owned keys must stay belowpkg.<package-slug>.*. Only active package language files are aggregated into the generated runtimemessagescatalogue during the package rebuild queue. - Use
templates/frontend/frontend.html.twigas the native frontend base layout. Usetemplates/backend/admin.html.twig,templates/backend/editor.html.twig, andtemplates/backend/setup.html.twigas backend area layouts. Put optional variants belowtemplates/frontend/layouts/**andtemplates/backend/layouts/**. - Use
templates/frontend/content/entity.html.twigas the generic content-entity fallback renderer. Schema Twig may replace the inner content rendering later. - Use
templates/frontend/error-pages/default.html.twigas the generic error fallback when no custom system entity and no status-specific error template exists. - Use
templates/backend/operations/**for the ActionLog overlay and other operation UI fragments. Operation overlays should not require their own full layout. - Use a resolver service for template candidates and asset candidates.
- Use tagged services for additive package contributions such as content block renderers, editor widgets, or navigation renderers.
- Aggregate reusable Twig macro/function files by package namespace rather than letting package files overwrite global helpers.
- Keep native macro namespaces stable for fallback templates, for example
core.form,core.ui, andcore.content; package macro namespaces should include the package identifier. - Validate package template paths before activation: any valid package scope may ship additive
templates/frontend/**andtemplates/backend/**views,frontend-themeandbackend-themedecide whether those views can override native area templates,system-templatemay ship root/shared templates,templates/macros/core/**is reserved for system-template, and package-owned macros must live undertemplates/macros/{package-slug}/**. - Use stable native provider slots where optional providers can contribute markup. Example: native
@frontend/partials/forms/fields/captcha.html.twigincludes@provider/captcha/field.html.twig, and native@backend/editor/fields/richtext.html.twigincludes@provider/editor/richtext.html.twig. Missing captcha providers remain a backend no-op/resolved decision; Twig renders the nativetemplates/provider/captcha/**fallback when no provider package is active. - Use service decoration or configuration only for explicit replacement points such as a custom template resolver.
- Cache resolved package metadata and invalidate it when package configuration, manifests, or override files change.
- Keep dependency and compatibility checks in the package installer/activation workflow rather than in generic package planning.
- Discover newly extracted packages as inactive/available until an administrator explicitly activates them.
- Require explicit activation before a discovered package contributes templates, assets, PHP classes, Twig extensions, or event subscribers.
- Trigger an explicit asset rebuild after package activation, deactivation, update, or relevant override changes.
- Roll back to the previous active package option when activation fails before the new package can be safely finalized.
- Run package lifecycle rebuilds through
assets:rebuild. - Use this command order: package asset mirror and registry rewrite, translation aggregation,
assets:install,importmap:install,tailwind:build, production-onlypublic/assetscleanup andasset-map:compile, thencache:clearas the finalizer. - Treat package templates, CSS sources, JavaScript sources, imported assets, and manifest-declared assets as rebuild inputs.
- Rewrite generated package CSS registries with active package Tailwind
@sourceentries before active package CSS@importentries so Tailwind can discover classes and compile the final aggregate before AssetMapper serves it. - Add a schema-Twig Tailwind aggregation layer before production depends on custom schema classes; the rebuild should generate a deterministic source or safelist artifact from active schema Twig stored in the database.
- Rewrite generated package JavaScript registries with static imports for mirrored active package JavaScript so AssetMapper can map the same deterministic import tree.
- Mirror static package assets such as images, fonts, videos, SVGs, and vendored browser dependencies into the AssetMapper-visible package path. Do not expose source package directories directly.
- Normalize CSS
url()references during asset mirroring where needed so referenced fonts and images still resolve after Tailwind writes the built aggregate CSS. - Normalize package-authored JavaScript static imports during asset mirroring when the mirrored path differs from the source path. Do not rewrite vendored dependency files under
vendor,vendors, ornode_modules. - Use a development watcher only as a local convenience; do not rely on watchers for production/admin lifecycle changes.
- Provide diagnostics for resolved templates and assets in development mode.
SystemDebugCollectorcurrently records public hook dispatches and Twig namespace path order, then exposes that data throughdebug_info()and anAPP_DEBUG=1HTML comment. - Keep package assets namespaced to avoid collisions.
- Keep database-backed schema Twig outside the theme file fallback order.
- Package releases are extracted as subdirectories below
packages/, where their manifest can be discovered before PHP classes, templates, assets, and overrides are loaded.
- Test fallback order for local, scoped package, extension, and core templates.
- Test folder-structure-based overrides replace only allowed base files.
- Test partial themes fall back to native field, form, pagination, menu, and error templates when files are missing.
- Test macro aggregation keeps native macro namespaces available when packages add their own macro files.
- Test package PHP classes are loaded only after manifest validation and only through documented hooks.
- Test missing templates fall back safely or produce actionable developer errors.
- Test that package contributions are accepted only for allowed template areas.
- Test package template path validation for additive frontend/backend views, root templates, core macros, and package-owned macro paths.
- Test active package manifest validation.
- Test dependency, conflict, and compatibility diagnostics once the package installer defines the manifest keys.
- Test newly discovered packages remain inactive until explicitly activated.
- Test inactive packages do not contribute templates, assets, classes, extensions, or event subscribers.
- Test that theme resolution does not override database-backed schema Twig behavior.
- Test database-backed schema Twig class aggregation before relying on schema-authored Tailwind classes in production builds.
- Test package activation/update triggers the asset rebuild workflow.
- Test failed package activation restores the previous active package option and reports the failure.
- Test asset rebuild failures leave the previous active assets usable where practical and report actionable errors.
- Test
assets:rebuildreports planned and current steps through the ActionLog payload. - Test
cache:clearruns after package asset sync, Tailwind, and production AssetMapper compilation. - Run
php bin/console tailwind:buildafter Tailwind-related changes. - Keep
php bin/console asset-map:compileproduction-only; do not use it as a local development verification step. - Render representative public routes with
php bin/console render:route /<route>.
- Decision recorded: Use Twig and AssetMapper as the first theme engine foundation.
- Decision recorded: Use
.manifestfiles withPACKAGE_*keys for package metadata. - Decision recorded: Packages live under
packages/<package-slug>/and may contain.manifest, optionalpackage.php,src/,templates/,assets/,config/,migrations/, and optionallanguages/. - Decision recorded: Packages may ship PHP classes under their own namespace and integrate through documented event hooks, tagged services, or Twig extensions after manifest validation and activation.
- Decision recorded: Make local project overrides stronger than active theme files.
- Decision recorded: Database-backed schema Twig is owned by content schema rendering, not the theme engine. Themes provide outer layout and generic fallback rendering.
- Decision recorded: Database-backed schema Twig needs a Tailwind build input layer because
tailwind:buildcannot scan custom Twig that only exists in the database. - Decision recorded: Theme file overrides follow the original folder structure and replace the corresponding base file only in allowed public template areas.
- Decision recorded: The native project templates behave as immutable virtual
frontend-theme,backend-theme, andsystem-templatepackages, so content, shared fallbacks, and backend UI remain renderable without installed packages. - Decision recorded: Theme-capable packages are allowed to be partial. Missing templates and assets fall back to the native baseline.
- Decision recorded: Scope-specific template areas protect backend templates from frontend-only packages and frontend templates from backend-only packages.
- Decision recorded: Logical Twig names are canonical for area templates:
@frontend/...,@backend/..., and@root/.... Relative cross-area imports should not be used in native templates. - Decision recorded: Packages may reference
@root/..., but they may only override root-level shared templates when their manifest declaressystem-template. - Decision recorded: Package CSS/JS follows the same area boundary as templates:
assets/frontend/**is frontend-theme-specific,assets/backend/**is backend-theme-specific, and root/shared package assets are global extension assets only for packages with a global runtime scope. - Decision recorded: Asset buckets define ownership and rebuild order, not hard CSS sandboxing. CSS rules still need area root selectors until a later validator can enforce stricter selector namespaces.
- Decision recorded: Package translations are active-state-gated like assets and templates. Source catalogues live under
packages/<slug>/languages/<locale>/*.yaml, require a configured fallback catalogue when present, must usepkg.<slug>.*keys, and are aggregated into generated Symfony runtime catalogues only for active packages. - Implemented baseline: Translation runtime aggregation is split into source collection, YAML merge/collision handling, and staged runtime-directory writing behind the aggregate action used by package-aware rebuilds.
- Decision recorded: Package macro namespaces are directory-based:
templates/macros/core/**belongs to the system namespace, while packages may add their own macros undertemplates/macros/{package-slug}/**. - Decision recorded: Reusable Twig macros/functions are aggregated by provider namespace instead of overwritten globally. Native fallback templates only depend on native macro namespaces.
- Decision recorded: Package release archives are extracted as subdirectories below
packages/, then discovered through their manifest before classes, templates, assets, or overrides are loaded. - Decision recorded: Discovered packages are inactive by default and require explicit activation before they can affect rendering or register contributions.
- Decision recorded: Package dependency maps are intentionally deferred to the package installer and activation workflow; generic Core package planning remains dependency-neutral.
- Decision recorded: Failed package activation rolls back to the previous active package option where practical and reports the error through the operational action log.
- Decision recorded: Package lifecycle changes run
assets:rebuild, with AssetMapper compilation only in production and cache clearing as the finalizer. File watchers are only a development convenience. - Decision recorded:
assets:rebuildis the global rebuild operation for package lifecycle and manual recovery flows. It runs package asset sync before Tailwind and runscache:clearlast so streamed or polled ActionLog UIs can recover from cache invalidation using persisted cursors. - Decision recorded: Theme packages should extend rendering through documented context, content-output, navigation, response-header, and final-output hooks where needed; template namespace discovery and package asset sync do not need separate public collection hooks for now.