Deploy Previews
Most hosting services build the site again whenever a commit lands, and many build a separate copy for each pull request. Sveltia CMS asks the Git backend where those builds ended up, so an editor can open the page they just worked on without hunting for the URL — and can tell whether the build has finished yet.
This works in both production workflows, with a different meaning in each:
- In Editorial Workflow, an unpublished entry links to the deploy preview built for its pull request, so editors can see a draft before it goes live.
- In Simple Workflow, where changes are committed straight to the configured branch, an entry links to the live site, and the CMS reports whether the build for the latest change has finished.
Requirements
- A GitHub, GitLab or Gitea/Forgejo backend.
- A CI/CD provider connected to the repository. See CI/CD Integration.
- A
preview_pathon each collection you want entry links for. Without it, an unpublished entry links to the root of its deploy preview, where you can find the page manually. A published entry has no link.
Date tags need a date field
When preview_path uses {{year}}, {{month}} or another date tag, the CMS reads the date from one of these:
- the top-level DateTime field named by
preview_path_date_field - otherwise, the first DateTime field of the collection, or of the file in a file collection
If no such field exists, a configuration warning says so. Otherwise the preview link would go missing with no explanation.
A field that exists but is left empty on an entry has the same effect. That can only be spotted on the entry itself.
Configuration
There’s nothing to turn on. As long as the requirements above are met, deploy preview links appear on their own.
The options below shape what the links do:
| Option | Where | What it does |
|---|---|---|
preview_path | Entry collection, or each file of a file collection | Path template appended to the site or preview URL. Without it, only the root of a deploy preview is linked |
preview_path_date_field | Entry collection, or each file of a file collection | Which top-level DateTime field the {{year}}, {{month}} and similar tags read. Default: the first DateTime field |
site_url | Top level | Base URL of the live site |
show_preview_links | Top level | Set to false to hide every preview link. Default: true |
preview_context | backend | Names the exact commit status or environment that carries the preview URL |
If site_url isn’t set, the CMS falls back to the URL reported by the production deployment, so links can still work without it. When site_url is set, it always wins.
How It Works
Every entry belongs to a commit: the head of its pull request in Editorial Workflow, or the head of the configured branch otherwise. The CMS asks the backend what the CI/CD provider reported for that commit, takes the URL from the answer, and appends the collection’s preview_path.
So an entry whose preview_path is /blog/{{slug}} links to https://example.com/blog/my-post on the live site, and to https://cms-posts-hello.example.pages.dev/blog/my-post on a deploy preview built by Cloudflare Pages.
Where the URL Comes From
Providers report a deployment in one of three ways, and Sveltia CMS reads all of them:
| Source | Known to use it |
|---|---|
| Deployments (GitHub) and environments (GitLab) | Vercel, GitHub Pages, GitLab Review Apps |
| Commit statuses | Vercel, Gitea/Forgejo Actions and CI services that post a build status |
| Check runs (GitHub only) | Cloudflare Pages, AWS Amplify |
Gitea and Forgejo have commit statuses alone, so on that backend a provider has to post one to be seen. See Reporting a Preview from Gitea/Forgejo Actions.
All three are read whichever provider is used. So a provider that isn’t listed still works, as long as it reports through any of them.
Equally, a provider that reports nowhere the Git host can see can’t be detected at all. Several publish only to their own dashboard, or to a pull request comment. If you’re unsure which applies, open a recent commit on your Git host and see whether anything is attached to it.
Choosing Among Several Reports
When more than one provider reports on the same commit, the CMS first sets aside the ones that don’t look like a deployment. A commit status or check run whose name doesn’t mention a known provider, or a word like “deploy”, “preview” or “pages”, is most likely a test suite or a linter. GitLab posts a commit status for every CI job, as do Gitea and Forgejo for every Actions job, and CI services like CircleCI do the same on GitHub.
Such a candidate is still reported if nothing else is there, so a provider these rules have never heard of still works. But a finished test suite can’t stand in for a deploy preview that’s still building.
Among the remaining candidates, the CMS prefers, in order:
- a finished build with a page to open
- the source whose URL is most reliable — a deployment’s environment URL is always the site, while a commit status URL is sometimes a build log
- an environment whose name matches what it’s looking for, so a
productionenvironment isn’t passed over for apreviewone on the live branch
Ties go to the newest.
Ignored Addresses and Builds
An address is only taken from a finished build, because several providers hand out a placeholder while they work. For example, Cloudflare Pages reports its own dashboard until the build succeeds, then replaces it with the preview address. So nothing is offered until there’s a page behind it.
A URL leading back to the Git host is ignored for the same reason: it’s a job log, which every GitLab CI job and every Gitea/Forgejo Actions job reports.
A build that was canceled or skipped is ignored, even when the provider reports it as successful. This happens in a monorepo, where a site that the commit didn’t touch still reports a result, and its URL leads to the build log rather than a page.
How check runs are read
A check run’s own link usually leads to a build log rather than a site, so it takes more care than the other two sources.
Every run reports its build state. So even a provider these rules have never heard of still reports that a build on the commit is running or has failed.
Offering an address is another matter. Only a run whose name suggests a deployment offers one. A run that doesn’t is ranked below every one that does, so a green test suite can’t stand in for a build that hasn’t finished.
For a run that does look like a deployment, the address is taken in this order:
- A URL published in the run’s output. Cloudflare Pages links the check itself to the Cloudflare dashboard, but writes a table of preview URLs into its check summary. So that table is read, and any dashboard links in it are passed over.
- The run’s own link, but only if the name says “preview”. AWS Amplify reports “AWS Amplify Console Web Preview” and links straight to the site.
- Neither. The build state is still reported — so the editor sees that a build is running or has failed — but no address is offered.
If your provider’s naming defeats this, name the check explicitly with preview_context.
Build States
What the control does depends on whether a preview is still on its way.
While a preview is being built for an unpublished entry, the button reads Checking for Preview, is disabled and shows a turning icon. The live-site link isn’t offered, because the live site doesn’t show that entry. It holds the published version, or nothing at all when the entry is new.
Otherwise the button is a link. It reads View Preview when it points at a deploy preview, and View on Live Site when it points at the live site.
A build that’s still running or has failed is described on the control for screen readers. It’s also shown as a badge on the Editorial Workflow page:
| Build state | Badge on the workflow card |
|---|---|
| Building | Building… |
| Failed | Build Failed |
| Being queried, finished or nothing reported | none |
A published entry is never made to wait, whatever its build is doing: the live site genuinely holds that page, so the link stays available.
When no CI/CD provider reports anything, nothing is lost: the same live-site link as before is shown. This happens when no provider is connected, or when it reports in a way the backend doesn’t expose.
Checking Again While Building
While a build is running, the CMS checks again every 5 seconds, so a preview is offered as soon as it exists. Each check costs:
- on GitHub, a single request
- on GitLab, one shared call plus one per commit being watched
- on Gitea/Forgejo, one request per commit being watched
The CMS gives up after 10 minutes. The icon stops turning, the link comes back, and a Check for Preview action appears in the entry editor’s options menu. Reopening the entry starts a fresh round of checks, so a build that takes longer isn’t lost.
Checking Whether the Page Is Live
A finished build isn’t quite the same as a page that can be opened. A CDN may not have caught up, and a brand-new entry can 404 for a moment after publishing. Where it can, the CMS requests the page itself, and keeps treating the build as unfinished until the page answers.
This check only runs when the page is on the same origin as the CMS. That’s the usual case when the CMS is served from /admin on the site it edits.
A browser can’t read a cross-origin response without permission from that server, and no major static host grants it. So for other origins, the request is skipped rather than sent to learn nothing. Deploy previews are almost always on another origin, so their state comes from the provider alone.
Where Links Appear
- In the entry editor toolbar, for the default locale.
- In each locale pane’s options menu, so a multilingual entry links to the right translation. See Managing Preview Paths with I18n.
- On the cards of the Editorial Workflow page, as an icon button, with a badge when a build is running or has failed.
Reporting a Preview from Gitea/Forgejo Actions
Gitea and Forgejo Actions post a commit status for each job, but it links to the job log, not to anything the job deployed. A workflow that publishes a deploy preview has to report the address itself. Once the preview is up, post a commit status of your own on the head of the pull request:
- name: Report the deploy preview
env:
TOKEN: ${{ github.token }}
PREVIEW_URL: https://preview.example.com/pr-${{ github.event.pull_request.number }}/
STATUS_URL: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}
run: |
curl --fail -X POST "$STATUS_URL" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"context\": \"pages/preview\", \"state\": \"success\", \"target_url\": \"$PREVIEW_URL\"}"Posting a status takes write access to the repository. Whether the automatic token has it depends on where the pull request comes from:
- A branch of the same repository, which is where Editorial Workflow saves entries: the token has write access by default, unless Gitea’s Actions settings restrict it to read access for the repository or its organization.
- A fork, as in Open Authoring: the token is read-only by default.
When the automatic token is read-only, the preview needs an access token with write access, stored as a secret. Otherwise it goes unreported.
The context, pages/preview here, can be anything. A name with “preview”, “deploy” or “pages” in it is recognized as a deployment without configuration, and the CMS then prefers it to the Actions job’s own status, whose link is a log.
The job that posts the status may itself be named like a deployment — Preview / preview (pull_request), say. Its own status then still reports the build as running until the address arrives, which is what you want. If you’d rather the job were left out entirely, set preview_context to the context you post.
Specifying a Status Context
If several providers report on the same commit, or the CMS picks the wrong one, name the one you want with preview_context in the backend section. Only that commit status, check run or environment is then considered.
backend:
name: github
repo: user/repo
preview_context: Cloudflare Pages[backend]
name = "github"
repo = "user/repo"
preview_context = "Cloudflare Pages"{
"backend": {
"name": "github",
"repo": "user/repo",
"preview_context": "Cloudflare Pages"
}
}{
backend: {
name: 'github',
repo: 'user/repo',
preview_context: 'Cloudflare Pages',
},
}The name is matched exactly first, and as a partial match if nothing matches exactly — so cloudflare finds Cloudflare Pages too. Matching is case-insensitive.
Setting this option narrows the search deliberately. If nothing matches, the CMS reports no preview rather than falling back to a provider that wasn’t requested. If a link stops showing up, check the name against the status or environment as it appears on your repository.
Limitations
- On GitHub, a workflow that deploys the site but reports no deployment, no commit status and no check run can’t be detected. Publishing to GitHub Pages with the official actions creates a deployment, so it works; a hand-rolled workflow that only uploads files may not.
- Cloudflare Workers publishes its preview URL in a pull request comment, which no API surfaces alongside the commit. Its check run still reports whether the build succeeded, so the build state is available without a preview link. Cloudflare Pages, which writes the address into its check output, works fully.
- Reading a preview URL out of a check run’s output means reading what that provider chose to write. If the format changes, the address is no longer found and the entry falls back to its live-site link — degraded rather than broken.
- On Gitea/Forgejo, nothing reports a deploy preview unless a workflow or an external service posts it as a commit status. The statuses Actions posts for its jobs give the build state without an address.
- On GitLab, deployments can’t be filtered by commit through the API, so the CMS scans the most recent 100 from the past week and matches them to the branch. A project that deploys more often than that may push a Review App out of range. Its commit status still covers it then.
- A preview behind access control, such as Vercel’s Deployment Protection, answers the liveness check with an authentication error rather than a page. The CMS treats that as “can’t tell” and leaves the link alone, since it works for anyone signed in.