← Back to Component Index

Dynamic Feed — API Contract & Rendering Pipeline New · D27–D29

Site-scoped dynamic feed: SiteDtoPageDtoContainerDto[]ItemDto[]
Decisions: D27 · D28 · D29  |  Work Item: AB#1821  |  Status: Implemented — live at GET /api/mobilecontent/dynamic/site/{siteKey}/page/{pageKey}; client factory + DTO aligned to the canonical §3.3/§5 shape 2026-07-15 (D60), field names reconciled against the shipped Mobile Content API 2026-08-29  |  Updated: 2026-08-29 07:30 ET  |  Audited: 2026-07-09 21:15 ET

1. Purpose

Diagrams: the big-picture hybrid-UI architecture, the full rendering pipeline (with the theme-token rail), and the content→component adapter map are drawn in dynamic-ui-architecture.html — §1.1, §3, and §3.1.

TL;DR — Big Picture
ALPA Mobile is a Blazor Hybrid app. A Blazor-based admin surface lets content editors compose pages — choosing containers, ordering items, applying MEC brand theming — and that composition is stored on the server. When a pilot opens the app, the device fetches the page definition and Razor components render it natively, giving a result that closely matches what the editor previewed. Think of it as a lightweight CMS where the admin preview and the device render share the same Razor component library — what you build in the admin is what the pilot sees.

Any screen in ALPA Mobile that is designated for dynamic UI receives its layout from the server — the backend composes the page and the client hydrates it using its known library of Razor components and ViewModels. The client knows every component type it can render; the server decides which components appear, in what order, and with what content for a given screen and user context. Not all screens are eligible: pages with fixed regulatory requirements, complex domain-specific interactions, or platform constraints remain statically defined and are outside this system. This document defines the JSON contract the backend endpoint must deliver and the client-side rendering pipeline that consumes it for any screen that opts in.

The full content hierarchy is Site → Page → Container → Item. A Site (e.g. ual, dal) is the top-level resource the client fetches first — GET /api/mobilecontent/dynamic/site/{siteKey} returns the site's page list and its activeThemeId. This document covers the three levels below Site — the per-page render pipeline; the SiteDto shape itself, and how Site and Page each independently resolve a theme, are covered in dynamic-feed-adapter-gaps.html §2.

The container layer owns all layout authority — BackgroundToken, Columns, Title, and ViewAllLink are container properties and must not be placed on individual items. For standalone items (e.g. a single EmergencyButton or PilotCard), the backend wraps them in a bare "Stack" container with one item. The client renders this with no visual overhead and no special-casing — the wire format is uniform across all cases.

2. Client Rendering Pipeline

The pipeline mirrors the three-level data hierarchy. ComponentView.razor is the same single dispatcher at Levels 2 and 3.

Level 1 — Page (data ingestion)

Backend API
JSON response
PageDto
deserialized
PageFactory
maps to ViewModel
PageViewModel
.Components
Home.razor
page root

Level 2 — Container (page iterates containers)

Home.razor
iterates .Components
ComponentView.razor
dispatches container type
Carousel.razor
GridContainer.razor
… container component

Level 3 — Widget (container iterates items)

Carousel.razor etc.
iterates .Items
ComponentView.razor
dispatches item type
CardSmall.razor
CardHero.razor …
leaf component

At Level 1, the API response is deserialized into a PageDto, mapped by PageFactory into a PageViewModel, and handed to Home.razor. At Level 2, Home.razor iterates PageViewModel.Components and passes each container to ComponentView.razor, which dispatches to the correct container component. At Level 3, each container component iterates its own .Items collection and calls <ComponentView Vm="item" /> for each leaf. The page never sees individual items; containers never see the page structure.

3. DTO Shapes (API → Client)

3.1 PageDto — root response

public class PageDto { public string Key { get; set; } = ""; // e.g. "home", "mec" — lowercase semantic page key public string? Title { get; set; } // optional page heading public int SortOrder { get; set; } public Guid? ActiveThemeId { get; set; } // page-level theme override; when set, use instead of the site theme public string? ActiveThemeKey { get; set; } public List<ContainerDto> Containers { get; set; } = []; }

Field name reconciled 2026-08-29. The wire field is key (matching SiteDto.key/ThemeDto.key naming across the Mobile Content API), not pageId. ActiveThemeId/ActiveThemeKey are new — see §3.4 for how the client resolves them against the theme endpoints.

3.2 ContainerDto — layout wrapper

public class ContainerDto { public int Id { get; set; } // Layout discriminator — see §4 for valid values public string ContainerType { get; set; } = ""; // Section header (Carousel, Grid) public string? Title { get; set; } // "View All" link (Carousel) public string? ViewAllText { get; set; } public string? ViewAllLink { get; set; } // Grid-specific: number of columns (default 1) public int? Columns { get; set; } // Surface theming — all three are semantic token names resolved by the client via the // theme endpoint. They are NOT set by editors directly; the server populates them from // the component-type → theme mapping. CornerRadiusToken retained as a field for // backwards compatibility if per-container control is ever needed. public string? BackgroundToken { get; set; } // e.g. "Surface/Brand" public string? CornerRadiusToken { get; set; } // e.g. "BorderRadius/M" public string? PaddingToken { get; set; } // e.g. "Spacing/Small" // Sort authority: server delivers Items pre-sorted by admin-defined order. // IsSortable=false locks this container's position — the device user cannot // reorder it. Default true (user may reorder freely). public bool IsSortable { get; set; } = true; public int SortOrder { get; set; } // display order within the page // Items — pre-sorted by admin; order is authoritative. Client may allow user // reordering per-item subject to ItemDto.IsSortable. public List<ItemDto> Items { get; set; } = []; }

Id and SortOrder added 2026-08-29. Both are shipped fields on the real ContainerDto that this doc previously omitted. Id is the server-assigned int database id; the client-side reconciliation-key notes in §9 ("Caching strategy") apply to it the same way they apply to ItemDto.Id.

3.3 ItemDto — leaf item

public class ItemDto { // Item discriminator — see §5 for valid values public string ItemType { get; set; } = ""; // Server-assigned database id — used by the client for cache diffing, favorites, // and reorder persistence. Stable for the lifetime of this item and unique within // its container. Domain content references (podcast ID, document ID, etc.) go in // CtaLink/Link, not here. public int Id { get; set; } public int SortOrder { get; set; } // display order within the container // Sort authority: IsSortable=false locks this item's position within its container — // the device user cannot reorder it. Set by the comms/content team per item. // Default true (user may reorder freely within the container). public bool IsSortable { get; set; } = true; // Shared display fields — populate only what the item type uses public string? Title { get; set; } public string? HeaderText { get; set; } // card section heading (PilotCard, ButtonCard) public string? Description { get; set; } public string? Eyebrow { get; set; } // category label above title public string? Label { get; set; } // parent-category eyebrow (CardText) public string? Image { get; set; } // URL or resource path public string? Link { get; set; } // navigation target public string? LinkText { get; set; } // visible link label (PilotCard) public string? CtaText { get; set; } // CTA button label (ButtonCard, Button) public string? CtaLink { get; set; } // CTA button navigation target public string? BackgroundToken { get; set; } // Surface/* token override // Per-field theme token overrides — same theme-endpoint resolution path as BackgroundToken public string? EyebrowToken { get; set; } public string? TitleToken { get; set; } public string? DescriptionToken { get; set; } public string? HeaderTextToken { get; set; } public string? IdentityToken { get; set; } // resolves user/member identity styling public string? ContractLinkToken { get; set; } public string? CtaTextToken { get; set; } // Raw JSON, item-type-specific layout/data payload — parsed and applied by the // client based on ItemType. Not a typed backend DTO; see dynamic-feed-adapter-gaps.html §1 // and dynamic-feed-template-resolution.html for how the client interprets this string. public string? Template { get; set; } }

Fields added 2026-08-29 to match the shipped ItemDto: SortOrder, the seven per-field *Token overrides, and Template (raw JSON string). Id corrected from string? to int — it is the real database id, not an opaque reconciliation-only string. Note the Admin write-side model (AdminItemRequest) additionally supports a free-form tokenOverrides JSON string and four roster-specific tokens (rosterTitleToken, rosterNameToken, rosterEmailToken, rosterTelToken), plus viewAllLinkToken, iconSizeToken, and iconColorToken — these are not yet reflected in this read-side contract; flagged here rather than guessed at, since the Dynamic Feed integration guide doesn't confirm whether the read-side ItemDto carries them too.

Token resolution is separate from the page response. BackgroundToken, PaddingToken, and CornerRadiusToken in ContainerDto (and BackgroundToken in ItemDto) are semantic key names only — e.g. "Surface/Brand", "Spacing/Small", "BorderRadius/M". Their resolved values (colors, sizes, radii) come from a dedicated theme endpoint that the client queries separately and caches aggressively, since MEC themes change far less frequently than page content. The client resolves token names against the loaded theme at render time; the page response never carries raw color or size values. These fields are not set by editors — the server maps component type → theme on the way out.

Client factory shape shipped 2026-07-15 (D60); field list reconciled against the real backend contract 2026-08-29. ALPAMobile.Application/ApiModels/ItemDto.cs (moved from the head with D61) carries every field above, plus legacy/transition fields from the prototype wire shape (BlurbDescription, IconImage, and the ThreeUpButtons/Slider* container-flattening fields). The factory prefers the canonical field and falls back to the legacy one, so both payload generations deserialize and render. The SortOrder, per-field *Token, and Template fields added in this pass have not yet been independently confirmed against the client's checked-in ItemDto.cs — flagged for a follow-up client-code audit rather than assumed.

Deserialization gotcha for the AB#2133 client wiring: the camelCase JSON in this contract (e.g. "headerText") binds to the PascalCase C# properties only with JsonSerializerOptions.PropertyNameCaseInsensitive = true (or web defaults) — ItemDto carries a [JsonPropertyName] attribute on ItemType only. The mock services already deserialize this way; the live feed client must too.

PROPOSED contract extension — favoriteItemTypeId (int, nullable), client-first 2026-07-16 (D62, Task AB#2271): the shipped ItemDto additionally carries a nullable FavoriteItemTypeId — the Favorites API item-type id this item favorites as (e.g. 2 = Document for a document-backed hero, 4 = DocumentCategory for a category quick-list row). Absent → the renderer applies its per-component historical default; items without an Id render no favorite affordance regardless. This field is not yet part of the canonical wire shape — it is a client-side extension the mock feeds populate, to be adopted into this contract with the AB#2133 / WI-2270 work so editors' composed items can declare their favorite identity.

Flat bag, not a union type. ItemDto carries all possible fields; each ItemType populates only the subset it needs. The factory ignores unused fields. This avoids a polymorphic JSON type hierarchy at the API boundary and keeps deserialization simple.

3.4 Token Reference

Token names used in BackgroundToken, PaddingToken, CornerRadiusToken, and the per-field *Token overrides are Figma variable paths. The base values below are authoritative — full definitions in design-tokens.html — Theme Tokens. There is no separate per-MEC theme lookup — the client resolves each page's one active theme (GET /api/mobilecontent/dynamic/theme/{themeId}, where themeId is PageDto.ActiveThemeId if set, otherwise SiteDto.ActiveThemeId) and applies its tokens dictionary against the key set below. GET /api/mobilecontent/dynamic/theme returns the full theme catalogue for pre-fetching. See dynamic-feed-adapter-gaps.html §2 for the Site/Page theme-resolution order.

Surface tokens — BackgroundToken

Token pathBase valueUsage
Surface/Default#ffffffStandard white surface — default for most containers and cards
Surface/Brand#05273eALPA navy — MEC-branded container backgrounds (e.g. MEC Grid)
Surface/Subtle#dfedf9Blue tint — secondary surface, welcome card background
Surface/Primary#efefefLight grey — page-level background, alternating rows
Status/Error#c02126Red — emergency button background and error-state surfaces

Client fallback: when BackgroundToken is absent or null the client applies no background (equivalent to Transparent). Do not emit "Transparent" as a literal token value from the backend — omit the field instead.

Text/On-Brand (#ffffff) is a text token, not a background token. It is used by component Razor files to colour text and icons placed on Surface/Brand surfaces. It is not a valid BackgroundToken value.

Spacing tokens — PaddingToken

Token pathValueCSS var
Spacing/None0px--space-none
Spacing/XTiny4px--space-xtiny
Spacing/Tiny8px--space-tiny
Spacing/XXSmall12px--space-xxsmall
Spacing/XSmall16px--space-xsmall
Spacing/Small20px--space-small
Spacing/Medium24px--space-medium
Spacing/Large28px--space-large
Spacing/XXLarge40px--space-xxlarge

Corner radius tokens — CornerRadiusToken

Semantic token keys that map to Figma BorderRadius/* variables. Delivered via the theme endpoint; client resolves to a pixel value at render time.

Token pathBase valueUsage
BorderRadius/None0pxFlush containers — full-bleed banners and grids that extend edge-to-edge
BorderRadius/S4pxSubtle rounding — small chips, tags, and inline badges
BorderRadius/M8pxStandard card rounding — CardHero, CardSmall, CardText
BorderRadius/L12pxProminent rounding — ButtonCard, MEC grid tiles
BorderRadius/XL16pxLarge container panels — welcome card stack, full-width modals
BorderRadius/Full50%Pill/circle shapes — avatar frames, FAB buttons

Client fallback: when CornerRadiusToken is absent or null the client applies BorderRadius/M (8 px) — the design-system default for card components. Do not emit "0" as a raw value; use BorderRadius/None instead.

Figma typo — do not replicate: the Figma source contains Surface/LIght (capital "I"). This token is not part of the API contract. Use Surface/Subtle for the blue-tint surface. Never emit Surface/LIght from the backend endpoint.

4. Container Type Discriminators (ContainerDto.ContainerType) D28

The ContainerType string determines which ContainerSurfaceViewModel subclass the factory produces.

ContainerType value Client ViewModel Razor component Layout Notes
"Carousel" CarouselViewModel Carousel.razor Horizontal scroll Requires Title; optional ViewAllText / ViewAllLink. Items are typically HeroCard.
"Grid" GridContainerViewModel GridContainer.razor Fixed-column grid Columns (default 3). v1: ButtonCard items only; v2 widened to mixed items per D29.
"ButtonGroup" ButtonGroupViewModel ButtonGroup.razor Horizontal row Items are Button. No section header.
"Stack" StackContainerViewModel Stack.razor Vertical stack Surface-neutral vertical container. Items are typically CardSmall or CardText. Also used as a single-item wrapper for standalone widgets (e.g. PilotCard, EmergencyButton). D30.

Unknown ContainerType values: the factory must skip unknown container types (log a warning, do not throw). This ensures forward-compatibility when the backend adds new container types before the client ships support for them. Client status: implemented as ContainerFactory.TryCreate → null (skipped at the page level), shipped with WI-2270 and pinned by unit test.

5. Item Type Discriminators (ItemDto.ItemType) D28

The ItemType string determines which ComponentViewModel subclass the factory produces.

ItemType value Client ViewModel Populated fields Typical container
"PilotCard" CardViewModel (chevron variant) HeaderText, LinkText, Link Stack (single item)
"HeroCard" CardHeroViewModel Title, Description, Eyebrow, Image, Link Carousel, Stack
"CardSmall" CardSmallViewModel Title, Image, Link Stack, Grid
"CardText" CardTextViewModel Label, Title, Description, Link Stack
"ButtonCard" ButtonCardViewModel HeaderText, Description, CtaText, CtaLink, BackgroundToken Grid
"Button" ButtonViewModel CtaText (= label), Image (= icon), CtaLink ButtonGroup, Stack
"EmergencyButton" ButtonViewModel (IsEmergency=true) CtaText, CtaLink Stack (single item)

Legacy type aliases: the current ItemComponentFactory accepts several legacy strings ("SingleButtonLarge", "DocumentHero", "SliderImages", etc.) from the prototype branch. The new endpoint should use the canonical values above. The factory will continue to support the legacy aliases for transition.

Canonical values implemented client-side (2026-07-15, D60): ItemComponentFactory now accepts every canonical discriminator in the table above (including "HeroCard", "CardText", and "ButtonCard", which previously only had legacy or no handling), preferring the canonical §3.3 fields with legacy-field fallback (Description ?? Blurb, Image ?? Icon, etc.). Every row in this table is pinned by UnitTest/ItemComponentFactoryTests.cs. The Button/EmergencyButton rows above were also corrected in this pass — they previously listed Title/Link as the label/target fields, contradicting §3.3's own field comments (CtaText/CtaLink are the CTA fields) and the shipped factory.

6. Example Payload — UAL Home (Figma node 20716:1802)

Derived from the UAL screen in the ALPA Mobile Figma file, fetched via GET /api/mobilecontent/dynamic/site/ual/page/ual-home. backgroundToken: "Surface/Brand" resolves to UAL's branded navy (#002243) via the active theme (see §3.4) — the page payload carries the semantic key only, never the raw colour. id and container/item sortOrder are server-assigned integers, shown here as illustrative placeholder values.

{ "key": "ual-home", "sortOrder": 1, "containers": [ // Welcome card — PilotCard in branded Stack (UAL navy via active theme) { "id": 1, "containerType": "Stack", "backgroundToken": "Surface/Brand", "sortOrder": 0, "items": [ { "itemType": "PilotCard", "id": 101, "sortOrder": 0, "headerText": "Welcome Captain Johnson", "linkText": "View Contract", "link": "/contract" } ] }, // Quick-access row 1 — branded 3-column ButtonCard grid { "id": 2, "containerType": "Grid", "columns": 3, "backgroundToken": "Surface/Brand", "sortOrder": 1, "items": [ { "itemType": "ButtonCard", "id": 102, "sortOrder": 0, "headerText": "PDR", "ctaLink": "/pdr" }, { "itemType": "ButtonCard", "id": 103, "sortOrder": 1, "headerText": "HOTELS", "ctaLink": "/hotels" }, { "itemType": "ButtonCard", "id": 104, "sortOrder": 2, "headerText": "CALENDAR", "ctaLink": "/calendar" } ] }, // Quick-access row 2 — second branded 3-column ButtonCard grid { "id": 3, "containerType": "Grid", "columns": 3, "backgroundToken": "Surface/Brand", "sortOrder": 2, "items": [ { "itemType": "ButtonCard", "id": 105, "sortOrder": 0, "headerText": "DYK", "ctaLink": "/dyk" }, { "itemType": "ButtonCard", "id": 106, "sortOrder": 1, "headerText": "DASHBOARD", "ctaLink": "/dashboard" }, { "itemType": "ButtonCard", "id": 107, "sortOrder": 2, "headerText": "MY LEC", "ctaLink": "/lec" } ] }, // Media — Carousel of HeroCard items (podcast + communications) { "id": 4, "containerType": "Carousel", "title": "Media", "viewAllText": "View All", "viewAllLink": "/media", "sortOrder": 3, "items": [ { "itemType": "HeroCard", "id": 108, "sortOrder": 0, "eyebrow": "PODCAST", "title": "Listen Now: The Flightdeck", "image": "/images/podcast.jpg", "link": "/media/podcast" }, { "itemType": "HeroCard", "id": 109, "sortOrder": 1, "eyebrow": "COMMUNICATIONS", "title": "Read Latest: MEC Weekly", "image": "/images/comms.jpg", "link": "/media/weekly" } ] } ] }

7. Client-Side ViewModels

7.1 PageViewModel — page root

PageViewModel sits outside the ComponentViewModel hierarchy — it is not renderable by ComponentView.razor and is never an item inside a container. It is the root of the page tree, consumed directly by Home.razor. Decision: D27.

public class PageViewModel : ObservableObject { public string? PageId { get; set; } public string? Title { get; set; } public ObservableCollection<ComponentViewModel> Components { get; set; } = []; }

Components holds ContainerSurfaceViewModel subclasses (Carousel, Grid, ButtonGroup, Stack) as returned by the factory. Home.razor iterates Components and passes each to <ComponentView Vm="item" />. ComponentView.razor already dispatches all container types.

7.2 Factory Chain

Implemented as-built 2026-07-17 (WI-2270 / D63): the real classes are ContainerFactory and PageFactory in ALPAMobile.Presentation/Components/, with the composition below intact (container factory delegates items to ItemComponentFactory; unknown containers → null → filtered at the page level). Two as-built deviations from the sketch, recorded deliberately: (a) ButtonGroup items map through ItemComponentFactory then filter OfType<ButtonViewModel>() with IsTile = true — matching the legacy ThreeUpButton arm's tile rendering, so both wire paths produce identical output; (b) BackgroundToken passes through null — no "Transparent" default is injected; the renderer owns absent-token handling. Consumed today by /home-preview via the mock-pinned IHomeFeedQueries/HomeFeedQueriesRouter seam; the flat CreateAll path remains the documented legacy shape for pages that have not rolled up.

// PageFactory — maps PageDto → PageViewModel public sealed class PageFactory { private readonly ContainerFactory _containers; public PageFactory(ContainerFactory containers) => _containers = containers; public PageViewModel Create(PageDto dto) => new() { PageId = dto.Key, Title = dto.Title, Components = new([..dto.Containers .Select(_containers.TryCreate) .Where(vm => vm is not null) .Select(vm => vm!)]) }; } // ContainerFactory — maps ContainerDto → ContainerSurfaceViewModel (or null for unknown types) public sealed class ContainerFactory { private readonly ItemComponentFactory _widgets; public ContainerFactory(ItemComponentFactory widgets) => _widgets = widgets; public ComponentViewModel? TryCreate(ContainerDto dto) => dto.ContainerType switch { "Carousel" => MapCarousel(dto), "Grid" => MapGrid(dto), "ButtonGroup" => MapButtonGroup(dto), "Stack" => MapStack(dto), _ => null // unknown type — skip, log warning }; private CarouselViewModel MapCarousel(ContainerDto dto) => new() { Title = dto.Title, ViewAllText = dto.ViewAllText, ViewAllLink = dto.ViewAllLink, BackgroundToken = dto.BackgroundToken ?? "Transparent", Items = new([..dto.Items.Select(_widgets.Create)]) }; private GridContainerViewModel MapGrid(ContainerDto dto) => new() { Title = dto.Title, Columns = dto.Columns ?? 3, BackgroundToken = dto.BackgroundToken ?? "Transparent", Items = new([..dto.Items.Select(_widgets.Create)]) // D29: mixed items }; private ButtonGroupViewModel MapButtonGroup(ContainerDto dto) => new() { BackgroundToken = dto.BackgroundToken ?? "Transparent", Items = new([..dto.Items .Select(_widgets.Create) .OfType<ButtonViewModel>()]) }; private StackContainerViewModel MapStack(ContainerDto dto) => new() { BackgroundToken = dto.BackgroundToken, Items = new([..dto.Items.Select(_widgets.Create)]) }; }

8. ComponentView.razor — Dispatch

ComponentView.razor dispatches all container and leaf types via its C# type-switch. Container cases (D30 resolved — StackContainerViewModel replaces the ListViewModel placeholder):

// ComponentView.razor — container + leaf dispatch @switch (Vm) { case CarouselViewModel carousel: <Carousel Vm="carousel" /> break; case GridContainerViewModel grid: <GridContainer Vm="grid" /> break; case ButtonGroupViewModel group: <ButtonGroup Vm="group" /> break; case StackContainerViewModel stack: <Stack Vm="stack" /> break; // ... leaf component cases unchanged ... }

9. Open Items

ItemStatusNotes
Endpoint URL + auth Resolved Live at GET /api/mobilecontent/dynamic/site/{siteKey}/page/{pageKey}. [AllowAnonymous] — auth is handled at the API gateway level, per the Dynamic Feed Integration Guide. Returns 404 if the page doesn't exist or isn't Published; a page in the requesting user's DynamicPagePreviewUsers allowlist gets the live draft instead of the published snapshot (invisible to the client — no separate preview flag in the response).
ButtonGroup canonical wire path Open A button row is currently expressible BOTH as a ContainerDto.ContainerType = "ButtonGroup" (§4) and as the legacy ItemDto.ItemType = "ThreeUpButton"/"ButtonGroup" arm (§5) — one ButtonGroupViewModel, two wire paths (both supported since WI-2270). Backend to confirm which is canonical for the AB#2133 feed so the other can be documented as transition-only. Ask recorded on the AB#2270 work item.
Theme endpoint contract Resolved Full contract: theme-endpoint-contract.html. Real routes: GET /api/mobilecontent/dynamic/theme/{themeId} (themeId is the GUID from PageDto.ActiveThemeId if set, else SiteDto.ActiveThemeId) and GET /api/mobilecontent/dynamic/theme for the full catalogue. There is no mecId/per-MEC lookup, no ?since=, and no 304 — every fetch returns the full ThemeDto { id, key, description, tokens }. See §3.4.
Stack container vs ListViewModel Resolved · D30 Introduced StackContainerViewModel : ContainerSurfaceViewModel with Items: ObservableCollection<ComponentViewModel>. Surface-neutral — layout is owned by the bound surface (Stack.razor for Blazor). ListViewModel (D10/D11) unchanged; it remains a Track B domain control for date·headline·subtitle rows.
GridContainerViewModel.Items widened Resolved · D29 D25 deferred this. D29 resolves: Items widened to ObservableCollection<ComponentViewModel> to support mixed-type grids from the server.
Caching strategy Noted Client-side cache is keyed on the id/key property (PageDto.Key for pages; ItemDto.Id for individual items). ItemDto.Id is the server-assigned database id, stable for the lifetime of the item. No HTTP ETag or cache-control header — Vlad's Mobile Content API docs (2026-08-29) confirm neither ?since=/304 nor any other conditional-GET mechanism exists for the dynamic site/page/theme endpoints; treat re-fetch cadence as a purely client-side policy (e.g. on foreground) rather than a server-negotiated one.
Spacing token strategy Resolved · D35 Spacing tokens are delivered via the theme endpoint, not the page feed (D35). PaddingToken in ContainerDto carries a semantic token name (e.g. "Spacing/xsmall"); the client resolves the final pixel value from the active theme's ThemeDto.tokens dictionary (§3.4) — same resolution path as BackgroundToken. Every site/page gets ALPA base spacing values unless the theme assigned to it overrides them; overriding requires only an Admin Portal token edit, no app release. DQ-12 remains open to confirm Figma path name casing.
Differential update / re-fetch strategy Resolved — no server support Confirmed 2026-08-29: the real GET /page/{pageKey} endpoint has no ?since= parameter and no conditional-GET (304) support — every call returns the full PageDto. There is no differential-update endpoint and none is planned per Vlad's Mobile Content API docs. Client owns reconciliation entirely — re-fetch on a client-side cadence (e.g. app foreground, short TTL per §9 "Caching strategy") and compare the incoming payload against cache by id, resolving adds, removes, updates, and reorders itself.

Skeleton UX for initial load vs silent background refresh: DQ-13.
Sort authority + IsSortable Resolved Server delivers ContainerDto.Items and PageDto.Containers pre-sorted in admin-defined order. Device users may reorder containers/items within their local cache. IsSortable=false on a ContainerDto or ItemDto locks that element's position — the comms/content team sets this per item. Default is true (user may reorder freely). See §3.2 and §3.3.
Token fields are theme-derived, not editor-set Resolved BackgroundToken, PaddingToken, and CornerRadiusToken in ContainerDto are populated server-side from a component-type → theme mapping. Editors do not set them directly. They are global theme decisions, not per-item overrides. Retained as explicit fields for backwards compatibility — if a future admin UI needs limited controls, they are already surfaced. See §3.2 and §3.4.
CornerRadiusCornerRadiusToken rename Resolved Field renamed from double? CornerRadius to string? CornerRadiusToken to be consistent with BackgroundToken and PaddingToken. Carries a semantic key (e.g. "BorderRadius/M") resolved via the theme endpoint, not a raw pixel value. See §3.2 and §3.4.
WidgetDtoItemDto rename Resolved DTO class renamed from WidgetDto to ItemDto for consistency: the container property that holds these objects is named Items (not Widgets), and the companion factory is ItemComponentFactory. The JSON field name "items" is unchanged. See §3.2, §3.3, §7.2.