Automated Deployment of Static Sites with CI/CD Pipelines
After deploying a static site to S3, users get 404 on Next.js routes, and the CloudFront cache shows stale content. These are typical consequences of incorrect CI/CD configuration: missing invalidation, wrong Cache-Control, and unaccounted generator specifics. Over 5 years of experience and more than 100 delivered pipelines, we've developed schemes that work reliably: no cache surprises, auto-deploy, and preview environments. One client reduced CloudFront costs by 35% after implementing our caching scheme — saving an average of $250 per month. Average build time for Hugo on 2000 pages is 5 seconds, Jekyll without cache is 3 minutes (36x slower), with cache is 5 seconds. Below — how we do it and what's included.
Problems CI/CD for Static Sites
CDN cache invalidation. You upload a new file to S3, but users see the old version. Without create-invalidation, CloudFront caches HTML for hours. We use a two-pass s3 sync with different Cache-Control: for static assets — max-age=31536000, for HTML — no-cache. This reduces invalidation count and costs by up to 50%.
Next.js Export without configuration. next export does not support getServerSideProps, API routes, and Image Optimization. If you have dynamic routes, you need to switch to ISR or configure output: 'export' with trailingSlash: true and unoptimized: true. Otherwise the build fails.
Slow Jekyll build. Without cache, bundle install pulls gems for 2-3 minutes. Enable bundler-cache: true — time drops to 5 seconds, a 95% reduction. This is a common beginner mistake.
How We Design the Pipeline
For each project — its own config, but a common template:
- Build: fixed generator version (Hugo extended, Ruby 3, Node 20).
- Optimization: minification, compression, content hash in filenames.
- Deploy: rsync via S3 or direct upload to Cloudflare Pages / Netlify.
- Invalidation: CloudFront, or automatic on Netlify/Cloudflare.
- Preview: for PRs — temporary URL.
Case: Hugo + S3 + CloudFront. We use peaceiris/actions-hugo with extended mode for SCSS. Build takes 5 seconds on 2000 pages. Deploy with two aws s3 sync commands — for static assets and HTML separately. Then invalidate root (/*). For preview — a separate prefix s3://bucket/previews/PR-42/. Build time is 36x faster than Jekyll without cache.
---
- uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.124.0'
extended: true
- name: Build
run: hugo --minify --baseURL "https://my-site.pages.dev"
- name: Deploy to S3
run: |
aws s3 sync public/ s3://my-bucket/ \
--delete \
--cache-control "public, max-age=31536000" \
--exclude "*.html" \
--exclude "*.xml"
aws s3 sync public/ s3://my-bucket/ \
--delete \
--cache-control "no-cache" \
--include "*.html" \
--include "*.xml"
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
Jekyll on GitHub Pages. We use the official workflow via actions/deploy-pages — no need to push to gh-pages branch. Cache gems.
---
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Build Jekyll
run: bundle exec jekyll build --destination _site
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
Next.js Export on Cloudflare Pages. Cloudflare Pages automatically runs npm ci && npm run build, but for export configuration matters. We add next.config.js with output: 'export' and disable image optimization.
module.exports = { output: 'export', trailingSlash: true, images: { unoptimized: true } } ---
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
command: pages deploy out/ --project-name=my-site
Choosing a Hosting Platform
When choosing a platform, consider cost, performance, and built-in features. S3 + CloudFront gives full control over cache and CDN but requires manual invalidation and preview setup. Cloudflare Pages is 2x more cost-effective than Netlify for high-traffic sites (free up to 500 builds/month) and provides preview out of the box. Netlify is convenient for teams but more expensive. For most projects, we recommend Cloudflare Pages for its balance of price and functionality. You can use this guide to select the optimal platform for your project.
Hosting Platform Comparison (click to expand)
| Platform | Cache Invalidation | Preview Environments | Price |
|---|---|---|---|
| S3 + CloudFront | Manual (API) | Manual configuration | Pay for traffic + requests |
| Cloudflare Pages | Automatic | Built-in | Free up to 500 builds/month |
| Netlify | Automatic | Built-in | Free up to 300 builds/month |
Why CDN Cache Invalidation Matters?
Without invalidation, users see the old version until TTL expires — from minutes to days. We use a strategy: static assets (CSS/JS/images) — long cache with content hash, HTML — no cache. This reduces CDN load and ensures content freshness. According to CloudFront Developer Guide, invalidation must be performed after every deploy. Proper caching strategy allows some clients to save up to $500 per month on CDN costs. Implementing atomic deployments with hash-based cache busting ensures instant content freshness.
Preview Environments Use Cases
Note: when the project has multiple developers. Preview allows seeing changes in isolation before merging into main. Netlify and Cloudflare Pages provide this out of the box. For S3+CloudFront, we create prefixes per PR number.
---
- name: Deploy preview
if: github.event_name == 'pull_request'
run: |
aws s3 sync public/ s3://my-bucket/previews/pr-${{ github.event.number }}/
echo "Preview URL: https://d1w2x3.cloudfront.net/previews/pr-${{ github.event.number }}/"
---
Process
- Analysis — review current pipeline, repository, hosting.
- Design — select stack: S3+CloudFront, Cloudflare Pages, or Netlify.
- Implementation — configure CI/CD, cache, preview.
- Testing — verify build, deploy, invalidation.
- Deployment — roll out to production.
What's Included
- CI/CD configuration (GitHub Actions / GitLab CI)
- Hosting and CDN setup
- Preview environment integration
- Pipeline documentation
- Team training
- 1 month support after launch
- Pricing starts from $500
Common Static Deploy Mistakes:
- No Cache-Control for HTML — users see old version.
- Missing CDN invalidation after deploy — content doesn't update.
- Next.js export without trailingSlash — 404 on routes.
- No gem caching during Jekyll build — build takes minutes.
- Using output: 'export' without disabling image optimization — build fails.
Timeline Estimates
| Task | Time |
|---|---|
| Hugo/Next.js → GitHub Pages / Netlify | 0.5 day |
| S3 + CloudFront + cache invalidation | 1 day |
| Preview environments for PRs | +0.5 day |
| Jekyll with cache + custom domain | 1 day |
With 5+ years of expertise and over 100 delivered pipelines, we ensure your static site deployment is reliable and fast. Prices are calculated individually after analyzing your project. A robust static site cache strategy includes versioning and selective invalidation. Cloudflare Pages automatically manages cache with built-in purging. Jekyll CI/CD optimization primarily involves gem caching and parallel job execution. Consider requesting an estimate to get started.







