Vai al contenuto
Vai al contenuto
Navigazione documentazione

Questa pagina non è ancora disponibile nella lingua scelta. È mostrata la versione EN.

Template Data Contract

This is the authoritative migration and ownership guide for data crossing the PHP controller/renderer boundary. It applies to PHP Plates HTML rendering and to ViewModel JSON negotiation from k0smos 0.26.0 onward.

Qualified SEO alternate rendering

SeoTemplateData::alternates is an optional locale-to-public-path map carried by ViewModel::setSeoAlternates(), independently from navigation. The existing core.seo contribution is its sole reserved-root owner in front HTML rendering. Null/missing is unqualified; [] explicitly means none. Limits are 32 locales and 2,000 UTF-8 bytes per path, with a safe root-relative URL and only a normalized page=N query. Domains, fragments, arbitrary queries, double encoding, control characters and dot segments cannot enter the declared contract. Error responses publish none. Owner services preflight unsupported locale inventories and keep usable content responses intact; strict setter validation remains a programming contract.

All eight public heads use front/_partials/_seo_alternates from Default. They require at least two members, self, a matching absolute canonical and no noindex/none directive. Hrefs use the same public base authority; x-default requires a qualified default member. PC3 derives og:locale:alternate from this same group. The language selector retains its broader navigation map, including fallbacks. Page and archives remain unqualified without an owner-proven family. Ecommerce now qualifies actual catalog/category slices selected from canonical bounded SQL, in stable base-name/ID order, with an explicit localized name for every rendered product and public category/native/source identity. Secondary card fields retain fallback; this does not certify complete editorial translation. Incomplete slices and noindex views publish none. Two qualified nondefault members can form a group without x-default. The synthetic page-two head test proves transport only; owned SQL/Kernel evidence belongs to Ecommerce. Ecommerce's declared product payload additionally owns product_selection_path and product_variant_paths: served no-JS/currency paths and selection-aware navigation, separate from query-free product-family SEO alternates. Existing core SEO bounds remain unchanged; variant queries do not enter seo.alternates. The product payload's localized variant names and complete ProductGroup graph remain Ecommerce-owned. Detail and search share a batched locale overlay; complete family schema stays static during selection while navigation and visible controls change. This pure builder and SQL helper add no shared core API, reserved root, DI key or module dependency. The bounded core-promotion decision below remains a transport decision.

Self membership, absolute URLs and reciprocity follow Google's localized-version guidance. The requirement for exact visible owner translation is this application's qualification policy, not a claim that Google forbids template-only localization.

Ecommerce profile recovery data

The existing Ecommerce overview payload owns optional profile_values and profile_errors. These are escaped nonsecret form drafts and stable field codes, separate from the stored customer used by the account header. The existing password_csrf_token covers native profile POST. Password values never enter a recovery draft; JSON refusal responses expose only reason/field codes. Canonical and DM1 templates share translated field rendering and linked ARIA descriptions. The pure validator and controller-specific JSON adapter remain module-owned; there is no new core API, root owner or DI key. Input/trust checks do not imply atomic profile/password persistence or address/import qualification.

Invariants

  • A declared payload is immutable and implements TemplatePayloadInterface; its owner is stable and appears in collision diagnostics.
  • Values normalize to null, scalar values, backed-enum values, or arrays. Presentation value objects opt in through TemplateValueInterface. Domain entities, repositories, services, containers, DB connections, and arbitrary objects are rejected before Plates or the serializer receives them.
  • Core shared roots are reserved by ViewDataSchema. Page and module payloads may not claim them. TemplateDataComposer names both owners for duplicate roots; no array spread or array_merge is allowed to decide ownership.
  • FrontViewDataProviderRegistry resolves providers from active runtime modules only. It applies the same reserved-root and duplicate-owner checks before the public SPA receives module data. Providers implementing FrontLayoutViewDataProviderInterface belong to provideLayout() instead: the Kernel adds their data to front HTML layouts only, excluding admin/JSON and avoiding duplicate homepage contributions. An inactive module has no provider instance and therefore no payload.
  • HTML and JSON receive the same normalized shape for a declared payload. JSON is not wrapped in the legacy HTML-only data variable.
  • BaseViewDataAggregator consumes flash data through SessionStore::pull(); rendering code never accesses $_SESSION directly.
  • Template-slot parameters cannot shadow the renderer-owned slot or slotContext variables.

Framework Root Inventory

The executable inventory is ViewDataSchema::definitions(). Every entry declares its PHP/presentation type, owner, sensitivity (public, internal, or personal), mutability, and Default/Sober consumers. Tests fail if catalog keys collide or an entry omits either bundled admin theme.

Owner Root variables Sensitivity / mutability
core.theme ver, theme, themeName, themeSide, assetBase, assets public, immutable
core.base app, env, request, user, flash public/internal/personal; immutable except consumed-on-read flash
core.render-profile features, ui internal/public, immutable
core.public-settings social, company, klaro, frontContent, favicons public, immutable
core.feature-flags featureFlags internal, immutable
core.module-runtime activeModules internal, immutable
core.theme-preference themePalette, themeMode personal, immutable
core.navigation menu, menuChannel, footerMenu, navigation public/internal, immutable
core.breadcrumbs breadcrumbs public, immutable
core.locale locale_switcher, locale public, immutable
core.tenant / core.spa tenant, initial internal, immutable
core.seo seo public, immutable

The favicons value contains ico, png, apple_touch_icon, regular PWA 192/512, and maskable PWA 192/512 URLs. A Media-managed family is all-or-nothing: if its module is inactive or the projected set is incomplete, all values fall back to the shared project icons. URL-only legacy tenants continue to resolve their configured values, with regular PWA icons also serving as their maskable fallback.

Request, user, tenant, locale, navigation, breadcrumb, flash, asset, and SEO value objects live under src/Rendering/TemplateData. PageTemplateData is the page-specific envelope when a more specific presentation DTO is not warranted.

Compatibility Boundary And Deprecation Window

LegacyTemplateDataAdapter is the only compatibility boundary. In 0.26.x, a raw array passed to ViewModel remains a set of direct Plates variables and is not recursively normalized; this preserves existing templates that have not yet migrated. A declared payload is strict immediately. A top-level object is not silently wrapped for HTML and fails with an actionable exception.

The migration window is:

  1. 0.26.x: all new/changed controllers and front providers use declared payloads; existing raw controller arrays remain supported.
  2. 0.27.x: CI inventory may warn on raw arrays in touched controllers; module authors must migrate front-provider contributions.
  3. 0.28.x: raw front-provider contributions are scheduled for rejection. Existing controller arrays remain behind the adapter.
  4. 0.29.0 retains the compatibility adapter and arbitrary controller arrays. Any later removal requires a separate release note and migration verification.

Module Migration

For a page, replace the raw array without changing the direct variables used by the template:

return (new ViewModel(new PageTemplateData(
    'module.example.admin-overview',
    ['exampleStatus' => $status, 'exampleWritable' => $canWrite],
)))->setTemplate('example/overview', 'admin');

Choose module-specific roots (exampleStatus, blogFeed) rather than generic core names (user, features, locale). If construction rejects an object, map it at an application/presentation boundary; never loosen the normalizer.

For front integration, implement FrontViewDataProviderInterface and return a TemplatePayload whose owner is the provider class. Register the provider only through the module's FrontViewDataProviderModuleInterface contribution. Do not place module providers in core DI definitions or instantiate inactive modules to obtain view data.

Delivered Pilots

  • Core admin settings overview: declared page payload; its former ambiguous features page root is now settingsFeatures, avoiding the core render- profile root.
  • Blog homepage feed: declared active-module contribution under blogFeed.
  • OneUptime admin overview/settings: declared page payload with scalar/array values and the module-specific oneUptimeLocale root.

Contract, collision, unsupported-object, legacy compatibility, session, JSON, active/no-module, and Default/Sober rendering coverage lives in tests/Unit/Rendering plus the focused Blog and OneUptime tests.

Public layout pilot

Ecommerce owns the declared ecommerceCurrency layout root (active choices and current session selection). It registers through the existing module provider contract; core base data remains independent of optional commerce services. LTS1 consumes this context on home/contact/product and native preview surfaces. The Blog homepage feed remains page-scoped and is not queried for every layout.

Qualified SEO alternates — core promotion review (2026-10-13)

  1. Capability/contract: extend the existing SeoTemplateData with a bounded optional alternates map, carried separately from navigation by ViewModel::setSeoAlternates(). Core transports safe locale/path values to the single reserved seo root; it does not determine translation eligibility. Missing/null means unqualified, empty means explicitly none, and neither permits inference from locale_switcher.
  2. Consumer A: Ecommerce published product detail owns active catalog identity, exact field-wise translations, ignored default-locale translation rows, slug ownership and current canonical eligibility. Preview is excluded.
  3. Consumer B: Blog published article detail owns its stable post family, exact translations and whole-row overlays including the default locale. Both consumers publish only current public same-tenant identities, exclude fallback-only/ambiguous content and recompute eligible reciprocal groups on each read. No personal state, write, retention or concurrency mutation crosses the shared presentation boundary; withdrawal removes the affected candidate.
  4. Ownership: core owns typed validation, one HTML contribution and error- status suppression. Modules retain content/locale/publication and slug policy; their existing Page dependency owns canonical/native route, rewrite and Page collision validation. Core never queries optional module tables. Language navigation remains independent and may offer fallback content.
  5. Optionality: no new DI key, registry or optional module probe is introduced. Unqualified pages and inactive consumers have no SEO alternative contribution; an explicitly empty map remains valid. Page has no translation-family identity and cannot infer equivalent pages from matching independent slugs.
  6. Verification: bounded typed/unsafe-path and duplicate-root tests, Kernel transport/navigation tests, both owner SQL/controller tests, all-eight-head renders and module-neutral/DI ownership ratchets. Category and catalog page parity need additional actual listing-source qualification, not this transport. The focused value/transport/head/owner/DI selection passes 403 cases / 3,706 assertions; three actual temporary-Kernel native/source product/Blog cases pass 164 assertions. Scoped production PHPStan and independent review pass. A broader architecture run reports two preexisting untouched failures in TenantLocaleBackfill's Documentation imports and the case-sensitive ArcadeDb migration path scan. Neither is waived or treated as new transport debt.
  7. Migration: additive metadata only; no schema/settings/data conversion. Forward deploy core and all public heads with consumer qualification together. Unknown or invalid qualification emits no alternates. Rollback must retain that fail-closed boundary rather than restore navigation-derived SEO claims. Export/reimport retain existing owner translation identities.
  8. Alternatives: local duplicate page roots fragment eight heads; a private library still requires a stable rendering root; events add no value to read- only presentation. Extend the existing core SEO value, keeping domain queries and Page's canonical resolver outside core instead of promoting a SEO service.
  9. Decision: accept the bounded transport extension based on these two actual consumers (agent source review within the authorized SEO work). Reopen for new transport semantics; additional domain consumers must prove their own family, translation and public-identity rules. Pagination is not declared complete by this record.