PageDto → ContainerDto → ItemDto contractScope: the companion contract doc defines Page → Container → Item and is current for that scope. This doc covers the two things it does not cover: the Site and Theme levels that sit above Page, and the Template object that lets an Item pull dynamic data instead of shipping static fields. Both are referenced informally elsewhere; neither has a settled, modeled contract yet.
An ItemDto can carry a Template instead of static display fields — instructions for
where to fetch data, how to filter/sort/limit it, and what ItemType to render the result as. The
template body is deliberately schema-less below two dispatch fields, so adding a new content source never
requires a wire-contract or DTO change:
Wire shape: per the Mobile Content API docs, ItemDto.Template is a raw JSON string, parsed and interpreted by the mobile client based on ItemType — the backend does not send a structured TemplateDto object. Everything below (TemplateDto, its typed RenderType/ContentType, the fail-soft accessors) is the client's own parsed/typed representation of that string, not a backend-defined DTO.
renderType — stamped onto each resolved item's ItemType; selects the ViewModel via the existing, unmodified ItemComponentFactory switch.contentType — selects which adapter resolves the template (e.g. "LecLink", "DocumentItem").parentFilterType, filterValue, scope, category, searchFilter, action, …) — an open key/value bag each adapter reads only the keys it defines.| Piece | Status | Notes |
|---|---|---|
TemplateDto |
Scaffolded | ALPAMobile.Application/ApiModels/TemplateDto.cs — typed RenderType/ContentType, opaque Parameters dictionary, TryGetString/TryGetInt/TryGetBool fail-soft accessors. Wired onto ItemDto.Template. |
IFeedTemplateContentFactory (renamed from IFeedTemplateContentAdapter) |
Scaffolded | One implementation per contentType; the async/IO boundary (RawRepresentationFactory rule — the pure Container/Item factories never resolve services at runtime, so lookups live here). One concrete adapter now exists: DynamicListFactory (contentType: "DynamicList") — see Template Resolution, Content Adapters & PageResolver. Still open: single-row adapters like DocumentItemContentAdapter, LecLinkContentAdapter. |
SearchFilterSpec |
Scaffolded | Parses "?sort_by=date&order=desc&limit=3" into sort+take. Now called from DynamicListFactory — but only the limit parameter is applied; sort_by/order are parsed and unused (no generic sortable field on ItemDto). See Template Resolution §3. |
| Template-resolution orchestrator | Done | PageResolver (ALPAMobile.Presentation/Adapters/PageResolver.cs) walks a raw PageDto, dispatches each templated item to its adapter by ContentType, and splices resolved ItemDtos back in before handing the page to the unmodified PageFactory. Not yet wired into DI or called from a real page load — see Template Resolution, Content Adapters & PageResolver §5. MockHomePreviewFeedService's hand-rolled equivalent is still the only thing exercised end to end today. |
| Route placeholder resolution | Gap | No generic {lecId}-style substitution exists. Every existing factory (CommitteeCardFactory, DocumentHeroFactory, CategoryCardFactory) hand-interpolates its own route string — a template's action field needs a shared resolver instead. |
| "List" item repeater expansion | Done | PageResolver expands any item with Template set into N sibling ItemDtos, not gated to itemType: "List" specifically — broader than originally scoped here. Worth confirming that's the intended contract; see the note in Template Resolution §4. |
| Document-hub rendering component | Gap | No document-listing/hub component exists on DotNet10/Blazor Hybrid to pair with a future DocumentItemContentAdapter. A DocumentsHub.razor (+ DocumentsBrowser, DocumentRow, DocumentScopeBanner, DocumentIconMask, DocumentSort) exists on the unmerged origin/feature/aa/2368---Blazor-Documents-Page branch, but predates the Blazor Hybrid rename (namespaced ALPADocs.Components.Library, not ALPAMobile) and targets the pre-refactor factory-based component system, so it can't be pulled in as-is. Needs a rebuild against the current architecture, using that branch only as reference. |
docs/mobile-content-api/dynamic-feed.html and admin-portal.html describe the real
hierarchy that sits above PageDto — confirmed live 2026-08-29:
Site → Page → Container → Item, with Theme fetched independently by GUID. Theme is not a nesting level in the containment tree — it's a resource fetched via activeThemeId, which both SiteDto and PageDto independently carry (a Page's activeThemeId, when set, overrides the Site's for that page only). The recommended client integration order is still fetch Site → fetch its active Theme and apply tokens → fetch Page(s) → if the Page carries its own activeThemeId, fetch and apply that theme instead — that's a fetch sequence, not a hierarchy level.
| Concept | Documented shape | Code |
|---|---|---|
| Site | GET /api/mobilecontent/dynamic/site/{siteKey} → { key, description, activeThemeId: guid?, activeThemeKey: string?, pages: PageDto[] } — the top-level container. pages is the page list only (keys/titles/sortOrder) — full page content is fetched separately per page. |
No SiteDto — nothing in ALPAMobile.Application/ALPAMobile.Domain as of this doc's last code check; needs re-verification against current source. |
| Theme | GET /api/mobilecontent/dynamic/theme/{themeId} (themeId = guid) or GET /api/mobilecontent/dynamic/theme (full catalogue) → { id, key, description, tokens: Dictionary<string,string> } — a flat token map, the source of values like ContainerDto.BackgroundToken = "Surface/Brand". No ?since=/304 — every call returns the full dictionary. |
No ThemeDto — same, needs re-verification against current source. |
| Page workflow metadata | workflowState (Draft/Published/Unpublished/Archived), activeSnapshotId, previewMessage, isLocked, lockedByUser — documented on the admin side only. |
Unmodeled — Q1 below explains why the client-facing endpoint doesn't need these at all. |
There is only one theme system — the ThemeDto/activeThemeId one above. A separate IThemeResolver/IThemeQueries pipeline (ALPAMobile.Application/Abstractions/Theming/) previously targeted an unbuilt, speculative per-mecId ThemeResponse endpoint (/api/theme/{mecId}?since=) documented in theme-endpoint-contract.html. That endpoint was never built and has since been corrected to describe the real theme system instead — there is no more risk of two systems colliding. If IThemeResolver still contains mecId-keyed or ?since=-based caching logic, that logic doesn't correspond to anything the backend actually implements and should be pointed at the real GET /theme/{themeId}/GET /theme routes instead.
Neither Site nor Theme needs the same kind of adapter as an Item. A Theme's token dictionary is applied once
per page render, upstream of container mapping — closer to a resolver that populates
ContainerDto.BackgroundToken/CornerRadiusToken/PaddingToken than to a
per-row RawRepresentationFactory<T>. A Site is mostly orchestration — given a key,
decide which Page(s) to hand to PageFactory and which Theme wins (page override vs. site default) —
a query/coordinator service, not a mapping factory.
Confirmed 2026-08-29: yes, but the mechanism is entirely server-side and invisible to the client. Each page carries a DynamicPagePreviewUsers allowlist (managed via the Admin API's preview-users endpoints). When the authenticated requesting user is on that list for a given page, GET /page/{pageKey} transparently serves the live draft rows instead of the published snapshot — same PageDto shape, no separate preview flag in the response. There is no on-device editor-preview toggle and the client never sees workflowState/isLocked/previewMessage/lockedByUser — those stay admin-only.
filterValue: "SELF" resolve?A template's filterValue: "SELF" (e.g. "this pilot's own LEC") implies resolving against the current user's context. No current-user/context service was found among the query abstractions surveyed.
Does the backend pre-resolve SELF before the client ever receives the template, or does a content adapter need to call a user-context service directly? This determines whether adapters need a new dependency.
activeThemeId override: confirmed by both docs? ResolvedConfirmed 2026-08-29 against both docs. dynamic-feed.html's recommended integration pattern states it explicitly: "If PageDto.activeThemeId is non-null, fetch and apply that theme instead of the site-level theme for this screen." admin-portal.html's write-side model corroborates it independently: AdminPageRequest.activeTheme is documented as "Key of the theme to override the site-level theme for this page," and AdminPagePatchRequest.clearActiveTheme exists specifically to "removes the page-level theme override so the site theme applies." Both docs agree — no drift on this point.
IThemeResolver? SupersededSuperseded 2026-08-29 — the premise no longer holds. The separate per-MEC ThemeResponse pipeline this question assumed was "existing" was never actually built server-side (see §2's note); the real backend has only ever had one theme system, the dynamic-feed ThemeDto token map described in §2. There is nothing left to converge — IThemeResolver should simply be pointed at the real GET /theme/{themeId}/GET /theme routes, if it isn't already.
searchFilter's grammar complete?SearchFilterSpec currently only parses by (sort field), direction, and top (limit) — the full vocabulary seen in sample payloads to date.
Confirm whether additional operators (e.g. date-range predicates, multi-field sort) are expected before more content adapters are built against this grammar.
action) need to be?Sample templates use simple single-placeholder routes ("/lec/{lecId}"). No shared substitution helper exists yet — every current factory hand-interpolates its own route string.
Confirm whether a plain {field} substitution against the resolved raw row covers every planned action value, or whether richer expressions (conditionals, nested lookups) are anticipated.