Static publication serves explicitly public Inertia pages from a named storage disk. Each page has separate HTML and JSON objects. The origin keeps the route available when storage or a CDN is unavailable.
Declare a public page
Use the same checked props contract as RenderCache. Every field must be public. Register shared props by name and list every shared key as public. Authentication, errors, flash and credential props cannot be public. An opaque shared provider cannot prove its fields and refuses publication.
use ;
async
A group can opt in through router.public_page_group("/articles"). Each
handler still needs an Inertia::public_page::<Props>(pattern, component)
declaration. An unmarked route never enters the publication inventory.
share?;
public_shared?;
Public rendering uses no visitor session. It skips session and CSRF middleware, excludes session props and uses the dependency collector to refuse a handler that reads session, identity or secret context. A refusal names the route and cause. It fails the whole publication and keeps the previous inventory live. The failure and route appear in status. It does not stop the server listener.
Configure a disk and boot publication
The disk can be local, S3, GCS, Azure or any registered OpenDAL operator. Give publication its own disk root. Pruning owns that root and can remove objects that no retained publication references.
use ;
use ;
Register the source disk public-assets against the application's public
directory before this function. It supplies built assets and source images.
Use the same Inertia asset version for the page config and the publisher.
Asset versions reserve lease, manifest.json, lease.json, every name that
begins with _ and every name that contains /. Both publisher constructors
refuse these names because they overlap the publication control namespace.
Concrete parameterized paths can be listed with add_path. A missing public
path is also rendered and published when its first origin request arrives.
Server::run starts publication after TcpListener::bind. Health checks can
run while assets upload. Shutdown cancels rendering and staging. Once delivery
starts, it runs as owned work; graceful shutdown waits for the switch and
manifest commit or rollback. The configured timeout bounds this work, including
cleanup. A quarter of the timeout, up to ten seconds, is reserved for recovery.
Uploads have a configured concurrency limit. An upload failure leaves the live
manifest in place and logs an error.
The publish command runs the same pipeline as boot. Status reports the live revision, last outcome and pending work. No page body or credential appears in the status output.
Objects, versions and switching
Pages use _pub/<asset-version>/<revision>/<path>.html and
_pub/<asset-version>/<revision>/<path>.json. The root page uses index.
The extra revision keeps regeneration from overwriting the currently live
page. Fingerprinted assets retain their names and receive
public, max-age=31536000, immutable. Other assets, such as an SVG favicon,
receive bounded cache directives. Pages reference their versioned keys, so every referenced object exists before delivery begins.
Page objects never receive immutable. Their Cache-Control contains
s-maxage, stale-while-revalidate and stale-if-error. Browser max-age
is clamped to revalidate. Object entries retain content type and hash CSP.
Cookies are excluded. Vary may name only X-Inertia and X-Inertia-Version;
other fields and * refuse publication. Request cache directives compare
case-insensitively before storage lookup.
use Duration;
use PublishConfig;
let policy = PublishConfig ;
_pub/manifest.json records live and a versions map. Each version records
its asset version, publication time, retirement time and objects. Each object
records its storage path, public URL, SHA-256 digest, asset version, dependency
tags and response headers. Previous versions remain until their grace expires.
All versioned uploads finish before the manifest PUT. Unchanged digests skip
uploads. A CDN driver selects _pub/<asset-version>/<revision>/ as the page
origin path. Without a driver, stable <path>/index.html and
<path>/index.json aliases are written one at a time after the manifest commit.
The manifest is the origin switch; stable bucket aliases are a post-commit mirror that converges.
Publishers in separate processes
Publication and pruning acquire an epoch record under _pub/lease/ before
reading the manifest or planning work. Keys use a zero-padded 20-digit unsigned
integer, such as _pub/lease/00000000000000000001.json. A record contains
owner, epoch and expires_at_ms. The highest epoch is the current lease;
foreign keys are ignored and no lease key is served by the origin.
Installing static publication reserves the /_pub/lease/ URL prefix for
GET and HEAD requests. Such a request below it receives 404 before application
routing, so a GET or HEAD application route declared there is never reached.
Other methods are not intercepted.
If no record exists, acquisition atomically creates epoch 1. An unexpired
highest record refuses another publisher. An expired record permits atomic
creation of the next epoch. A competing creation of that same key refuses as
held. Acquisition never deletes or replaces a record. Takeover needs only
write_with_if_not_exists or write_with_if_none_match and works the same on
every disk. Every publisher sharing the disk must use this epoch protocol.
Fs lease creation uses the registered-disk confined exclusive-create primitive
at the operator's root, including for application operators with atomic_write_dir.
Application operator layers still handle publication data, lease reads and release;
lease creation uses the confined operator directly. A root that cannot support
this primitive refuses setup and names Storage::register_fs in the diagnostic.
Verification lists the chain and checks the highest epoch, owner and acquired expiry. Release overwrites only the holder's own record with expiry zero, so the next acquisition advances the epoch instead of reusing its key. Bounded Drop cleanup does the same; failed release leaves expiry as the fallback. Prune verifies ownership before deleting records below the highest, which it always retains. All other keys in the lease namespace stay outside orphan cleanup.
The manifest retains its separate conditional write: write_with_if_match
uses the observed manifest ETag when available; otherwise a byte comparison
detects a changed manifest. Atomic epoch creation does not require conditional
replacement. A process that ignores the lease must not write the publication
disk.
Hosts must use NTP and keep their clock difference below
STATIC_PUBLISH_CLOCK_SKEW seconds, which defaults to 10; non-integer settings refuse publication setup. A holder stops using
its lease at expires_at - skew; a taker waits until expires_at + skew.
Rollback verifies ownership first. If ownership changes or the holder's usable
window expires, it leaves the CDN to the new owner and records the failure.
STATIC_PUBLISH_CLOCK_SKEW=10
Mirror writes, prune deletions and prune manifest commits verify ownership before each operation; a lost lease stops maintenance and reports the current owner.
Run the same commands with the application's console binary, from the server deployment or a console process:
The publisher releases its lease before bounded status persistence, so a slow status write cannot consume the lease release budget.
A refused lease appears in status. Boot continues serving. An interrupted
manifest PUT is reconciled by reading the durable manifest: the attempted live
version means success; the predecessor means failure and a fenced rollback.
A failed read or a third version records outcome: "unknown", previous_version
and attempted_version in status and leaves the CDN unchanged. The next
publication reads the manifest fresh and checks CdnDriver::selected_version
before rendering. A different or unknown selection calls switch_version for
the manifest's live version, independently of cached status. CloudFront reads
its configured origin path; custom drivers may return unknown. The first
publication has no live version to restore.
Mutable assets and the fingerprinted dependencies in their relative asset graph
form one set at _pub/<asset-version>/_assets/<set-digest>/<public path>.
The set digest is SHA-256 over the JSON array of sorted public path and content
digest pairs. HTML and JSON reference set members at these paths, so relative
CSS URLs and module imports resolve inside the set. Page-only changes reuse the
set without uploading or copying assets. A changed set writes every member once,
using server-side copy for unchanged members when the disk supports it, or upload
otherwise. Fingerprinted assets also retain their canonical names.
Each manifest version records asset_set_digest and a page_asset_sets map of
page URLs to set roots. Omitted pages keep their set references in a partial
publication. Prune removes a set directory only when no retained version
references it; the live version and retired versions inside grace retain theirs.
CDN drivers
CdnDriver is the single publish-side interface. It switches a version,
advertises purge capabilities and purges tags, URLs or a prefix. The in-memory
driver records operations for tests. An application can add Cloudflare or
Bunny by implementing this same trait.
use Arc;
use ;
use MemoryCdnDriver;
let driver = new;
let publisher = new?
.with_cdn;
CloudFront uses AWS signed requests with the same credential provider chain
as the S3 disk. It reads the complete distribution configuration and its ETag,
changes the selected origin's path, sends If-Match, waits for deployment and
creates invalidations. Static keys can be passed from S3Config; otherwise
the AWS profile or instance role supplies credentials.
The default cdn-cloudfront feature supplies the driver independently of SQS.
Register the publication disk and construct the driver from its configuration:
use ;
use ;
use ;
use CloudFrontDriver;
public-pages is the distribution's origin ID. An S3 root of pages selects
/pages/_pub/<version>; the constructor refuses ambiguous root segments. The driver preserves other
origins and configuration fields. A failed switch or manifest write restores
the previous origin version, including the original distribution configuration
when the first publication fails. Rollback failures appear in the publication
error. A custom driver implements rollback_version to restore its initial
configuration as well.
Configure the distribution to map /about to /about.html and
/about/index.json to /about.json. Send fingerprinted asset paths and /_pub/* asset paths to the
asset origin without the page version prefix, retaining the S3 root prefix. An origin-path switch changes
the prefix; it does not create these viewer-request rewrite rules.
S3 returns custom headers as x-amz-meta-*. Those names do not enforce CSP.
Publish the supplied viewer-response function once. Write its source with:
use RESPONSE_HEADERS_FUNCTION;
Create and publish the function with the AWS CLI:
Associate its ARN with the viewer-response event on each cache behavior
that uses the public page origin. Pass that ARN to
with_response_headers_function. The driver refuses a switch if any matching
behavior lacks the function. The function replays every validated recorded header, including Content-Disposition,
Reporting-Endpoints and HSTS, then removes the metadata headers. Headers the
viewer-response function cannot carry refuse publication before switching.
The origin applies HSTS only for trusted HTTPS delivery. It blocks scripts in an HTML object
that has lost its CSP metadata. This follows the
CloudFront response event format.
Storage writes delivery policy headers as metadata: Cache-Control, CSP, Vary, HSTS, reporting, content type protection, referrer and permissions policies, cross-origin policies and validated application headers. Content-Type is native storage metadata. Content-Length, ETag and Content-Encoding belong to the served representation; the CloudFront function preserves them after compression.
A local static server or another CDN must likewise serve the manifest's
headers as HTTP headers. A plain S3 website exposes custom headers only as
metadata; place a header bridge in front of it before serving public documents.
The Suprnova origin fallback already serves the headers directly. Header restrictions
follow the AWS edge function contract.
Fingerprint classification uses the same default file-stem pattern as StaticFiles:
a hyphen followed by eight base64url characters with a digit, uppercase letter,
underscore or hyphen. Names such as brand-background.svg remain mutable.
Select another build convention explicitly:
use ;
let publisher = new?
.fingerprint?;
CSP and private requests
Nonce documents cannot be published. Select the public hash mode described in Security Headers. It hashes the trusted document's executable inline scripts. Public pages contain no nonce attributes. Private pages retain their fresh per-request nonce. A later injected script does not gain a hash source.
let headers = balanced.hash_public_pages;
register_global_middleware;
All three starter kits install the same static HTTP client.
The document lists public page JSON URLs. Public navigation fetches these
objects while preserving the canonical page URL. Partial reloads, polling,
deferred and once props, and mutations use the live URL with
__suprnova_origin=1 and Cache-Control: no-store. A version mismatch reloads
the document. Flash and validation errors arrive through this private path.
The client reads XSRF-TOKEN. If the cookie is absent before a mutation, it
makes a same-origin private GET first. The existing CSRF middleware issues the
cookie and accepts its X-XSRF-TOKEN header. Public objects contain no CSRF
token. See Inertia Responses.
Regenerate after writes and by time
Generation writes keep RenderCache's transactional advancement. A successful framework commit queues affected dependency tags. Rollback discards the work. The worker coalesces queued tags and renders dependent HTML and JSON through the original publication pipeline. It purges through the installed driver after the switch. Without a purge driver, the edge lifetimes bound staleness. The configured revalidation interval also refreshes pages by time.
Publisher::publish and publish_with replace the complete inventory, so
removed routes and assets can retire. publish_partial and
publish_partial_with retain omitted pages and assets during a targeted
regeneration. Boot, the console command and timed revalidation publish the full
inventory; dependency regeneration and first-visit fallback publish a subset.
use Arc;
use ;
Bind the application's Arc<dyn BroadcastHub> before installing publication to
carry committed dependency tags from standalone workers to serving nodes. The
publication subscriber uses the same configured hub transport as other framework
broadcasting and queues each received tag for the serving worker. For example:
use Arc;
use ;
let hub: = new;
;
Use a cross-process hub driver in a worker deployment. Without a configured hub, notifications remain process-local and the revalidate interval bounds freshness. A disconnected or lagging hub also falls back to timed revalidation and logs the failure. Only dependency digests travel through the hub, never rendered props.
Prepare images
Publication resolves local image URLs against the document path. Posters, image preloads and other collected raster references use the same preparation path. Without the media feature, raster references refuse publication with a route diagnostic. It emits fingerprinted WebP objects at quality 85 and keeps a metadata-stripped original fallback. Picture sources preserve responsive descriptors, media and sizes. Quality must stay between 85 and 100. AVIF requires explicit enablement and the ImageMagick driver with an AVIF codec.
use ImageOptions;
let mut images = default;
images.keep_png.insert;
publication.images?;
data-static-keep-png, configured keep_png paths, linked PNG icons and web
manifest PNG icons use oxipng Maximum with Zopfli. Metadata is stripped from
every prepared raster. Absolute URLs on the application's public origin use the local source disk.
Raster data URLs are decoded and published as metadata-stripped objects, with
modern variants and a rewritten fallback just like local images. An invalid
raster data URL refuses publication with the route and cause; the origin keeps
serving the page. Third-party images are neither transcoded nor published.
Linked stylesheets are build outputs published byte for byte; their images belong
to the build, including small images Vite inlines below assetsInlineLimit.
Raster data URLs and same-origin raster url(...) references in document style
attributes or <style> elements refuse publication with the route and element;
move the image into an <img>, a <picture> or a stylesheet file.
Non-raster data URLs stay unchanged. SVG and ICO files
keep their existing representation. See Images.
The collector reads the complete Vite inventory before image preparation. A raster referenced only by a Vite entry or CSS keeps its key and its original format. The media pipeline strips its metadata and recompresses retained PNGs with the Maximum preset. Without the media feature, these dependencies refuse publication with the route and the reason.
Prune retired objects
Every successful attempt prunes, including an unchanged inventory. The same pass is available on demand. Reports list removed and kept keys. Live objects, stable aliases and versions inside the grace period stay protected. Grace starts when a version stops being live. Prune commits removal of expired version records before any object deletion. A failed deletion leaves orphans rather than dangling manifest references. Set reuse consults only versions still in the manifest. The next prune sweeps unreferenced asset-set directories, including failed staging sets, within the publication deadline. Other objects with no reliable age stay protected. A prune failure does not remove the live version or undo its switch.
Why Suprnova diverges
Laravel does not publish Inertia routes to object storage itself. Packages
provide that behavior. Suprnova uses its typed props declarations, transactional
dependency collector and named disk drivers. Separate HTML and JSON keys avoid
depending on an object store varying responses on X-Inertia.
The origin trusts a forwarded HTTPS scheme only from the proxies named in
APP_TRUSTED_PROXIES; APP_TRUSTED_PROXIES is empty by default, so a deployment
behind a TLS-terminating proxy must list it or HSTS is never sent.
