Manoj SutharMicroservices · GenAI · Cloud
Back to all articles

Architecting High-Performance SSG with Next.js, AWS S3, and CloudFront

A deep technical dive into configuring static site export with Next.js App Router, CloudFront Origin Access Control (OAC), and automated cache invalidation pipelines.

Manoj Suthar
3 min read

Introduction

Static Site Generation (SSG) remains the gold standard for developer blogs, technical documentation, and high-traffic knowledge bases. By decoupling the content generation step from runtime request serving, static architectures deliver:

  1. Sub-50ms Time-to-First-Byte (TTFB) via global Edge CDN caches.
  2. Infinite Scalability with zero server runtime or database bottlenecks.
  3. Rock-Solid Security because static file buckets have no server attack surface.

In this article, we break down how to configure Next.js App Router for pure static export (output: 'export'), protect your S3 origin with Origin Access Control (OAC), and automate deployments with GitHub Actions.


Next.js Static Export Configuration

To compile your Next.js application into pure static HTML, CSS, and client-side JavaScript, enable output: 'export' inside next.config.ts:

next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  output: "export",
  images: {
    unoptimized: true, // Required for pure static host without Node server
  },
  reactStrictMode: true,
};
 
export default nextConfig;

When you execute next build, Next.js will prerender every page into an out/ directory.


Dynamic Route Pre-Generation with generateStaticParams

For dynamic routes such as /blog/[slug], Next.js needs to know all possible URL parameters ahead of time during the build:

src/app/blog/[slug]/page.tsx
import { getPostSlugs, getPostBySlug } from "@/lib/mdx";
import { notFound } from "next/navigation";
 
export async function generateStaticParams() {
  const slugs = getPostSlugs();
  return slugs.map((slug) => ({ slug }));
}
 
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = getPostBySlug(slug);
 
  if (!post) {
    notFound();
  }
 
  return (
    <article className="prose dark:prose-invert max-w-none">
      <h1>{post.frontmatter.title}</h1>
      <p className="text-muted-foreground">{post.readingTime}</p>
      {/* Render MDX content */}
    </article>
  );
}

Securing AWS S3 with CloudFront Origin Access Control (OAC)

Never configure your S3 bucket as a public website. Instead, keep the bucket strictly private and authorize CloudFront via an Origin Access Control (OAC) bucket policy:

s3-bucket-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipalReadOnly",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::manojsuthar-blog-static/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD6EXAMPLE"
        }
      }
    }
  ]
}

Cache Invalidation Strategy

Static files are cached on CloudFront edge locations according to your Cache-Control headers. When deploying updates:

deploy.sh
# 1. Sync static directory with deletion
aws s3 sync out/ s3://manojsuthar-blog-static --delete
 
# 2. Invalidate CloudFront distribution edge caches
aws cloudfront create-invalidation \
  --distribution-id EDFDVBD6EXAMPLE \
  --paths "/*"

Conclusion

Combining Next.js SSG with AWS S3 and CloudFront provides an uncompromising developer experience, instant load times, and minimal infrastructure maintenance costs.

Enjoyed this article? Share it with your network: