Skip to content

Case study: SSR to SSG control plane

This case study shows Mateu used in a distributed content publishing system. A CMS, an SSG worker, and a static server are each owned by separate services; a Mateu control plane manages and monitors the pipeline. It illustrates how the patterns from the rest of the real-world section combine in a single production system.


The system publishes content from a CMS to a static site generator (SSG). The control plane is a Mateu backoffice that manages and monitors the pipeline.

Content editors → Mateu control plane → Content service → SSG pipeline → Static servers

Components:

ServiceRole
control-planeAdmin UI (Mateu) — monitors the pipeline, triggers deployments
content-serviceStores and serves structured content
ssg-workerConverts content to static HTML
static-serverServes the generated HTML
shellAggregates the UIs of all services into one backoffice

Each service exposes its own Mateu UI:

// In the content-service
@UI("/_content")
public class ContentServiceUI {
@Menu PagesOrchestrator pages;
@Menu TemplatesOrchestrator templates;
}
// In the ssg-worker service
@UI("/_ssg")
public class SsgServiceUI {
@Menu JobsPage jobs;
@Menu DeploymentsPage deployments;
}

The shell aggregates them:

@UI("")
public class ControlPlane {
@Menu RemoteMenu content = new RemoteMenu("Content", "https://content-service/_content");
@Menu RemoteMenu ssg = new RemoteMenu("Publishing", "https://ssg-worker/_ssg");
}

See Service-owned UI modules for the full pattern.


The jobs listing shows publishing jobs as UI rows with per-row actions:

record JobRow(
String id,
String page,
LocalDateTime startedAt,
Duration duration,
Status status,
ColumnActionGroup actions
) {}
// In search():
new JobRow(
dto.id(),
dto.pagePath(),
dto.startedAt(),
dto.duration(),
new Status(mapStatus(dto.status()), dto.status().name()),
dto.status() == RUNNING
? new ColumnActionGroup(new ColumnAction[]{ new ColumnAction("cancel", "Cancel", IconKey.Close.iconName) })
: new ColumnActionGroup(new ColumnAction[]{ new ColumnAction("retry", "Retry", IconKey.Refresh.iconName) })
)

See Query services and UI rows.


Triggering a full site rebuild takes time. SSE streams progress back to the browser:

Action.builder()
.id("rebuild-all")
.sse(true)
.confirmationRequired(true)
.confirmationTexts(new ConfirmationTexts(
"Rebuild entire site",
"This will re-generate all pages. Continue?",
"Yes, rebuild",
"Cancel"
))
.build()
@Override
public Object handleAction(String actionId, HttpRequest httpRequest) {
return ssgWorker.rebuildAll() // returns Flux<BuildProgressEvent>
.map(event -> new State(Map.of(
"currentPage", event.pagePath(),
"pagesProcessed", event.count(),
"total", event.total()
)));
}

Each Mateu page class is stateless — no mutable fields survive between requests. State lives either in:

  • The JWT (user identity, permissions)
  • The database (content, job status)
  • The browser (form state, via state and data contexts)

This allows horizontal scaling with no sticky sessions.


PatternWhere documented
Service-owned UI modulesReal-world: service-owned modules
Query services and UI rowsReal-world: query services
RemoteMenu aggregationNavigation and menus
SSE for long operationsActions: SSE
JWT-based authorizationSecurity
@EyesOnly per roleSecurity

  • Service-owned UI modules — the federation pattern used by the shell in this case study
  • Security — JWT forwarding and @EyesOnly in a multi-service setup
  • Testing — how to test the page classes shown here in isolation