Preview Environments for Static Sites
If you ship a website or app from a GitHub repository, every branch can get its own live URL and every pull request a link to click. No platform vendor, no build step you do not control, just a hundred lines of YAML and the infrastructure you already own.
Talk to an EngineerWhat This Actually Looks Like
Someone on your team, and it does not have to be an engineer, pushes a branch called video-label-updates to a static site repository. Within about forty seconds, the site is live at staging.perceptioninstruments.com/video-label-updates/, served from its own CloudFront distribution with the same wildcard TLS certificate as production. No one asked for it. No one configured it. The workflow saw a branch that was not main and deployed it.

When that branch becomes a pull request, the same workflow notices the event type, deploys to staging.perceptioninstruments.com/pr-2/ instead, and posts a comment on the PR with the live URL. The reviewer clicks the link, sees the change rendered on real infrastructure, and reviews with confidence that the staging version is the production version with one branch of difference. When the branch is eventually deleted, a cleanup job removes the S3 prefix, and the preview disappears.
This is not a platform feature. There is no Vercel, no Netlify, no vendor preview toggle to enable. It is a GitHub Actions workflow, an S3 bucket, a CloudFront distribution, and a CloudFront Function that rewrites /branch-name/ to /branch-name/index.html. The total infrastructure is defined in a single Rabbit IaC config, and the workflow that drives it is about a hundred lines of YAML.
Why This Matters More Than It Sounds
Preview environments are usually pitched to large engineering teams, which quietly tells everyone else the tool is not for them. It is. Anyone who ships a site or an app from a repository benefits, and the reason is simple: preview environments matter because they are the difference between reviewing code and reviewing the thing the code produces.
A diff shows you that a CSS class changed. A preview environment shows you that the heading is now too wide on mobile. A diff shows you that a video source attribute was updated. A preview environment shows you whether the new video actually plays. For static sites, where the entire deliverable is what the browser renders, reviewing without a preview is reviewing the recipe instead of tasting the food.
The second reason is less obvious but more valuable over time: preview environments make non-developers part of the review process. A scientist who builds instruments, a stakeholder who cares about the copy, a designer who notices that the spacing is off, none of them will read a diff, but all of them can click a link. The preview URL turns a pull request from something only engineers participate in into a checkpoint the whole team can join.
The Actual Infrastructure
The whole system is four pieces, and none of them is exotic.
S3 as a bucket of prefixed directories. Every branch or PR deploys to its own prefix in a single S3 bucket: s3://staging-perceptioninstruments-com/video-label-updates/ for a branch, s3://staging-perceptioninstruments-com/pr-2/ for a pull request. The workflow runs aws s3 sync with --delete, which means the preview is always a perfect mirror of the branch's static/ directory with nothing stale left behind.
CloudFront as the edge. A dedicated staging CloudFront distribution serves the bucket with TLS, HTTP/2 and HTTP/3, and edge caching. The distribution uses the same ACM wildcard certificate as production, so the staging subdomain is indistinguishable from the real site to a browser. A CloudFront Function on viewer-request handles the default-index rewrite; without it, /branch-name/ would return a 403 because S3 does not serve index.html from subdirectories on its own.
GitHub Actions as the orchestrator. The workflow triggers on three events: branch pushes (excluding main and production), pull request opens and updates, and branch deletes. A single conditional step determines the deploy prefix, pr-$NUMBER for pull requests and the branch name for everything else. For PRs, a final step uses actions/github-script to post or update a comment with the preview URL, so the link appears once and stays current through force-pushes.
Rabbit IaC as the definition. The staging CloudFront distribution, its S3 origin, its cache policy, and the CloudFront Function are all defined in a .rabbit/staging/ config that deploys through the same infrastructure pipeline as production. The staging environment is not a snowflake someone configured in the console; it is code that lives in the same repository it serves.
What We Got Wrong the First Time
The concurrency model. GitHub Actions lets you group workflow runs so that a new push cancels the in-progress run for the same branch, which prevents stale deploys from overwriting fresh ones. We scoped the concurrency group by branch name alone, which seemed right until a PR was opened against a branch that already had a push deploy running. The PR event cancelled the branch deploy, and the next branch push cancelled the PR deploy, and neither finished reliably.
The fix was one line: scope the concurrency group by both event type and branch name, so push deploys and PR deploys run in separate lanes. It took longer to diagnose than to fix, and the diagnosis was worth the cost, because the bug only appears when a team is actively using the system, exactly the moment you want it to be reliable.
What This Costs
The staging CloudFront distribution costs nothing when idle, because CloudFront charges per request and a staging preview that nobody is looking at serves zero requests. The S3 storage for a typical static site is measured in single-digit megabytes, which at S3 pricing rounds to zero. The GitHub Actions minutes are consumed by the aws s3 sync and the CloudFront invalidation, which together take about thirty seconds per deploy on the free tier.
The honest cost is the engineer's time to set it up the first time. For us, that was about two hours across two sites, including the IaC config, the DNS record, the ACM certificate, and discovering the concurrency bug described above. The second site took two minutes, because the workflow is a copy with three environment variables changed.
When You Do and Do Not Need This
If your site deploys once a month and one person touches the code, you do not need preview environments. Open the HTML file in a browser, check it, push it. The overhead of the infrastructure is not justified by the frequency of the reviews.
If multiple people contribute to the site, if the people who approve content are not the people who write the code, or if you are making visual changes that a diff cannot meaningfully represent, preview environments pay for themselves the first week. The cost of a staging CloudFront distribution is rounding error. The cost of shipping a broken layout to production because nobody saw it rendered is not.
If you run WordPress, Next.js, or anything with a build step, the pattern is the same but the workflow is longer: you add a build step before the sync, and the build artifacts go to S3 instead of a static directory. The infrastructure underneath, S3, CloudFront, the PR comment, the branch cleanup, does not change. We run this pattern on WordPress at scale and on zero-dependency static sites with equal ease, which is the whole point of keeping the architecture simple enough that the platform does not matter.
Array
The parts of our practice this pattern draws on.
CI/CD pipelines with security and preview environments built in.
GitOps automation, including the IaC that defines these staging environments.
The broader architecture practice preview environments are part of.
The same preview pattern on WordPress, where the build step is longer.
Security architecture for the production side of the same pipeline.
Shipping to the Azure Marketplace
Where this preview discipline ends up: a certified Kubernetes app, rehearsed end to end.
"
Talk to an Expert
Connect with our diverse group of UDX experts that can help you implement successful DevOps practices within your organization.