Deploy a site
pagedeck build writes a static site, and no pagedeck command uploads it.
You upload it to your origin, the bucket or directory your host serves, with
the host's own tools. This page covers what to upload, how to upload only the
files that changed, how to put an earlier build back, what to keep between CI
runs, and how to give your host the site's redirects and headers.
The pagedeck command lists every verb, flag and exit code
used here.
The commands run in the site's directory, the one holding
pagedeck.config.ts. The paths are the defaults of the site
npm create pagedeck writes: the site goes to site/, and the content store
is content.db. If you have no site yet,
Your first site makes one.
1. Upload the output directory
Sync the content into the content store, then build:
npx pagedeck sync
npx pagedeck buildUpload everything in site/. Any host that serves a directory of files can
serve it. Do not upload content.db or .pagedeck/: the build reads them, and
a visitor has no use for them. They are outside site/ so that copying the
whole directory is safe.
A full build deletes from site/ every file the previous build wrote and this
build did not. A retracted page is gone from the directory after the next
build. A host that mirrors the directory must also delete on its side. For an
S3 bucket, aws s3 sync deletes only when you ask it to:
aws s3 sync site/ s3://my-bucket/ --deletesite/manifest.json is part of the upload. It records every file the build
wrote and the hash of each, and the next section uses it. A plain static host
also serves it to anyone who asks for /manifest.json. The edge artifacts in
section 5 answer that request with a 404.
2. Upload only what changed
A full upload is simple, but it sends every file on every deploy. To send only the files that changed, compare the manifest of the build your origin serves with the manifest of the new build:
npx pagedeck diff live-manifest.json site/manifest.json > plan.jsonlive-manifest.json is the manifest.json your last deploy uploaded. Read it
from the origin itself, not from the site's public URL, because the edge
artifacts refuse /manifest.json. For an S3 bucket:
aws s3 cp s3://my-bucket/manifest.json live-manifest.jsonOr keep the site/manifest.json of each deploy as a CI artifact, and download
the last one. The deploy recipe
reads it from a bucket with a presigned GET. On a first deploy there is no live
manifest: upload the whole of site/ as section 1 does.
pagedeck diff writes a JSON document, the plan. Your upload script reads it:
treeshas one entry per output tree. A site with no per-locale domains has one. A tree with adomainkey is the directory of that name undersite/.- Each tree's
uploadlists the files that are new or changed. A row'spathis the file's path in its tree:/about/index.htmlissite/about/index.html. Upload the rows in the order listed. The scripts, stylesheets and images come before the pages, so no page goes live before a file it loads. - Each tree's
prunelists the files the new build no longer writes. Delete them, but not before the time in the plan's top-levelprune.notBefore. A visitor who loaded a page from the old build can still ask for a script that build used. The wait is seven days after the new build was made, and--grace-secondschanges it. statscounts the files added, changed, pruned and unchanged, and the bytes to upload.
Upload site/manifest.json last, after every row. It is the live manifest for
the next deploy.
pagedeck diff refuses a build that was not made on top of the live build.
Each build records the newest build in .pagedeck/manifests when it started.
If that is not the build the origin serves, another deploy happened in
between, and this plan would overwrite it. The refusal exits with code 2. Run
pagedeck build again so the new build starts from what is live. A build with
no record at all is refused the same way, which is what happens on a CI runner
that starts with an empty .pagedeck/. Section 4 keeps it.
--force deploys the build anyway. Pass it by hand, for one deploy you have
checked. A pipeline that always passes it has turned the check off.
Deploy serialization and rollback explains
the record and how to keep two deploys from running at once.
3. Roll back
Each build keeps a copy of its manifest in .pagedeck/manifests, as
<build id>.json. To put an earlier build back, list them, and pass the name
without .json:
ls .pagedeck/manifests
npx pagedeck rollback 3c77b1de-52a0-4e77-bb0e-8f0a1c2d3e40 > rollback.jsonpagedeck rollback writes the same kind of plan as pagedeck diff. It
compares site/manifest.json, the build your origin serves, with the kept
manifest. Run it where site/ holds the live build. Your upload script runs
the plan as it runs a deploy. A rollback is always out of order, so it takes
no --force.
The plan names files, and the manifest holds no file contents. Upload the rows
from the output of the build you are restoring: keep each deployed site/ as
a CI artifact, or build that commit again.
The build keeps the 20 newest manifests, and build.retention.keep changes
that number.
Deploy serialization and rollback
covers how far back a rollback reaches.
4. Keep the content store and .pagedeck/ between CI runs
A CI runner starts with no content.db and no .pagedeck/. Without
content.db, every run syncs all the content from the source. Without
.pagedeck/, every build records no earlier build, and pagedeck diff
refuses every deploy.
Keep the content store as a snapshot. pagedeck store pull downloads
content.db and pagedeck store push uploads it. Give the target in
PAGEDECK_SNAPSHOT_URL and none on the command line, as
PAGEDECK_SNAPSHOT_URL explains:
- run: npx pagedeck store pull
env:
PAGEDECK_SNAPSHOT_URL: ${{ secrets.SNAPSHOT_URL }}
- run: npx pagedeck sync --incremental
- run: npx pagedeck build
- run: npx pagedeck store push
env:
PAGEDECK_SNAPSHOT_URL: ${{ secrets.SNAPSHOT_URL }}
On the first run there is no snapshot to pull, and no cursor for
pagedeck sync --incremental to start from. Run pagedeck sync and
pagedeck store push once to create both.
Keep .pagedeck/ in your CI's cache. Restore it before the build and save
it after. It holds the kept manifests, so it is also what pagedeck rollback
reads.
pagedeck build --incremental renders only the pages the content changes
touch, and reads the rest from the previous site/. On CI it needs site/
from the last run too, from the same run as the snapshot. A full build does
not.
5. Give your host the redirects and headers
build.routing declares the site's redirects, its 404 page and the response
headers for each path prefix. Routing covers the fields,
and Security headers the headers to set
before a deploy. The build compiles them into one host-agnostic routing
document first, writing it into site/manifest.json in a form that names no
host.
An adapter compiles that document into edge artifacts, the files one host
reads. Each host has its own adapter package. Install the one for your host,
here Netlify, and name it in build.adapter:
npm install @pagedeck/adapter-netlifyimport { defineConfig } from "@pagedeck/core";
import { netlify } from "@pagedeck/adapter-netlify";
export default defineConfig({
collections: [],
build: {
pages: [],
components: {},
adapter: netlify(),
},
});A new site can skip this step: npm create pagedeck@latest my-site -- --host netlify
installs the adapter and writes both of these for a fresh site, and
--host vercel or --host cloudflare-pages do the same for those hosts. The
above is what to do by hand, or to a site that already exists.
pagedeck build runs the adapter after it plans the routing document and
before it reports success. Each edge artifact has a role, and the role says
where the build writes it: a tree-file into site/<tree>/, where it rides
site/manifest.json and pagedeck diff like any other file, and every other
artifact into edge/<tree>/, beside site/, which you install on the host
yourself (<tree> is empty for a site with one output tree). An incremental
build writes the same files a full build does, and an artifact an earlier
build wrote that this one does not is removed from wherever it was written,
the way a full build removes a retracted page from site/. pagedeck dev
runs no adapter: it renders pages from the store on request and writes
neither site/ nor edge/.
For another host, install its adapter and name its factory in build.adapter
in place of netlify():
netlify()from@pagedeck/adapter-netlifywrites_redirects, and_headerswhen the site declares header rules. Both are tree files, sopagedeck diffuploads them with the rest of the site, after the site's own files, so no redirect goes live before its target.nginx()from@pagedeck/adapter-nginxwritesrouting.confintoedge/.includeit in theserverblock that servessite/.cloudfront()from@pagedeck/adapter-cloudfrontwrites a viewer-request and a viewer-response CloudFront Function, and configuration fragments when the routing needs them, intoedge/. Publish each function and associate it with the distribution. A function left inedge/does nothing until you publish it.cloudflareWorker()from@pagedeck/adapter-cloudflare-workerwritesworker.jsintoedge/, published as a Worker with the site's R2 bucket bound to it. This adapter has not yet served a site in production: test it on a staging deploy before you rely on it.cloudflarePages()from@pagedeck/adapter-cloudflare-pageswrites_redirects, and_headerswhen the site declares header rules, the same as Netlify's. Both are tree files. It refusestrailingSlash: "never".vercel()from@pagedeck/adapter-vercelwritesvercel.json, a tree file.
Outside pagedeck build — against a manifest from another build, or to try an
adapter without building — call it directly over a routing document:
import { readFileSync } from "node:fs";
import { readManifest } from "@pagedeck/core";
import { netlify } from "@pagedeck/adapter-netlify";
const manifest = readManifest(readFileSync("site/manifest.json", "utf8"), "site/manifest.json");
for (const artifact of netlify().compile(manifest.routing).artifacts) {
console.log(artifact.role, artifact.path);
}Every adapter's edge artifacts answer /manifest.json and /.pagedeck/ with a
404, even for a site that declares no routing. Install them, and the manifest
you uploaded to the origin stays private.
Host guides
Each adapter has a page that takes a site from a build to its host: the host's build settings, where each artifact goes, how to keep the content store between builds, and how to send a path prefix to an app.
| Adapter | Host | Guide |
|---|---|---|
cloudflarePages() |
Cloudflare Pages, or Workers Static Assets | Deploy to Cloudflare Pages |
cloudflareWorker() |
a Cloudflare Worker in front of an R2 bucket | Deploy to a Cloudflare Worker |
cloudfront() |
S3 behind CloudFront, with an app on AWS Lambda | Deploy to CloudFront |
netlify() |
Netlify | Deploy to Netlify |
nginx() |
your own nginx server | Deploy to nginx |
vercel() |
Vercel | Deploy to Vercel |