Knowledge Base

Next.js Static Export and Private Page Security

Understanding how Next.js static export handles private pages and the security implications for protecting sensitive content.

The Architecture

When using output: 'export' in Next.js, the build produces:

out/
├── _next/
│   └── static/
│       └── chunks/        # ALL JS/CSS for entire site (public)
├── private/
│   ├── me/
│   │   ├── index.html     # Private page content
│   │   └── index.txt      # RSC payload
│   └── ...
├── writings/
└── index.html

Key insight: There is only ONE _next/static/ directory at the root. All JavaScript chunks for the entire site (including private pages) are placed here.

What Gets Protected

With edge middleware protecting /private/*:

AssetLocationProtected
Private HTML/private/**/*.htmlYes (302 redirect)
Private RSC payloads/private/**/*.txtYes (302 redirect)
Shared JS chunks/_next/static/chunks/*.jsNo (publicly accessible)

The Soft Leak

Private page component code ends up in public JS chunks:

// Publicly accessible at /_next/static/chunks/abc123.js
{href:"/private/me"}
"← Back to Dashboard"
{value:"notes"}

What leaks:

  • Route paths (/private/me, /private/notes)
  • UI labels ("Dashboard", "Notes", "Drafts")
  • Component structure and logic

What does NOT leak:

  • Actual content (KB articles, notes, draft posts)
  • User data
  • API responses

Why This Happens

Next.js chunks are split by module graph, not by route protection:

  1. Shared components (layout, nav) reference private routes
  2. Private page components get bundled into shared chunks
  3. All chunks go to global /_next/static/
  4. No per-route output directory support

Can We Fix It?

What Doesn't Work

ApproachWhy It Fails
assetPrefixGlobal setting, not per-route
basePathChanges entire app, not specific routes
Webpack splitChunksNo per-entry output path support
Custom output directoryNot supported by Next.js

Workarounds

1. Accept the Soft Leak (Simplest)

For personal sites or internal tools, the component code leak may be acceptable since actual content remains protected.

2. Client-Side Only Private Pages

Don't statically generate private pages. Fetch content after auth:

// app/private/me/page.tsx
'use client';
 
import { useQuery } from '@tanstack/react-query';
 
export default function PrivateDashboard() {
  const { data, isLoading } = useQuery({
    queryKey: ['dashboard'],
    queryFn: () => fetch('/api/dashboard').then(r => r.json()),
  });
 
  if (isLoading) return <Skeleton />;
  return <Dashboard data={data} />;
}

Benefits:

  • No private components in static chunks
  • Content fetched from API after middleware auth
  • Middleware still protects the route shell

Trade-offs:

  • No SSG benefits for private pages
  • Requires API endpoints for private data
  • Loading states required

3. Separate Deployment

Split into two Next.js apps:

  • Public app at kai.land
  • Private app at private.kai.land (with its own /_next/static/)

Trade-offs:

  • Two builds, two deployments
  • Shared components must be duplicated or packaged

4. Post-Build Script

Move private-specific chunks after build:

# Identify chunks only used by private pages
# Move them to /private/_next/static/
# Update HTML references

Trade-offs:

  • Fragile (chunk filenames change each build)
  • Must track which chunks are private-only
  • Complex to maintain

Verification Commands

Check if directory listing is enabled:

curl -s -o /dev/null -w "%{http_code}" "https://example.com/_next/static/"
# Should return 404 (no listing)

Check if private routes are protected:

curl -s -o /dev/null -w "%{http_code}" "https://example.com/private/me"
# Should return 302 (redirect to login)

Search for private UI strings in public chunks:

find out/_next -name "*.js" | xargs grep -l "Dashboard\|Private Notes"

Check what JS a private page references:

grep -o '/_next/static/chunks/[^"]*\.js' out/private/me/index.html | sort -u

Recommendation

For most use cases with edge middleware on Cloudflare Pages:

  1. Content is protected - actual sensitive data never leaks
  2. Soft leak is acceptable - UI structure visible but not harmful
  3. If strict security needed - use client-side rendering for private pages

The middleware approach protects what matters (content), even if it can't hide that private routes exist.

Related