How to Deploy a Static Website to AWS
A complete guide covering S3, CloudFront, Route 53, HTTPS — plus the fundamentals of how websites actually work.
What You'll Learn
Fundamentals
- • What is a website, really?
- • HTTP vs HTTPS — why it matters
- • DNS — how domains find servers
- • TLS/SSL — encryption explained
- • CDN — why speed matters globally
AWS Deployment
- • Building a Next.js static site
- • S3 — storing your files
- • CloudFront — CDN + HTTPS
- • Route 53 — DNS management
- • OG images — social media previews
Part 1: How Websites Work
What is a Website?
A website is just a collection of files — HTML, CSS, JavaScript, images — stored on a computer (server) that's connected to the internet 24/7. When you type a URL in your browser, your computer asks that server for those files, downloads them, and renders them on your screen.
# Simplified: What happens when you visit a website
1. You type: www.example.com
2. Your browser asks DNS: "What's the IP address of example.com?"
3. DNS responds: "It's 93.184.216.34"
4. Your browser connects to 93.184.216.34 and asks for files
5. Server sends back HTML, CSS, JS, images
6. Your browser renders the page
Static vs Dynamic: A static website serves the same files to everyone — perfect for marketing sites, blogs, and documentation. A dynamic website generates pages on the fly based on user data — think dashboards, e-commerce carts, or social media feeds. This guide focuses on static sites.
HTTP vs HTTPS — Why the "S" Matters
HTTP (Hypertext Transfer Protocol)
- • Data sent in plain text
- • Anyone on the network can read it
- • No verification of server identity
- • Browsers show "Not Secure" warning
- • Port 80
HTTPS (HTTP Secure)
- • Data is encrypted
- • Only you and the server can read it
- • Server identity verified by certificate
- • Browsers show padlock icon
- • Port 443
Why HTTPS is now mandatory
TLS/SSL — How Encryption Works
TLS (Transport Layer Security) is the technology that makes HTTPS secure. You might also hear "SSL" — that's the older name (SSL was replaced by TLS, but people still say "SSL certificate").
How TLS works (simplified):
- 1. Handshake: Browser and server agree on encryption method
- 2. Certificate check: Server proves its identity with a certificate
- 3. Key exchange: Both sides create a shared secret key
- 4. Encrypted communication: All data is scrambled using the key
AWS Certificate Manager (ACM) provides free TLS certificates for domains you own. The certificate must be in the us-east-1 region to work with CloudFront.
DNS — The Internet's Phone Book
DNS (Domain Name System) translates human-readable domain names into IP addresses. Computers communicate using IP addresses (like 93.184.216.34), but humans remember names (like example.com).
| Record Type | Purpose | Example |
|---|---|---|
| A | Maps domain to IPv4 address | example.com → 93.184.216.34 |
| AAAA | Maps domain to IPv6 address | example.com → 2001:db8::1 |
| CNAME | Alias to another domain | www → example.com |
| MX | Mail server for the domain | mail.example.com |
| TXT | Text data (SPF, verification) | v=spf1 include:_spf... |
CDN — Making Websites Fast Globally
CDN (Content Delivery Network) copies your website to servers around the world. When someone visits your site, they download files from the server closest to them — not your origin server, which might be on the other side of the planet.
Without CDN:
User in Sydney → Server in Virginia (16,000 km) → 200ms latency
With CDN (CloudFront):
User in Sydney → Edge server in Sydney (50 km) → 10ms latency
AWS CloudFront has 450+ edge locations worldwide. It also handles HTTPS termination, caching, compression, and DDoS protection.
Part 2: The Tech Stack
This guide uses Next.js for building the site and AWS for hosting. Here's the complete stack:
Build Tools
- Next.js 14 — React framework with static export
- Tailwind CSS — Utility-first CSS framework
- Framer Motion — Animations
- Lucide React — Icons
- Sharp — Image processing (OG images)
AWS Services
- S3 — File storage (private bucket)
- CloudFront — CDN + HTTPS
- Route 53 — DNS management
- ACM — Free TLS certificates
- WAF — Firewall (optional)
{
"scripts": {
"dev": "next dev",
"prebuild": "node scripts/generate-og-images.js && node scripts/convert-svg-to-png.js",
"build": "next build",
"generate-og": "node scripts/generate-og-images.js && node scripts/convert-svg-to-png.js"
},
"dependencies": {
"next": "14.1.0",
"react": "^18",
"tailwindcss": "^3.3.0",
"framer-motion": "^11.0.0",
"lucide-react": "^0.309.0"
},
"devDependencies": {
"sharp": "^0.34.5"
}
}/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // Generates static HTML files
trailingSlash: true, // URLs end with / (e.g., /about/)
images: {
unoptimized: true // Required for static export
}
};
module.exports = nextConfig;What does 'static export' mean?
npm run build generates an /out directory with pure HTML, CSS, and JS files. No server required — just upload these files to any web host (S3, Netlify, Vercel, etc.).Part 3: OG Images for Social Media
OG (Open Graph) images are the preview cards you see when sharing a link on LinkedIn, Twitter, WhatsApp, or Slack. Without them, your links look plain and get fewer clicks.
Required meta tags in your HTML head:
<meta property="og:title" content="Your Page Title" /> <meta property="og:description" content="A brief description" /> <meta property="og:image" content="https://example.com/og/page-name.png" /> <meta property="og:url" content="https://example.com/page-name/" />
PNG is required — not SVG
Our approach: Generate SVGs programmatically (easy to template), then convert to PNG using Sharp.
const fs = require('fs');
const path = require('path');
const articles = {
'my-article': {
title: 'Article Title Here',
description: 'Brief description for social media preview.',
category: 'Technical Guide'
}
};
function generateSVG(slug, article) {
return `<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e293b"/>
<stop offset="100%" style="stop-color:#0f172a"/>
</linearGradient>
</defs>
<rect width="1200" height="630" fill="url(#bg)"/>
<rect x="0" y="0" width="8" height="630" fill="#0d9488"/>
<text x="60" y="80" font-family="system-ui" font-size="14"
font-weight="600" fill="#0d9488">${article.category.toUpperCase()}</text>
<text x="60" y="180" font-family="system-ui" font-size="48"
font-weight="300" fill="white">${article.title}</text>
<text x="60" y="280" font-family="system-ui" font-size="20"
fill="#94a3b8">${article.description}</text>
<text x="60" y="560" font-family="system-ui" font-size="24"
font-weight="600" fill="white">Your Brand</text>
</svg>`;
}
// Generate all OG images
const outputDir = path.join(__dirname, '../public/og');
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
Object.entries(articles).forEach(([slug, article]) => {
const svg = generateSVG(slug, article);
fs.writeFileSync(path.join(outputDir, `${slug}.svg`), svg);
console.log(`Generated: ${slug}.svg`);
});const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const ogDir = path.join(__dirname, '../public/og');
async function convertSvgToPng() {
const files = fs.readdirSync(ogDir).filter(f => f.endsWith('.svg'));
for (const file of files) {
const svgPath = path.join(ogDir, file);
const pngPath = path.join(ogDir, file.replace('.svg', '.png'));
await sharp(svgPath)
.resize(1200, 630)
.png()
.toFile(pngPath);
console.log(`Converted: ${file} → ${file.replace('.svg', '.png')}`);
}
}
convertSvgToPng().catch(console.error);Part 4: AWS Deployment
# Architecture
User Browser
│
▼
Route 53 (DNS: yourdomain.com)
│
▼
CloudFront (CDN + HTTPS + WAF)
│ Origin Access Control (OAC)
▼
S3 Bucket (private, stores files)Step 1: Build the Site
npm run build
# This runs:
# 1. generate-og-images.js → Creates SVGs in public/og/
# 2. convert-svg-to-png.js → Converts SVGs to PNGs
# 3. next build → Exports static HTML to /out directoryThe /out directory now contains everything: HTML pages, CSS, JS bundles, images, OG images, robots.txt, and sitemap.xml.
Step 2: Create S3 Bucket
S3 (Simple Storage Service) stores your website files. Create a private bucket — CloudFront will handle public access.
| Setting | Value |
|---|---|
| Bucket name | your-website-bucket |
| Region | us-east-1 (recommended) |
| Block Public Access | ON — block all |
| Static website hosting | Disabled |
| Versioning | Disabled |
Upload contents, not the folder
/out directory to the bucket root. The bucket should contain index.html at the root, not an out/ folder.Step 3: Create CloudFront Distribution
CloudFront serves your site globally with HTTPS. It connects to S3 using Origin Access Control (OAC), keeping your bucket private.
| Setting | Value |
|---|---|
| Origin | your-bucket.s3.us-east-1.amazonaws.com |
| Origin access | Origin Access Control (OAC) |
| Default root object | index.html |
| Viewer protocol | Redirect HTTP to HTTPS |
| Alternate domains | yourdomain.com, www.yourdomain.com |
| SSL certificate | ACM certificate (us-east-1) |
Critical: Default root object
index.html. Without this, visiting your domain returns an error instead of the homepage.Step 4: CloudFront Function (URL Rewrite)
S3 with OAC doesn't automatically serve index.html for directory paths. A CloudFront Function rewrites URLs before they reach S3.
function handler(event) {
var request = event.request;
var uri = request.uri;
// Add index.html to directory paths
if (uri.endsWith('/')) {
request.uri += 'index.html';
} else if (!uri.includes('.')) {
request.uri += '/index.html';
}
return request;
}
// Examples:
// / → /index.html
// /about/ → /about/index.html
// /articles/my-post/ → /articles/my-post/index.html
// /style.css → /style.css (unchanged)Associate this function with the distribution's default behavior as a Viewer request function.
Step 5: Configure Error Pages
S3 returns 403 (not 404) for missing files when using OAC. Configure CloudFront error responses:
| Error Code | Response Path | Response Code | Why |
|---|---|---|---|
| 403 | /index.html | 200 | Allows client-side routing |
| 404 | /404.html | 404 | Shows custom 404 page |
Step 6: Configure Route 53 DNS
Create A records pointing your domain to CloudFront:
| Record Name | Type | Alias Target |
|---|---|---|
| yourdomain.com | A | d123abc.cloudfront.net |
| www.yourdomain.com | A | d123abc.cloudfront.net |
Part 5: Updating the Site
To deploy changes after editing your code:
# 1. Build the site
npm run build
# 2. Upload to S3 (using AWS CLI)
aws s3 sync out/ s3://your-bucket-name --delete
# 3. Invalidate CloudFront cache
aws cloudfront create-invalidation \
--distribution-id YOUR_DIST_ID \
--paths "/*"Cache invalidation is critical
/* path invalidates everything. First 1,000 invalidations per month are free.Quick Reference
Key Concepts
- HTTP: Unencrypted web protocol (port 80)
- HTTPS: Encrypted web protocol (port 443)
- TLS: Encryption layer for HTTPS
- DNS: Translates domains to IP addresses
- CDN: Distributes content globally
AWS Services
- S3: File storage (keep private)
- CloudFront: CDN + HTTPS + caching
- Route 53: DNS management
- ACM: Free TLS certificates
- OAC: Secure S3 to CloudFront connection
Monthly Cost
$0/month for typical traffic on CloudFront free tier. S3 storage cost is negligible (<$0.01/month for ~5MB).