A deployment is a proof chain, not a file copy
A static site becomes trustworthy when its artifact is identified, staged, tested, promoted, and easy to reverse.
- problem
- A green local build does not prove that the public site is coherent. The wrong directory can be copied, a clean URL can fall through to the wrong document, an asset can arrive unreadable, or a reload can succeed while the browser still receives a broken release. Treating deployment as file transfer hides these failure modes until a reader finds them.
- scope
- A practical release contract for statically generated sites: build one candidate, give the bytes an identity, transfer them to a disposable stage, validate the router and representative pages, preserve the current release, promote the staged bytes through an explicit serving-layer boundary, and verify the public surface after reload.
- environment
- Static SvelteKit or similar output served by Nginx, a container, or another deterministic HTTP server. The pattern also applies to documentation sites, personal sites, and small internal frontends where a full deployment platform would be heavier than the application.
Assumptions
- The repository can produce a complete static artifact from a pinned or reviewable source revision.
- The operator has a staging destination and an authenticated path to the serving host.
- The serving layer can be syntax-checked and reloaded without replacing the host or mutating application data.
Limitations
- A digest proves which bytes moved; it does not prove that the source was correct, the content is safe, or the browser experience is accessible.
- The examples use generic paths and domains. They deliberately omit private hostnames, addresses, credentials, topology, and service-specific recovery commands.
- This is a static-release pattern, not a database migration strategy, a supply-chain attestation, or a substitute for security and accessibility review.
table of contents 7 sections
Build one candidate, then stop changing it
A release starts before the network. Run the tests, type checks, and static build against one source revision, then treat the resulting build directory as a candidate rather than a scratch folder. If a second build is created after the archive is made, there are now two releases and the operator has to guess which one was tested.
The exact commands vary by stack. The invariant is that the artifact comes from the same gate that produced the evidence. For a JavaScript static site, a minimal local contract looks like this:
set -eunpm testnpm run checknpm run buildtar -czf release.tar.gz -C build .sha256sum release.tar.gz > release.tar.gz.sha256Give the bytes an identity before they move
A commit identifies intent. A digest identifies the exact artifact. A deployment needs both. Record the source revision, artifact digest, target environment, and previous-release reference in a small manifest or receipt. A timestamp helps humans find the event, but it is not an identity: two different artifacts can share a minute, and the same artifact can be promoted more than once.
The digest is deliberately boring. It lets the receiving side answer a narrow question without trusting the transport narrative: do these bytes match the candidate that was tested? That question does not require a heavyweight platform, only a stable archive and a verification command.
| Field | Question it answers | Evidence |
|---|---|---|
| Source revision | What change was intended? | Commit or immutable source reference |
| Artifact digest | What exact bytes moved? | SHA-256 over the release archive |
| Target | Where was it promoted? | Named environment and serving root |
| Rollback | What was live before it? | Timestamped previous-release archive |
Stage the tree before touching the live root
Copying directly into the live document root collapses transfer, extraction, validation, and promotion into one irreversible-looking action. A temporary stage restores the boundaries. Transfer the archive, verify its digest, extract it to a new directory, and inspect the files there before the active root changes.
The stage check should be specific. Confirm the entry document exists, a representative deep route resolves to the expected generated file, the feed or sitemap is present when advertised, and assets are readable by the serving process. A directory listing is not a browser test, but it catches a surprising number of bad copies.
- build Tests, type checks, and static generation pass.
- identify Revision and digest are recorded.
- stage The candidate is extracted away from live traffic.
- validate Routes, assets, and serving config are checked.
- promote The previous root is archived before replacement.
- verify Public HTTPS and a hydrated browser are exercised.
Treat the router as part of the application
A static generator can produce the right files while the server still serves the wrong URL behavior. Clean routes, trailing slashes, fallback documents, feeds, redirects, and content types are part of the application contract. Test them against the staged tree and the serving configuration, not only against a local development server.
For Nginx, try_files makes the lookup order visible: check the requested file or directory, then redirect internally to the chosen fallback. That is powerful precisely because it is concrete. If a note route works only because a development server invents a fallback, the release is not ready.
The header-only commands below are reachability smoke checks. Pair them with representative body markers and a browser pass before calling the release verified.
# syntax/referenced-file gatenginx -t# header-only reachability smoke checkscurl --fail --silent --show-error --head https://example.com/curl --fail --silent --show-error --head https://example.com/notes/examplecurl --fail --silent --show-error --head https://example.com/feed.rssA reload is an operation, not evidence
Reloading a server only says that the process accepted an instruction. It does not say that the new root is complete, that the edge points at the intended origin, or that client-side hydration can load its assets. Verification must cross layers: inspect the serving process, request the public HTML, follow one deep route, fetch machine-readable surfaces, and load a representative page in a real browser.
The browser pass is not ceremony. It catches stale asset manifests, failed module requests, console exceptions, duplicate landmarks, horizontal overflow, and interaction regressions that curl cannot see. Keep the browser fixture small and repeatable: home, one heavy note, one mobile viewport, and one reduced-motion pass are more useful than an unbounded manual tour.
- HTTP: status, content type, and representative body markers from a body fetch.
- Server: configuration test, container/process health, and reload result.
- Browser: no failed resources or console errors, no document overflow, and usable article controls.
- Record: release digest, verification time, and the exact rollback archive.
If the only proof is “the reload returned zero,” the release has not been verified.
Make rollback boring enough to use
Rollback is not a dramatic recovery plan reserved for outages. It is the normal answer to a failed smoke check. Archive the previous root before promotion, keep the archive tied to the release receipt, and make the reversal use the same serving-layer validation as the forward path. A rollback that has never been rehearsed is a hope with a filename.
The safest small-site rollback is intentionally unglamorous: stop the promotion, restore the last known tree, validate the configuration, reload, and re-run the public probes. Do not “fix forward” while the release identity is still unclear; that produces a second unknown artifact and makes the incident harder to explain.
set -eursync -a --delete previous-release/ live-root/nginx -tnginx -s reload# reachability smoke check; repeat body/browser checkscurl --fail --silent --show-error --head https://example.com/What the digest does not prove
A matching digest proves byte identity between two copies. It does not prove that the content is correct, that a dependency is safe, that a route is accessible, or that a browser can hydrate the page. Keep semantic, security, accessibility, and browser checks in the release contract instead of asking one hash to carry them.
The release contract
The pattern is small enough to run without a deployment platform and strict enough to prevent the most expensive category error: confusing a successful copy with a successful release. Build once. Identify the bytes. Stage them. Validate the router and the reader-facing surface. Preserve the previous state. Promote only after the evidence is present. Verify from outside the process. Keep the reversal close.
That chain scales upward. Larger systems add signed attestations, immutable registries, progressive traffic shifts, and automated policy gates; they do not remove the underlying questions. What changed? What exactly moved? What is serving it? How do we know? What was live before it? How do we return there without improvising?
- implemented Artifact-first promotion path
A working static-site release path builds locally, archives and hashes one candidate, validates a remote stage, creates a rollback archive, promotes the same tree, checks the serving configuration, and probes the public routes.
- field-observed Rollback is cheapest before promotion
Preserving the previous document root before rsync turns a failed release into a bounded reversal instead of an improvised reconstruction.
- sourced SvelteKit defines a static output boundary
The adapter-static documentation describes the pages and assets written by the adapter and warns that trailing-slash and SSR choices affect the generated result.
inspect source ↗ - sourced Nginx route resolution is explicit
Nginx documents try_files as a sequence of file checks followed by an internal redirect, making clean-URL behavior a testable serving contract rather than an assumption.
inspect source ↗ - sourced Rsync has path-shape semantics
The rsync manual distinguishes copying a directory itself from copying the contents selected by a trailing slash, so the source path belongs in the release procedure.
inspect source ↗ - sourced SHA-256 is a checkable artifact identity
GNU Coreutils documents sha256sum as a command that computes and checks SHA-256 digests; the digest proves byte identity, not semantic correctness.
inspect source ↗ - sourced Nginx separates testing from reload
Nginx documents -t as syntax and referenced-file testing, while -s reload starts new workers with a new configuration and gracefully retires old workers; the release contract keeps those operations distinct.
inspect source ↗ - sourced curl --head is header-only
The curl man page defines --head as fetching headers only; header probes are reachability smoke checks, not body-identity or browser verification.
inspect source ↗
- reference SvelteKit adapter-static documentationopen ↗
Static pages/assets output, fallback trade-offs, and trailing-slash requirements.
- reference Nginx try_files documentationopen ↗
Official route and file-resolution semantics for a static server.
- reference Rsync manualopen ↗
Copy semantics, remote-shell transport, and the distinction between source paths with and without a trailing slash.
- reference GNU SHA-2 utilitiesopen ↗
Reference for computing and checking SHA-256 digests over release artifacts.
- reference Nginx command-line parametersopen ↗
Official semantics for configuration testing and graceful reload.
- reference curl man pageopen ↗
Official semantics and limits of header-only HTTP probes.