Next.js static export is not a silver bullet for building fast, scalable websites; it’s a trade-off that introduces significant limitations around dynamic data and server-side functionality.
Imagine you have a blog. For a statically exported Next.js site, each page (like a blog post) is rendered into a static HTML file at build time. This means when a user requests /my-awesome-post, the server just hands over a pre-built my-awesome-post.html file. Super fast.
Here’s a live example of a static page generated by Next.js. Notice how there’s no server logic to execute on request.
<!DOCTYPE html>
<html>
<head>
<title>My Static Blog Post</title>
<meta name="description" content="This is a pre-rendered static page.">
</head>
<body>
<h1>My Awesome Post</h1>
<p>This content was generated when the site was built.</p>
</body>
</html>
This simplicity is powerful for SEO and performance, but it breaks down when your content isn’t fixed at build time.
The Core Limitation: No Server-Side Logic
The fundamental limitation of next export is that it produces only static files. There’s no Node.js server running on your hosting provider to execute any JavaScript after the build. This means:
- No dynamic API routes: You can’t have
/api/usersthat fetches data from a database because there’s no server to run that fetching logic. - No real-time data fetching: Pages that rely on data that changes frequently (e.g., stock prices, live scores) will show stale information because the data was fetched only once during the build.
- No user-specific content: Personalization (e.g., "Welcome, [Username]") is impossible because the server can’t know who the user is.
- No form submissions: Server-side form handling is out.
Workarounds: When Static Isn’t Enough
When you hit these walls, you need to augment your static export with other services.
1. Client-Side Data Fetching:
For data that can change but doesn’t need to be on the initial HTML payload, fetch it in the browser using useEffect and fetch (or a library like SWR or React Query).
-
Diagnosis: You see stale data on your page, or you need to display data that changes after the initial page load.
-
Fix: In your React component, use
useEffectto fetch data when the component mounts.import { useEffect, useState } from 'react'; function MyDynamicComponent() { const [data, setData] = useState(null); useEffect(() => { async function fetchData() { const response = await fetch('/api/some-data'); // This /api/some-data must be a *separate* serverless function or deployed API const result = await response.json(); setData(result); } fetchData(); }, []); // Empty dependency array means this runs once on mount if (!data) return <p>Loading...</p>; return <div>{data.message}</div>; } -
Why it works: The static HTML is served immediately. Then, the JavaScript in the user’s browser runs, fetches the dynamic data from a separate API endpoint, and updates the DOM.
2. Serverless Functions for API Routes:
If you need API endpoints (e.g., for form submissions, or to serve dynamic data that then gets fetched client-side), deploy them as serverless functions (like AWS Lambda, Vercel Functions, Netlify Functions).
-
Diagnosis: You need an
/apiroute that performs actions or fetches dynamic data, butnext exportdoesn’t support it. -
Fix: Create your API routes in the
pages/apidirectory as usual. When deploying withnext export, you’ll need to configure your hosting provider to route requests to/api/*to your serverless function deployment. For example, on Vercel, this is handled automatically. On Netlify, you’d place your functions in anetlify/functionsdirectory.// pages/api/some-data.js export default function handler(req, res) { res.status(200).json({ message: 'This data is dynamic!' }); } -
Why it works: The static Next.js app is served from a CDN. Requests to
/api/*are intercepted by your hosting provider and routed to a separate, ephemeral serverless function that can execute Node.js code.
3. Static Site Generators (SSGs) with External Data Sources:
For dynamic content that can be fetched at build time, use getStaticProps. This is still part of static export, but it means your data source needs to be accessible during the build process.
-
Diagnosis: You have content that changes, but not so frequently that you need real-time updates. You want it pre-rendered for maximum performance.
-
Fix: Use
getStaticPropsto fetch data during the build.// pages/posts/[id].js export async function getStaticPaths() { const posts = await fetch('https://your-cms.com/api/posts').then(res => res.json()); const paths = posts.map(post => ({ params: { id: post.id } })); return { paths, fallback: false }; // fallback: false means 404 for unknown paths } export async function getStaticProps({ params }) { const res = await fetch(`https://your-cms.com/api/posts/${params.id}`); const post = await res.json(); return { props: { post } }; } function Post({ post }) { return ( <div> <h1>{post.title}</h1> <p>{post.content}</p> </div> ); } export default Post; -
Why it works:
getStaticPropsruns only duringnext build. The fetched data is embedded directly into the generated HTML file for that specific page, making it available immediately on load without any client-side fetching or server-side computation.
4. Incremental Static Regeneration (ISR) - Not for next export:
This is a crucial distinction: ISR is not compatible with next export. ISR allows you to update static pages after deployment without a full rebuild, by revalidating them on a timer or on demand. If you need ISR, you must use a Node.js server (like next start or Vercel’s Next.js runtime).
-
Diagnosis: You need static performance but also content that updates periodically without manual rebuilds.
-
Fix: Use
getStaticPropswith therevalidateoption, but deploy usingnext startor a platform that supports Next.js’s Node.js runtime.export async function getStaticProps() { const res = await fetch('https://api.example.com/data'); const data = await res.json(); return { props: { data, }, revalidate: 60, // Revalidate this page every 60 seconds }; } -
Why it works: The page is generated statically. When a request comes in after the
revalidateperiod, Next.js serves the old page immediately but triggers a regeneration in the background. The next request will get the newly generated page.
5. Third-Party Services for Dynamic Features:
For things like comments, forms, or search, leverage dedicated third-party services.
-
Diagnosis: You need features that inherently require server-side logic or user interaction management.
-
Fix: Integrate services like Disqus for comments, Algolia for search, or Formspree for form submissions. You’ll typically embed their JavaScript snippets into your statically generated pages.
<!-- Example: Embedding a comment system widget --> <div id="disqus_thread"></div> <script> var disqus_config = function () { this.page.url = "{{ .Permalink }}"; // Your page's canonical URL this.page.identifier = "{{ .File.UniqueID }}"; // A unique identifier for the page }; (function() { // DON'T EDIT BELOW THIS LINE var d = document, s = d.createElement('script'); s.src = 'https://your-disqus-shortname.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })(); </script> -
Why it works: These services handle all the server-side complexities for you. Your static Next.js app just acts as the client, loading their scripts and displaying their dynamic content.
When you’re using next export, you’re essentially building a set of HTML, CSS, and JS files that can be hosted on any static file server or CDN. Any dynamic behavior or data needs to be handled either by client-side JavaScript or by offloading the "server" part to a separate service, like serverless functions or a Jamstack-compatible CMS.
The next thing you’ll likely encounter is managing large numbers of static pages, which can lead to long build times and an unwieldy number of files.