Skip to content

Architecture / A practical architecture guide

Server Side Rendering 2.0

By ·

PHP began with a remarkably direct idea: the server knows the data, so let it build the page. Decades of richer interfaces have taught us what that model needed next. Semitexa combines one authoritative template with deferred blocks, live delivery, and explicit PHP boundaries. This article is a working example of that architecture.

Try the argument as you read. Change the delivery option to compare three render paths, interact with a block that arrives later, and connect to a real stream of server events. The examples use the framework running this page.

Before SSR had a fashionable name, PHP was rendering pages

Server rendering is part of PHP's foundation. The PHP project's history traces its early development through form handling, database interaction, and syntax embedded in HTML. The request arrived at the server; PHP evaluated the dynamic parts; the browser received a document.

A familiar template from a later generation of PHP applications looked like this. HTML supplied the structure, and a block object supplied the values:

Classic PHP template · HTML with PHP expressions
<article class="product-card">
    <h2><?= $block->getTitle(); ?></h2>
    <p><?= $block->getDescription(); ?></p>
    <strong><?= $block->getFormattedPrice(); ?></strong>
</article>

The browser never executes $block->getTitle(). PHP has already replaced that expression with output before the response arrives. The PHP manual describes this mixture of HTML and PHP directly. The block object is a familiar architectural example, not a claim that the earliest PHP versions used this class design.

The short example shows the old shape of the code. For a title supplied as plain text, an actual application must also escape output for its HTML context:

PHP · escaping a plain-text title
<h2><?= htmlspecialchars($block->getTitle(), ENT_QUOTES, 'UTF-8'); ?></h2>

This model had a valuable property: one template described what the user would see. Links were links, form submissions reached the server, and the page contained useful content from the beginning. Its problems came from how applications were structured and updated. Templates could grow SQL queries and business decisions. A slow dependency could delay the entire response. Updating a small region often meant loading the whole page again.

Those limitations were real. The useful lesson was to improve the boundaries and the delivery of the page while preserving the clarity of a single rendering definition.

We gained richer interfaces—and another application to keep correct

Separate frontend applications solved important problems. Browser code could manage complex interactions, update local state, and provide experiences beyond a form followed by a full reload. APIs made it possible to serve multiple clients. Specialist teams could work and release independently.

But a product feature rarely stops neatly at the API boundary. A new discount changes a server calculation, a preview total, an eligibility message, and a checkout button. What looked like one behavior becomes several tasks across repositories, languages, tests, and release schedules. The teams need a shared understanding of every state, including the states that an API document forgot to name.

Some organizations manage this with two specialist teams and careful coordination. Others ask every developer to work confidently in both stacks. Neither arrangement removes the underlying cost: people still have to understand two execution environments and keep the same product behavior consistent across them. Smaller teams often feel that cost particularly sharply.

The expensive leak is a decision copied across the boundary

Consider a $160 order with a 10% member discount and $12 express delivery. The backend calculates $156. A browser preview must display the same answer. If it reconstructs the discount rules from a customer flag and an item list, it has become another implementation of pricing. A change to the order of operations can produce two believable totals.

The same leak appears when a client infers permission from a role label, invents a workflow status, or guesses whether a refund is allowed. Client validation can improve feedback, but the server still has to enforce the rule. A hidden button cannot authorize an operation. A plausible progress animation cannot establish that a job has finished.

The goal is a clear owner for each decision. Keep pricing, permissions, and state transitions in application services. Pass their results to the view. Keep the view definition in one template. Transport should deliver those results without creating a competing model of the product.

The template is the single source of truth for the view

In Semitexa, a Twig template can remain the authoritative definition of a region whether that region appears in the first HTML response or arrives later. You do not have to maintain a PHP view and a separate JavaScript component that reproduce the same markup, labels, conditions, and empty states.

That statement has a precise scope. The template owns presentation. A PHP service owns business decisions. A repository or external system owns persisted facts. Putting every rule into Twig would recreate the coupling that early PHP applications struggled with. The benefit comes from giving each concern one clear home.

QuestionAuthoritative placeWhat the browser receives
What does this order cost?The PHP quote policyThe calculated amount
How should the quote appear?One Twig templateHTML, or that published template and its data
When is a slow region ready?The server operation completingA deferred delivery frame
Which carousel item is visible?A small browser interactionNo second pricing or stock policy

Here is part of the actual quote template used by this page. Its inputs are already decided. The template describes the labels and where values appear:

Twig · one view reused by all three quote panels
<h3>{{ quote.deliveryLabel }}</h3>
<dl>
  <div><dt>Member discount</dt><dd>{{ quote.discount }}</dd></div>
  <div><dt>Delivery</dt><dd>{{ quote.shipping }}</dd></div>
  <div><dt>Total</dt><dd>{{ quote.total }}</dd></div>
</dl>

The quote above uses partials/blog-ssr2-quote.html.twig for display and BlogQuotePolicy for its calculation. The Framework demo uses its own shared view and DemoSsrQuotePolicy for the deferred examples. In each case, the browser receives the server's decision rather than recalculating the discount.

Server Side Rendering 2.0: keep the page, improve how it arrives

“Server Side Rendering 2.0” is the architectural idea of this article, rather than a protocol version. Semitexa keeps the request-to-HTML path explicit: a typed payload represents input, a handler coordinates application services, a resource carries the result, and Twig renders the view.

Request→ typed payload + handlerPHP rules
Decided values→ resource + TwigFirst HTML
Slow operation finishes→ deferred SSE deliveryIts region appears
Business decisions remain on the server. A region can arrive later without acquiring a separate frontend implementation.

One slow region no longer has to hold the whole document hostage. A deferred slot reserves space with a skeleton. Semitexa resolves that slot after the shell, then delivers its result. Under the coroutine runtime, independent work can proceed concurrently; a real blocking call or a shared bottleneck still needs attention. Deferring work changes when the page can become useful, not the amount of work a database must do.

The quote below makes the immediate response visible. The linked Framework demos show how deferred regions and an interactive product rail arrive later, without moving the business rule into the browser.

Demo 1: a PHP policy renders the first response

Choose standard or express delivery. This ordinary GET form rerenders the quote on semitexa.com using the same PHP calculation and Twig template. For deferred delivery, open the live Framework demo.

Running in this article

Member order · $160 subtotal · 10% discount

The server calculates the amount; Twig displays it.

01 / First response

Standard delivery

Order subtotal
$160.00
Member discount
-$16.00
Delivery
Free
Total
$144.00

The 10% member discount is applied and standard delivery is free for this order.

Standard totals $144; express totals $156. The browser never calculates the discount. The form works without JavaScript.

What each delivery path proves

First response: the page handler calls the PHP policy and Twig renders the view on the server. Deferred HTML: a slot handler can render a view later and send finished HTML over SSE. Deferred template: the server can supply decided values and a reference to a published Twig template for the supported client renderer. Explore those latter paths in the Framework rendering demo.

The third path is especially useful when discussing a single source of truth. Browser rendering does not have to mean a second, independently maintained view. Semitexa's template mode reuses the declared Twig source. It supports a constrained template feature set, so complex server helpers belong in the PHP handler or in HTML mode. Values sent to this mode are visible to the client, just like any other browser payload.

PHP · the calculation used for the quote
// DemoSsrQuotePolicy::quote() — amounts are integer cents.
$subtotalCents = 16000;
$discountCents = intdiv($subtotalCents, 10);
$shippingCents = $delivery === 'express' ? 1200 : 0;
$totalCents = $subtotalCents - $discountCents + $shippingCents;

This is a condensed excerpt of the demo policy; it also formats the amounts and supplies display labels. The page payload accepts the delivery choice, and the deferred handler reads that choice from the page context. In a real checkout, the server would load and authorize the order, calculate against current data, and revalidate on submission. Sharing a template does not freeze a changing business record in time.

Demo 2: a deferred block can bring its own interaction

The Semitexa deferred carousel has a PHP handler for product data and a Twig template for the cards. Once the block arrives, a small client module activates Prev and Next. Try it in the Framework demo: JavaScript selects what is visible, while the product values come from the server.

The fixture products are demonstration data, and the delay is intentional. Reload the Framework demo to watch its skeleton resolve and compare how regions finish independently.

A small declaration describes the delivery contract

PHP · the HTML slot declaration, with its second registration omitted
use Semitexa\Ssr\Attribute\AsSlotResource;
use Semitexa\Ssr\Application\Service\Http\Response\HtmlSlotResponse;

#[AsSlotResource(
    handle: 'demo_blog_ssr2',
    slot: 'ssr2_receipt',
    template: '@project-layouts-semitexa-demo/partials/blog-ssr2-quote.html.twig',
    deferred: true,
    skeletonTemplate: '@project-layouts-semitexa-demo/deferred/blog-ssr2-receipt.skeleton.html.twig',
)]
final class BlogSsr2ReceiptSlot extends HtmlSlotResponse
{
    public function withQuote(array $quote): static
    {
        return $this->with('quote', $quote);
    }
}

In the Framework demo, template delivery registers the same resource with a different slot name and mode: 'template'. Both registrations name the same quote template. A discovered #[AsSlotHandler] supplies the data. That page chooses where each slot appears:

Twig · place a deferred region
<section aria-label="Order quote">
  {{ layout_slot_deferred('ssr2_receipt') }}
</section>

This is useful well beyond a loading animation. A product page can return its title and purchase context while recommendations wait on another service. A dashboard can reveal a quick summary while a slower report resolves. A component can bring a chart module that paints a canvas from server values. Each region has a declared template and lifecycle, instead of a bespoke fetch route plus another hand-written rendering function.

Deferred arrival, periodic refresh, and events are different jobs

MechanismTriggerGood example
Deferred slotThe region finishes preparingA recommendation service returns
refreshIntervalA server refresh interval elapses on a permitted persistent streamA metrics snapshot checked periodically
Server eventThe server emits a change notificationA background task reports completion

A deferred declaration does not automatically subscribe a block to every domain event. Live updates need an explicit publication and subscription path, or an explicitly configured refresh interval. Making that distinction keeps the architecture understandable: “render this later” and “keep this fresh” are related capabilities with different lifecycles.

Demo 3: the server changes, and the browser hears about it

Server rendering can stay live after the initial document has arrived. An SSE connection remains open so the server can send an event when it is produced. The browser does not have to ask every few seconds whether something happened. Delivery still takes processing and network time; “immediate” means pushed after emission, without waiting for the browser's next polling cycle.

The Semitexa SSE showcase lets you sign in, connect, and watch a server notification arrive. A new scheduler.tick follows at each server minute boundary when the debug producer is enabled. Its timestamp is produced on the backend; the visible countdown only predicts the next tick.

This optional persistent stream requires sign-in and the demo producer requires debug mode. It is separate from the short deferred delivery shown in the rendering demo.

The event carries the server's answer

PHP · illustrative application event using the installed SSE delivery API
use Semitexa\Ssr\Application\Service\Async\SseAsyncResultDelivery;

// Inside a server-side producer, for an authorized subscriber session.
SseAsyncResultDelivery::deliverRaw($sessionId, [
    'event' => 'report.ready',
    'reportId' => $reportId,
    'sent_at' => gmdate(DATE_ATOM),
]);

Here $sessionId must come from the application's authorized subscriber mapping, and $reportId from completed server work. The snippet illustrates publication; it does not establish subscriptions or authorize a report. The built-in panel uses its own notification and scheduler.tick events so you can inspect a working producer.

For an application, the next step depends on the UI contract. A notification can consume the event fields. A resource response can be rendered on the server and delivered with its HTML through Semitexa's asynchronous result delivery. A subscribed collection can react to a scope invalidation and obtain an updated server projection. In each case, the event announces a fact already decided by the server.

Job finishes→ persist result + publishServer event
Application presentation path→ the declared templateUpdated view
SSE delivery→ browser applies resultVisible change
Wire the application's event-to-view path explicitly. The browser can show a completed report without independently deciding when a report counts as complete.

That is the bigger opportunity behind deferred blocks and SSE. A useful page can appear early, a slow block can join it later, and subsequent server activity can reach an already open page. The delivery mechanism evolves while the business rule and the authored view retain clear owners.

AI agents benefit from an architecture with fewer conflicting answers

An AI agent can generate a second price function very quickly. It can also produce a perfectly plausible patch to the wrong one. When a PHP calculation, a TypeScript preview, a server template, and a browser component all describe one checkout, the agent must discover every relationship before changing the behavior safely. Missing one is enough to create drift.

Now give the same task to an agent working on this article's demo: “change the member discount to 15%, and rename the delivery label.” The calculation has one home in DemoSsrQuotePolicy. The shared view has one home in blog-ssr2-quote.html.twig. The first response, deferred HTML, and template delivery can be compared against the same expected result. The task becomes easier to locate, explain, and verify.

This also improves collaboration between people. A designer can work on the Twig markup. A PHP developer can change the policy. A browser specialist can improve carousel behavior or chart accessibility without taking ownership of order calculations. Specialists still matter; the architecture makes their responsibilities easier to join.

Semitexa's typed payloads, handlers, resources, and declared slots expose these relationships to its inspection tools. The Project Graph helps an agent trace dependencies and assess impact before editing. Structural clarity narrows the search; verification still has to prove the outcome.

One template is an architectural advantage, not an automatic correctness guarantee. It removes a duplicated view definition. Keeping business decisions in services removes a duplicated rule implementation. Together, those choices make both human and agent changes easier to reason about.

Use the delivery mode that the region actually needs

Render inexpensive, essential content in the first response. Defer a region when waiting for it would materially delay the page. Keep a stream open when the product needs updates after arrival. A fast label does not improve by becoming a skeleton, and a static paragraph does not need a persistent connection.

For production, make the slow operation bounded, keep placeholders stable, and verify that a failed region can recover without breaking the rest of the page. Check proxy buffering for SSE, decide what reconnect means for your data, and authorize both the initial content and subsequent updates. A cached fragment must respect the user and tenant boundaries of the values it contains.

Test the paths your product promises: first HTML, successful deferred delivery, transport failure, crawler rendering, and JavaScript disabled. This page deliberately keeps the first quote and its form useful without client code. That does not mean an unresolved deferred region magically becomes live without a runtime.

Commerce pages, account screens, forms, content, and operational tools often benefit from this model because the server already owns their important decisions. Rich offline editors, graphics applications, and heavily local interactions can justify a larger client application. Semitexa lets a server-rendered product add live behavior region by region.

The original PHP strength survives: one understandable path from a request to a page. The new capability is that the page can arrive in stages and continue responding to server activity, with one template for its view and one authoritative implementation of its rules.

← Back to the Semitexa Blog