static-site-deployment
Comprehensive guide to deploying static sites and single-page applications, covering GitHub Pages, Cloudflare Pages, AWS S3 with CloudFront, cache busting strategies, prerendering for SEO, SPA routing configuration, and CDN setup for optimal performance.
**Static sites**: Pre-rendered HTML for every route. Fast, SEO-friendly, cacheable. Built by Astro, Hugo, Eleventy, Next.js (static export). ## Key Points - Unlimited bandwidth (free tier) - Global CDN (Cloudflare's network) - Automatic HTTPS - Built-in analytics - Web Application Firewall - **Brotli**: 15-20% smaller than gzip for text assets - **Gzip**: Fallback for older clients 1. **No SPA fallback**: Users get 404 when refreshing on a client-side route. 2. **Caching `index.html`**: Users stuck on stale versions for hours/days. 3. **Invalidating all CDN paths on deploy**: Expensive and slow. Only invalidate HTML. 4. **Client-side rendering for landing pages**: Hurts SEO and perceived performance. 5. **Not compressing assets**: Serving uncompressed JS/CSS wastes bandwidth. ## Quick Example ``` www.example.com ``` ```bash # Via Wrangler CLI npx wrangler pages project create my-site npx wrangler pages deploy dist ```
skilldb get deployment-patterns-skills/static-site-deploymentFull skill: 490 linesDeploying Static Sites and SPAs
Static Sites vs SPAs
Static sites: Pre-rendered HTML for every route. Fast, SEO-friendly, cacheable. Built by Astro, Hugo, Eleventy, Next.js (static export).
SPAs: Single HTML file + JavaScript handles routing client-side. Built by React (Vite), Vue, Svelte, Angular. Requires routing configuration on the server.
GitHub Pages
Basic Setup
# .github/workflows/deploy-pages.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
SPA on GitHub Pages
GitHub Pages doesn't support SPA routing natively. The 404 hack:
<!-- public/404.html -->
<!DOCTYPE html>
<html>
<head>
<script>
// Redirect all 404s to index.html with the path preserved
const path = window.location.pathname;
const query = window.location.search;
window.location.replace(
window.location.origin + '/?redirect=' + encodeURIComponent(path + query)
);
</script>
</head>
</html>
<!-- index.html (in <head>) -->
<script>
(function() {
const params = new URLSearchParams(window.location.search);
const redirect = params.get('redirect');
if (redirect) {
window.history.replaceState(null, '', redirect);
}
})();
</script>
Custom Domain
Create public/CNAME with your domain:
www.example.com
DNS: CNAME www to username.github.io.
Anti-pattern: Using GitHub Pages for sites that need server-side logic. It's purely static. Use Cloudflare Pages or Vercel for anything with API routes.
Cloudflare Pages
Setup
# Via Wrangler CLI
npx wrangler pages project create my-site
npx wrangler pages deploy dist
Configuration
# wrangler.toml (optional, for advanced config)
name = "my-site"
pages_build_output_dir = "dist"
[env.production]
vars = { API_URL = "https://api.example.com" }
Framework Presets
Cloudflare Pages auto-detects and configures:
| Framework | Build Command | Output Dir |
|---|---|---|
| Astro | astro build | dist |
| Next.js | next build | .next |
| Nuxt | nuxt build | .output |
| SvelteKit | vite build | build |
| Vite/React | vite build | dist |
SPA Routing
Cloudflare Pages handles SPA routing automatically when you deploy a single-page app. No configuration needed. All unmatched routes serve index.html.
Functions (Serverless)
// functions/api/hello.js
export async function onRequestGet(context) {
return new Response(JSON.stringify({ message: 'Hello' }), {
headers: { 'Content-Type': 'application/json' },
});
}
Headers and Redirects
# public/_headers
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
/assets/*
Cache-Control: public, max-age=31536000, immutable
# public/_redirects
/old-page /new-page 301
/blog/* https://blog.example.com/:splat 302
Advantages
- Unlimited bandwidth (free tier)
- Global CDN (Cloudflare's network)
- Automatic HTTPS
- Built-in analytics
- Web Application Firewall
AWS S3 + CloudFront
S3 Bucket Setup
# Create bucket
aws s3 mb s3://my-site-production
# Configure for static hosting
aws s3 website s3://my-site-production \
--index-document index.html \
--error-document index.html # SPA fallback
# Bucket policy for CloudFront access
cat > bucket-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "CloudFrontAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-site-production/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789:distribution/EDFDVBD6EXAMPLE"
}
}
}]
}
EOF
aws s3api put-bucket-policy --bucket my-site-production --policy file://bucket-policy.json
CloudFront Distribution
# Create CloudFront distribution (simplified)
aws cloudfront create-distribution \
--origin-domain-name my-site-production.s3.amazonaws.com \
--default-root-object index.html
SPA Routing with CloudFront
Configure a custom error response:
{
"CustomErrorResponses": {
"Items": [
{
"ErrorCode": 403,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 0
},
{
"ErrorCode": 404,
"ResponsePagePath": "/index.html",
"ResponseCode": "200",
"ErrorCachingMinTTL": 0
}
]
}
}
Deploy Script
#!/bin/bash
# deploy-s3.sh
BUCKET="my-site-production"
DISTRIBUTION_ID="EDFDVBD6EXAMPLE"
# Sync with appropriate cache headers
# HTML files: no cache (always fresh)
aws s3 sync dist/ s3://$BUCKET \
--delete \
--exclude "*.html" \
--cache-control "public, max-age=31536000, immutable"
# HTML files separately with no-cache
aws s3 sync dist/ s3://$BUCKET \
--delete \
--include "*.html" \
--exclude "*" \
--cache-control "public, max-age=0, must-revalidate"
# Invalidate CloudFront cache
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/index.html" "/404.html"
Anti-pattern: Invalidating /* on every deploy. It's slow, costly, and defeats the purpose of a CDN. Only invalidate HTML files; hashed assets don't need invalidation.
Cache Busting
Content-Hashed Filenames
Modern bundlers (Vite, webpack, esbuild) output hashed filenames:
dist/
index.html ← No hash (must be fresh)
assets/
app-3a4b5c.js ← Hashed (cache forever)
app-7d8e9f.css ← Hashed (cache forever)
logo-1a2b3c.png ← Hashed (cache forever)
Vite Configuration
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
// Customize hash format
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash][extname]',
},
},
},
};
Cache Headers Strategy
# Hashed assets: cache forever
/assets/*
Cache-Control: public, max-age=31536000, immutable
# HTML: always revalidate
/*.html
Cache-Control: public, max-age=0, must-revalidate
# Service worker: never cache
/sw.js
Cache-Control: no-cache, no-store, must-revalidate
Anti-pattern: Setting long cache times on index.html. Users get stuck on old versions because the browser serves cached HTML that references old asset hashes.
Prerendering for SEO
Static Site Generation (SSG)
// astro.config.mjs — Full SSG
export default defineConfig({
output: 'static',
});
// next.config.js — Static export
module.exports = {
output: 'export',
};
Prerendering Specific Routes in SPAs
// vite.config.js with vite-plugin-ssr or similar
import { prerender } from 'vite-plugin-prerender';
export default {
plugins: [
prerender({
routes: ['/', '/about', '/pricing', '/blog'],
renderer: new PuppeteerRenderer(),
}),
],
};
Dynamic Prerendering with a Prerender Service
For SPAs that can't be statically rendered, use a prerender service for bots:
# Nginx configuration
map $http_user_agent $prerender {
default 0;
"~*googlebot" 1;
"~*bingbot" 1;
"~*twitterbot" 1;
}
server {
location / {
if ($prerender) {
rewrite .* /prerender/$scheme://$host$request_uri break;
proxy_pass https://prerender.example.com;
}
try_files $uri $uri/ /index.html;
}
}
Anti-pattern: Relying on client-side rendering for SEO-critical pages. Search engines can render JavaScript, but it's slower and less reliable.
SPA Routing Configuration
Every server/CDN must be configured to serve index.html for all routes so the client-side router can handle them.
Nginx
server {
listen 80;
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Apache (.htaccess)
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Caddy
example.com {
root * /var/www/html
try_files {path} /index.html
file_server
header /assets/* Cache-Control "public, max-age=31536000, immutable"
}
CDN Configuration Best Practices
Cache Tiers
Browser Cache → CDN Edge → CDN Shield → Origin
Security Headers via CDN
# Cloudflare Transform Rules or _headers file
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;
Compression
Ensure your CDN compresses responses:
- Brotli: 15-20% smaller than gzip for text assets
- Gzip: Fallback for older clients
Most CDNs (Cloudflare, CloudFront, Vercel) handle this automatically.
Common Anti-Patterns
- No SPA fallback: Users get 404 when refreshing on a client-side route.
- Caching
index.html: Users stuck on stale versions for hours/days. - Invalidating all CDN paths on deploy: Expensive and slow. Only invalidate HTML.
- Client-side rendering for landing pages: Hurts SEO and perceived performance.
- Not compressing assets: Serving uncompressed JS/CSS wastes bandwidth.
- Using S3 without CloudFront: S3 alone is slow, expensive for bandwidth, and doesn't support HTTPS on custom domains.
Platform Comparison
| Feature | GitHub Pages | Cloudflare Pages | S3 + CloudFront |
|---|---|---|---|
| Cost | Free | Free (generous) | Pay-per-use |
| Bandwidth | 100GB/mo | Unlimited | $0.085/GB |
| Custom domains | Yes | Yes | Yes |
| HTTPS | Auto | Auto | ACM (free) |
| SPA routing | 404 hack | Native | Error response |
| Functions | No | Yes (Workers) | Lambda@Edge |
| Build system | Actions | Built-in | External CI |
| Complexity | Low | Low | High |
Install this skill directly: skilldb add deployment-patterns-skills
Related Skills
database-deployment
Comprehensive guide to database deployment for web applications, covering managed database services (PlanetScale, Neon, Supabase, Turso), migration strategies, connection pooling, backup and restore procedures, data seeding, and schema management best practices for production environments.
docker-deployment
Comprehensive guide to using Docker for production deployments, covering multi-stage builds, .dockerignore optimization, layer caching strategies, health checks, Docker Compose for local development, container registries, and security scanning best practices.
fly-io-deployment
Complete guide to deploying applications on Fly.io, covering flyctl CLI usage, Dockerfile-based deployments, fly.toml configuration, persistent volumes, horizontal and vertical scaling, multi-region deployments, managed Postgres and Redis, private networking, and auto-scaling strategies.
github-actions-cd
Comprehensive guide to implementing continuous deployment with GitHub Actions, covering deploy workflows, environment protection rules, secrets management, matrix builds, dependency caching, artifact management, and deploying to multiple targets including Vercel, Fly.io, AWS, and container registries.
monitoring-post-deploy
Comprehensive guide to post-deployment monitoring for web applications, covering uptime checks, error tracking with Sentry, application performance monitoring, log aggregation, alerting strategies, public status pages, and incident response procedures for production systems.
netlify-deployment
Complete guide to deploying web applications on Netlify, covering build settings, deploy previews, serverless and edge functions, forms, identity, redirects and rewrites, split testing, and environment variable management for production workflows.