My personal site uses Next.js, but it does not need a Next.js server running all day.
The pages are built into static files, stored in a private S3 bucket, and served through CloudFront. Dynamic requests go to a separate API. This works well for a portfolio or blog whose pages change when you deploy.
The important question comes first: can your site be exported as static files? If it needs server-side rendering for each request, this setup is the wrong fit.
Export the Next.js site
Set the output mode in next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;
Run next build. Next.js writes the HTML, CSS, JavaScript, and other static assets into out/.
Some server features will not work in a static export. Check the Next.js static export guide before choosing this architecture, especially if your pages rely on request-time data, server actions, or built-in image optimization.
Keep S3 private
I use a regular S3 bucket as the CloudFront origin, with S3 Block Public Access enabled. Origin Access Control (OAC) grants the CloudFront distribution permission to read the objects.
Visitors reach CloudFront over HTTPS; they do not read files directly from the bucket. This is different from making an S3 website endpoint public. CloudFront also caches the static files close to readers.
Handle clean URLs
A static export may produce a file such as blog.html, while a visitor requests /blog. S3 will not automatically map that request to the file you meant.
I use a CloudFront Function on viewer requests to rewrite extensionless paths:
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri === "/" || uri.includes(".")) {
return request;
}
request.uri = uri.endsWith("/")
? uri + "index.html"
: uri + ".html";
return request;
}
Test this against your actual export output. The right rewrite depends on your trailingSlash setting and route structure. Check nested pages, assets, and 404s before calling it done.
Put the API behind the same domain
Static pages do not prevent the site from having dynamic features.
CloudFront can send normal page requests to S3 and route /api/* to an API Gateway origin backed by Lambda. The browser then calls /api/... on the same domain that served the page.
That simplifies browser origin handling. Configure the API cache behavior deliberately: forward the headers, query strings, and cookies your API needs, and do not accidentally cache personalized responses.
Deploy a change
The basic deployment flow is build, upload, then invalidate content that needs to refresh:
next build
aws s3 sync out s3://your-bucket --delete
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"
For a small personal site, a broad invalidation is simple. As the site grows, use sensible cache headers and more targeted invalidations so every deployment does not clear every cached asset.
This architecture has no always-on application server for the frontend. It still has S3, CloudFront, DNS, and potentially API costs, so check current CloudFront pricing for your configuration rather than assuming it will be free.
I use this setup for my own site. The original article has more context on why I chose it.
Where have static exports worked well for you, and where did you end up needing a server?
Top comments (0)