XTend Documentation
Enable dark mode

XTend Developer Center

Build with XTend today

Page runtime for Node and Laravel

Node and PHP/Laravel are independent SSR hosts. Node retains its compiler access, JavaScript services and streams. Production Laravel uses prebuilt RMT artifacts and the Composer package; it needs no Node process after the build.

The page runtime extends the existing Node and PHP adapters. It uses an XTend protocol without an Inertia dependency.

Build and contracts

xt pages build --root /path/to/app --target both --json uses the existing vNext compiler and import resolver. Render targets are node, php and both; --host laravel selects the Laravel output path. Existing CLI server targets remain available.

Declare sources in xtend.pages.json:

{
  "schema": "xtend.page-build.v1",
  "target": "both",
  "pages": {
    "Orders/Detail": {
      "source": "pages/order.rmt",
      "inputs": ["orders.detail"]
    }
  }
}

The build produces xtend.page-manifest.v2 and TypeScript page/layout mappings. The default output is .xtend-build/pages.json, or bootstrap/xtend/pages.json for Laravel. Host props replace declared inputs; omitted inputs retain defaults. The project index links configuration, page names, RMT sources, render targets and artifacts. Composer vendor directories are excluded from project sources.

The portable projection is xtend.rmt.portable-render.v2. It executes compiler descriptors without a PHP RMT parser. Text nodes render strings, numbers and booleans; null, missing and structured values produce empty text. Bindings, attributes, lists and explicit formatting handle structured data. PHP decoding preserves JSON objects and lists.

PHP supports the expressions listed by the portable renderer. Regex replacement and JavaScript functions, for example, are outside that target. A PHP build diagnoses these capabilities. Existing Node compiler and adapter access remains available.

assets.entry and assets.css contain same-origin URLs. Alternatively, "vite": {"manifest": "public/build/manifest.json", "entry": "resources/js/app.js"} reads the entry and its CSS dependencies from a completed Vite build. vite.base defaults to /build/. assetRoot is the local public directory, defaulting to public. Asset, configuration, source and runtime fingerprints contribute to the build version. Rebuild page artifacts with the package actually deployed after a package update.

Node integration

The runtime exports @ccslabs/xtend-rmt/node-page-host; the full package exposes @ccslabs/xtend/rmt/node-page-host.

import { createNodePageHost } from '@ccslabs/xtend-rmt/node-page-host';
import { Prop } from '@ccslabs/xtend-rmt/page-contract';

const pages = createNodePageHost({
  manifest,
  createContext: (request, signal) => host.requestContext(request, signal),
  resolvePage: async context => ({
    page: 'Orders/Detail',
    props: { 'orders.detail': Prop.once(() => host.loadOrder(context)) }
  }),
  validate: (context, fields) => host.validateOnly(context, fields),
  appServiceHost,
  cleanup: context => host.release(context)
});
// In the existing HTTP host:
// if (!await pages.handle(request, response)) nextHandler(request, response);

host, manifest and appServiceHost are application integration points. createNodePageHost() starts no server. The request context supplies an opaque contextKey bound to the application, user and tenant, and optionally a csrfToken. Authentication, routing, upload limits and business validation belong to the HTTP host. An existing createNodeAppServiceHost() instance can provide appServiceHost.

Resolvers return a page, { redirect }, or { download } carrying a Node/Web stream. Providers receive an abort signal. Request deadlines and cleanup budgets are configured separately; abort signals cannot interrupt synchronously blocking JavaScript callbacks.

createNodePageRouteManifest() consumes named routes explicitly obtained from the host router. Laravel independently exports its Route Collection. Both use xtend.page-routes.v1 with a distinct host field.

Laravel integration

scripts/build_laravel_package.js /absolute/fresh/output assembles ccslabs/xtend-laravel with its canonical PHP files. Install that artifact through a Composer repository. Local package tests use a Composer path repository with symlink: false. Publishing to Packagist remains a separate action.

use Ccslabs\XTend\Facades\XTend;
use Ccslabs\XTend\Data\Prop;

XTend::share('account', fn ($request) => $request->user()?->only('id', 'name'));
return XTend::render('Orders/Detail', [
    'orders.detail' => ['text' => $order->name, 'tone' => 'neutral'],
    'statistics' => Prop::defer(fn () => $order->statistics(), 'statistics'),
]);

Register Ccslabs\XTend\HandleXTendRequests in web middleware after session middleware. Composer discovers the ServiceProvider. FormRequests, redirects, error bags, flash and UploadedFile retain Laravel's lifecycle. API routes without this middleware keep their JSON behavior. Live validation additionally uses HandlePrecognitiveRequests.

xtend:install publishes configuration. xtend:routes exports names configured in xtend.routes. xtend:doctor checks the manifest, render target and PHP source hashes from xtend.php-package-sources.v1. Override xtend.root_view to customize the Blade root. A compiler bridge is an explicitly configured development tool; production pages render artifacts.

Shared browser lifecycle

import { createPageClient } from '@ccslabs/xtend-rmt/page-client';
import { createPageForm } from '@ccslabs/xtend-rmt/page-form';

const client = createPageClient({
  initialPage: JSON.parse(document.getElementById('xtend-page-data').textContent),
  encryptHistory: true
});
await client.start();
const form = createPageForm({ client, errorBag: 'edit', defaults: { name: '' } });
form.bind(document.querySelector('form'), { action: '/orders/1' });

xtend.page-response.v1 combines page identity, props, layout, head, version and context. The client handles internal links, superseded visits, Back/Forward, scroll regions (data-xtend-scroll) and anchors. data-xtend-native leaves a link to the browser. Forms expose dirty, processing, success, error and upload state; stale responses cannot reset newer edits. bind() accepts native forms and x-form submit events.

Declare layouts through layouts and page.layout. An RMT layout build names exactly one compiled node through outlet; this position contains the page. Bind resources to pages or layouts with registerResource(). User/tenant changes also release persistent DOM.

Prop.lazy, Prop.defer, Prop.merge and Prop.once have JavaScript and PHP APIs. reload({ only: [...] }) and deferred groups evaluate selected providers. prefetch(), poll() and whenVisible() share bounded loading/cache management. A prefetched visit rechecks host context and does not consume flash data early. loadMore() uses pagination.next, previous and props; Laravel's Ccslabs\XTend\Pagination::from() accepts offset and cursor paginators.

Use createNodePageValidator() or createPrecognitionValidator() as a form validation provider. For rotating sessions, supply csrfToken: () => client.page.csrfToken. client.optimistic() applies immediate changes; an older failure cannot undo newer state. Instant Visits and View Transitions are explicit visit options and honor reduced motion.

History stores selected remember() data and scroll positions. Passwords, token fields and files are excluded. Optional WebCrypto encryption does not replace authorization. invalidate() clears cached and remembered state. Background requests report deployment changes without forcing a document replacement over unsaved forms. Initial Resume uses the existing signature/integrity contract and its controlled hydration fallback.

Acceptance and operation

The canonical runner defines ssr-pages:node, ssr-pages:laravel and ssr-pages:laravel-browser. Executable reference hosts and the same CRUD browser flow live in tests/ssr-pages/. The Laravel matrix covers 12/PHP 8.2–8.5 and 13/PHP 8.3–8.5, with browser evidence for each Laravel major. Local PHP 8.3 results do not replace that CI matrix.

PHP-FPM/proxy infrastructure timeouts must bound synchronously blocking providers; frame and cleanup budgets cannot interrupt a running PHP function. Set XTEND_PHP_FPM_BINARY=/path/to/php-fpm before the Laravel browser profile to run it behind an isolated FPM pool and FastCGI proxy.

Run node tests/ssr-pages/measure_resources.js .xtend-test-results/ssr-pages-resources.json for 50 render iterations of the same list in both runtimes and a browser bundle measurement. PHP comparisons use scalar identities; compare list entries through their declared keys. String slices must preserve Unicode scalar boundaries.

The Node test path requires no PHP. The additional ssr-pages-php suite checks shared render parity once per PHP/Laravel environment. Assign router.pageClient = client before attaching an existing x-router, and call client.start(); the page runtime then owns links and history.

createPageClient({ initialPage, viewTransitions: true }) enables page animations through the existing RMT AnimationEngine and XUtils. data-xtend-page-transition marks the animated content inside the page root, keeping a persistent shell still. startMaracaPageApplication() forwards the same option. Background reloads do not animate; unavailable animation support, missing XUtils and reduced motion use the regular update. Navigation does not wait for animation; a new visit or disposal stops active effects. Visits can opt out with { transition: false }. A custom transition: async update => { /* … */ await update(); } host callback remains available and takes precedence; { transition: true } activates it.

Links and native GET forms accept data-xtend-preserve-scroll="true", data-xtend-preserve-state="true" and data-xtend-transition="none" ("false" is also accepted). Submit buttons can override the form settings. Product variants can therefore update the URL, data and metadata while retaining scroll position and remembered state. These attributes apply to internal navigation; the original links and forms remain usable without JavaScript.

Maraca pages and XTend.store

createMaracaPageClient() from @ccslabs/xtend/maraca/page-client connects the page client to a Maraca bundle. Each page or layout manifest may declare maraca: { entry: "/build/maraca/xtend.maraca.mjs" }. The bundle and portable projection use the same compiler facts; createRmtCompilationSession() shares analysis within a build. Configuration and referenced assets contribute to the build version.

The page runtime owns URL, history and transport; Maraca owns UI state and DOM commits. Each root has one controller, and unchanged shells survive navigation. Superseded activations are discarded and navigation releases remote surfaces. The generated entry uses startMaracaPageApplication() from @ccslabs/xtend/maraca/page-bootstrap: capture is installed before loading the composition, P-256 signatures use the public build key, and rejected integrity checks use the existing single hydration fallback.

The shared head contract also accepts canonical links (tag: "link", rel: "canonical", HTTP(S) URL) and identified JSON-LD records (tag: "json-ld", key, data). Node, PHP and browser consumers deduplicate by identity. Script content is serialized safely; event attributes and executable canonical URLs are rejected.

XTend.store demonstrates this integration with Laravel, a guest cart and a separate PHP DemoPay provider. The Node SSR adapter and createNodePageHost() remain independent integration APIs. The store is an additional reference application.

Maraca pages also route native GET search and filter forms through page navigation. The base page API enables this with forms: true; navigationAction can close declared RMT surfaces before a visit. POST forms retain their host or RMT contract.

Compact transport and conditional views

Node hosts can opt into compactResponses: true; Laravel uses compact_responses => true. HTML bootstrap data then uses xtend.page-wire.v1: a response-local table stores identical JSON objects and arrays once. Each child reference is an index into that table. encodePageWire() and decodePageWire() are exported from the page contract module. The existing page response and signed resume envelopes are reconstructed before validation and signature verification; their contracts do not change. References never cross requests or sessions. The decoder rejects unsafe keys, cyclic/foreign references, excessive depth, node counts and logical expansion.

The page client negotiates this transport with X-XTend-Page-Wire: 1. Older JSON consumers continue to receive ordinary page responses. For negotiated visits with a portable artifact, hosts send page inputs and the render artifact without generating another SSR envelope. Initial document requests still render complete HTML and signed resume data. This option requires a compatible bootstrap; custom Laravel root views should serialize $pageData (falling back to $page for older integrations).

Use portable conditional descriptors to materialize only the current view. The DOM renderer accepts both conditional and its existing when spelling; compiler-resolved portals may target a conditional branch. State can remain in the RMT model while controls are absent. An unchanged branch reconciles its existing nodes. This reduces DOM and page data, but does not automatically split a shared Maraca bundle into separately downloaded code chunks.

Laravel's optional style_nonce => true adds the document nonce to style-src and removes its unsafe-inline source. The store also sets style-src-attr 'none'. Trusted XTend component style templates obtain the bootstrap nonce; arbitrary HTML is never automatically authorized. Custom components must supply equivalent trusted style handling or external stylesheets before enabling this policy. Browser tests exercise both blocked style injections and the complete payment flow.

Page lifecycle and surface lifecycle

A page visit ends page-owned requests; layout-owned resources may remain alive according to their owner. Inside a Maraca page, the automatic Abort Boundary additionally protects every managed surface from late worker, hydration and DOM commits. Fast close or focus requires neither a new page artifact nor completion of business services.

Presentation epochs do not replace data versions or business-service cancellation. Shared prewarming may retain valid data. Custom integrations check the surface token after asynchronous responses and immediately before visible commits. Client-only Maraca uses the same guards without SSR.

Initial resume payload

Initial portable Node and Laravel documents built with initialResumeSchema: "xtend.page-initial-resume.v1" in server_prerender_resume send rendered HTML once in the DOM. Their separate initial envelope retains the descriptor fallback and signed resume state but omits duplicate markup.html/textContent from descriptor chunks. The general SSR response, HTML-only pages, hydration mode and subsequent page API responses keep their existing contracts. Integrity data and canonical signed state are unchanged. A rejected signature can still recover through the portable artifact/descriptor. State is not generically pruned: reducers, event bindings and recovery may consume it.

Migration to XTend 0.9

Rebuild pages and browser assets together. New builds write page-manifest v2, portable-render v2 (with renderer v2), and page-response v2. Compact Page API responses use the lossless page-wire v2; v1 remains paired with response v1. The initial resume envelope has its own reader, decodePageInitialDocument, and never passes through the Page Wire codec. Its SSR/chunk records omit general response kind/version and carry only markup.descriptor; full API responses still include HTML and text content.

In renderer v2, strings in attributes/attrs are literals: type: "search" stays unchanged even when the model has a search key. This applies to the DOM renderer, Node SSR, and portable Node/PHP projections using the v2 contract. Migrate implicit bindings such as value: "search.query" to value: "$model.search.query", then rebuild. Other explicit forms are $item.path, ${model.path}, and expression records; the general value/property resolver is unchanged. Existing portable v1 artifacts keep scalar implicit binding behavior. Direct renderer integrations select rendererSchema: "xtend.epic18.rmt-dom-descriptor-renderer.v1" explicitly while migrating and receive an informational deprecation diagnostic. Structured native/ARIA attributes are rejected in both paths; component properties and structured data-* attributes remain available.

Existing executable examples and regression tests: Node SSR attribute literals, bindings, and interpolation, portable Node/PHP projection, and v1/v2 compatibility including shipped DOM renderers.

Read SSR coverage with getRmtSsrCoverage(result) from the Node SSR adapter. The record is stored in fabricTelemetryHints.coverage, independently of hydration v1. Code using the unreleased hydration.coverage addition must migrate. Coverage measures emitted markers; verify actual browser resume/recovery separately.

Keep the existing v1 manifest and matching old client for a staged rollout. New clients read old full documents, v1 wire responses and the new envelope. A v1 manifest rejects v2 artifacts; never change an artifact's ID by hand. V2 manifests may use existing v1 artifacts, retaining their full initial document transport. Unknown contracts fail before initial DOM/state changes.

Legacy contracts remain supported for at least 0.9 and 0.10; removal is allowed only in 1.0 or later after a documented migration. Packaged JSON Schemas live at @ccslabs/xtend-rmt/contracts/*.json. The architecture decision records the complete contract graph and release gates.

Opt-in public resource cache

createPageResourceCache from @ccslabs/xtend/rmt/page-resource-cache stores only explicit JSON resources in memory, with TTL, LRU and UTF-8 byte limits. Reads and writes are copied; TTL never slides. stats() exposes counts, never keys.

Set PageClient resourceCache: {resolve, project, clear}. resolve(url, context) receives {page, signal, cache} and must return null for unapproved routes. project(resource, context) returns a complete same-origin, same-version and same-context page with no SSR envelope or CSRF token, using loaded page artifacts. Use visit(url, {cache: 'reuse'}) or data-xtend-cache="reuse" for reuse; the default is reload. The policy also receives reload visits to delete a target. History restoration requests reuse only when a policy is installed. Pages with CSRF tokens never enter this path. Prefetch authorization revalidation is unchanged.

Dispose, context/version changes and invalidate() call clear. Errors in a resource policy fall back to normal transport; superseded resolutions cannot commit. Never cache DOM, SSR/resume envelopes, event replay, credentials, editing drafts or authenticated pages. Revalidate permissions through a metadata-only App Service when needed. The Maraca bootstrap forwards resourceCache and fetch.

See the two-view Maraca App Services example.

When compact responses are enabled, the initial resume envelope uses xtend.page-initial-wire.v1. decodePageInitialDocument expands its bounded response-local reference table before validating the initial envelope. Signed state and descriptors are unchanged; ordinary PageResponse decoding still rejects reduced SSR chunks.

(c) 2026 - CCS Networks | Powered by XRouter PHP Extension