Deploy to Vercel
This page publishes a site on Vercel from a
connected git repository. Vercel runs the build, and vercel() from
@pagedeck/adapter-vercel writes the site's redirects and headers into the
vercel.json Vercel reads. Deploy a site covers what
every host shares: what to upload, the plan pagedeck diff writes, and
rolling back.
Name the adapter
A new site can start with it: npm create pagedeck@latest my-site -- --host vercel
installs the adapter and names it in the config. For a site that already
exists, install it:
npm install @pagedeck/adapter-vercelThen name its factory in build.adapter, in pagedeck.config.ts:
import { defineConfig } from "@pagedeck/core";
import { vercel } from "@pagedeck/adapter-vercel";
export default defineConfig({
collections: [],
build: {
pages: [],
components: {},
adapter: vercel(),
},
});vercel() writes vercel.json at the root of each output tree,
site/vercel.json for a site with one tree. It is a tree file, so
pagedeck diff uploads it with the rest of the site like any other.
Configure the project
Set these in the project's Build and Deployment settings:
| Setting | Value |
|---|---|
| Framework Preset | Other |
| Build Command | npx pagedeck sync && npx pagedeck build |
| Output Directory | site |
| Node.js Version | 22.x or 24.x, not 20.x, which predates @pagedeck/core's engines.node floor |
Vercel's own docs describe vercel.json as living at the project's root,
which for a Pagedeck site is where pagedeck.config.ts is, not where the build
writes site/. Setting Output Directory to site is what makes Vercel read
the one pagedeck build wrote instead. Check this on a staging deploy before
relying on it, by requesting a path the site redirects and reading the
response.
vercel.json sets trailingSlash from the site's own policy and cleanUrls
to true, since a Pagedeck page is never addressed by a .html name. Its
redirects and headers hold the site's redirects and header rules, and its
routes refuse the reserved deploy keys ahead of the site's files.
Keep the content store between builds
Vercel's build runs in a fresh container on every deploy: it holds no
content.db and no .pagedeck/ from any earlier build. A build that starts
with no content.db syncs every entry, so the Build Command above is the
normal case for this host, and it is always a full build:
pagedeck build --incremental needs site/ from the previous run too, which
Vercel's build container does not carry over.
If a full sync is too slow for your content source, keep content.db between
builds with npx pagedeck store pull and npx pagedeck store push.
PAGEDECK_SNAPSHOT_URL is a credential (see
PAGEDECK_SNAPSHOT_URL), and any build
that has it can overwrite the snapshot production builds start from. In the
project's Environment Variables settings, add it with the type Secret,
which Vercel describes as "write-only after saving", and with Production as
its only target environment
(Config and Secret environment variables).
Then run the snapshot steps only on a production build. Vercel sets
VERCEL_ENV to "the environment that the app is deployed and running on",
one of production, preview or development, at build time
(system environment variables).
Commit this script beside pagedeck.config.ts and set the Build Command to
sh build.sh:
build.sh:
set -e
if [ "$VERCEL_ENV" = "production" ]; then
npx pagedeck store pull
npx pagedeck sync --incremental
npx pagedeck build
npx pagedeck store push
else
npx pagedeck sync
npx pagedeck build
fiThe build itself stays a full build, but a production sync no longer
re-fetches every entry from the content source. A preview build gets no
PAGEDECK_SNAPSHOT_URL and takes the else branch, which syncs every entry
and touches no snapshot. On the first production run there is no snapshot to
pull and no cursor for --incremental to start from: run pagedeck sync and
pagedeck store push once first.
Send a path prefix to an app
A site that declares build.routing.upstreams sends each prefix to an app you
deploy yourself, as Run an app beside the site
describes. On Vercel each upstream must name url, the https origin the app
answers at:
import { defineConfig } from "@pagedeck/core";
import { vercel } from "@pagedeck/adapter-vercel";
export default defineConfig({
collections: [],
build: {
pages: [],
components: {},
adapter: vercel(),
routing: {
upstreams: [{ prefix: "/api/", url: "https://api.example.com" }],
},
},
});vercel() writes one route per prefix into routes, ahead of the site's
files, which sends /api and everything under /api/ to the app:
{ "src": "^/api(/.*)?$", "dest": "https://api.example.com/api$1" }Deploy the app before the site, so no request under the prefix reaches a host
that has nowhere to send it. The site's header rules never reach the app's
answers, so the app sets its own headers, Cache-Control included: Vercel
caches proxied answers for a project created on or after 6 April 2026, and it
caches what the app's Cache-Control allows.
Check it on a staging deploy
Some of what vercel() writes rests on Vercel behaviour its docs do not
state. Check each on a preview deploy before you rely on it, by requesting the
path and reading the status and headers:
- Vercel reads
vercel.jsonfrom the Output Directory. - A
routesentry ahead of{ "handle": "filesystem" }wins over a real file at that path, and one after it is reached only where no file answers. A request for/manifest.jsonshould answer 404. - A route ahead of the files runs before the trailing-slash redirect, the
request's query string reaches the app, and
$1is empty for/api.
The "The edge artifacts" and "An app beside the site" sections of the deploy recipe list these facts beside every other host's.