ContentSlot
A hole for managed content — a help text, a notice, an editorial block — filled by a content
service and resolved by the host while it builds the view. It is a MicroFrontend
with resolve = SERVER pointed at the content service, plus three guarantees: a stable key, a
declared fallback and a registry of every slot, generated at compile time.
@UI("/home")public class Home {
@ContentSlot( key = "front-office.home.destacado", fallback = "No news today", // blank = the slot is not painted timeoutMs = 100, description = "Banner at the top of the home page") String destacado;}The field’s value is irrelevant: the field marks where the slot goes. Fluent and YAML:
ContentSlot.builder().key("front-office.home.destacado").fallback("No news today").build()- type: ContentSlot key: front-office.home.destacado fallback: No news todayWhat it guarantees
Section titled “What it guarantees”- Stable key —
application.screen.name: lower case letters, digits and dashes, dot-separated, at least three segments (more are allowed: the last is the name, the ones in between the screen). Checked at compile time with the indexer annotation processor: a malformed key or one used twice in the module is a compile error. Changing a key is a contract change. - It never blocks the screen — an empty slot, a content service that is slow, down or answers an
error paints the fallback (or nothing). Unlike a plain micro frontend it does not degrade to
the browser asking the content service: the content is accessory, and the browser never receives
anything the server did not resolve for that user.
degradeToClient = trueopts into it. - Registry — the indexer writes every slot of the module to
META-INF/mateu/content-slots.json(below), which the content service loads to list the slots in its backoffice.
Configuration
Section titled “Configuration”mateu.remotes=contentmateu.remotes.content.base-url=/_content-hub # or https://content.acme.commateu.remotes.content.identity=forward-authorizationmateu.self-base-url=https://shell.acme.com # a path remote is called at this server# defaults:# mateu.content-slots.remote=content# mateu.content-slots.route=/content-slots/{key}A content remote that is not registered paints every slot’s fallback, with a warning per slot.
The contract with the content service
Section titled “The contract with the content service”The content service is a registered remote that speaks Mateu’s sync endpoint — a Mateu app, or anything that answers the same JSON.
Request — one per slot on screen, in parallel, while the host builds the view:
POST {content base url}/mateu/v3/sync/content-slots/front-office.home.destacadoContent-Type: application/jsonAuthorization: Bearer … (the user's, with forward-authorization)X-Mateu-Correlation-Id: 4bf92f35…X-Mateu-Fragment-Keys: 9f2c…,77ab… (only when the host holds keys for this url, see below)
{ "route": "/content-slots/front-office.home.destacado", "actionId": "", "consumedRoute": null, "serverSideType": null, "componentState": {}, "appState": {}, "initiatorComponentId": "mfe___content_hub__content_slots_front_office_home_destacado__"}Answer with content — a Mateu UI increment whose fragments carry the slot’s components
(targetComponentId = the request’s initiatorComponentId):
{ "fragments": [{ "targetComponentId": "mfe___content_hub__content_slots_front_office_home_destacado__", "action": "Replace", "component": { "type": "ClientSide", "id": "c", "children": [], "metadata": { "type": "Text", "container": "div", "text": "Summer offers are live" } }, "state": {}, "data": {} }], "fragmentCache": { "maxAgeSeconds": 300, "varyBy": "key", "key": "9f2c…" }}Empty — 204 No Content, or an answer whose fragments carry no component: the fallback is
painted. Error or timeout — any non-2xx, or no answer within the slot’s timeout: the fallback is
painted and the call is cancelled.
Cache (optional) — fragmentCache says what the host may do with the answer: varyBy none
(never cached — also when absent), shared, user, or key with an opaque key (e.g. the hash of
the user attributes the content targets). The host keeps the answer under url + key for
maxAgeSeconds, remembers each user’s key for as long, and sends the keys it holds in
X-Mateu-Fragment-Keys; when the key the service computes for this user is one of them, it may
answer without the fragments:
{ "fragments": [], "fragmentCache": { "maxAgeSeconds": 300, "varyBy": "key", "key": "9f2c…", "notModified": true } }A Mateu content service gets all of this by implementing FragmentCachePolicySupplier on the view
that answers (FragmentCachePolicy.byKey(hash, 300)): Mateu stamps the policy and strips the
fragments when the host already holds the key.
The slot descriptor
Section titled “The slot descriptor”Each module compiled with the indexer annotation processor carries
META-INF/mateu/content-slots.json:
{ "version": 1, "mateuVersion": "3.0-beta.26", "slots": [ {"key": "front-office.home.destacado", "application": "front-office", "screen": "home", "name": "destacado", "declaredIn": "com.acme.Home", "field": "destacado", "route": "/home", "fallback": "No news today", "description": "Banner at the top of the home page", "timeoutMs": 100, "degradeToClient": false} ]}The running app also answers it, merged across all its modules (a key declared by two modules is
listed in duplicates and warned about), when mateu.content-slots.descriptor.enabled=true:
POST /mateu/v3/sync/_no_route{"actionId": "__content_slots__", "route": ""}→ {"appData": {"_contentSlots": {"version": 1, "slots": [...], "duplicates": []}}, ...}It is off by default: it describes the application’s screens.
.NET and Python
Section titled “.NET and Python”Both ports have content slots, with the same contract with the content service. Two differences:
there is no annotation processor, so a malformed or repeated key fails when the views are
registered (at startup) instead of at compile time; and there is no YAML ContentSlot builder.
builder.Services.AddMateu(o =>{ o.Remotes.Add(new RemoteDescriptor("content", "/_content-hub", 1000)); o.SelfBaseUrl = "https://shell.acme.com"; // o.ContentRemoteId = "content"; o.ContentRoute = "/content-slots/{key}"; (the defaults)});
[UI("home")]public class Home{ [ContentSlot(Key = "front-office.home.destacado", Fallback = "No news today", TimeoutMs = 100, Description = "Banner at the top of the home page")] public string? Destacado { get; set; }}from typing import Annotated
from mateu_ui.uidl import ContentSlot, RemoteDescriptor, ui
@ui("home")class Home: destacado: Annotated[str | None, ContentSlot(key="front-office.home.destacado", fallback="No news today", timeout_ms=100, description="Banner at the top of the home page")] = None
CONTENT = RemoteDescriptor("content", "/_content-hub", 1000)# add_mateu(app, my_module, remotes=[CONTENT], self_base_url="https://shell.acme.com")The fluent twins are .NET new ContentSlot("front-office.home.destacado") { Fallback = "…" } and
Python components.ContentSlot(key=…, fallback=…). The slot descriptor action is switched on with
o.ContentSlotsDescriptorEnabled = true / add_mateu(content_slots_descriptor_enabled=True).
See it live in the Explorer: ContentSlot · @ContentSlot with fallback