Fix Next.js Localhost 404: Serve Static Blog Posts in Dev
Listen to this article
Why Your Static Blog Posts Throw 404 in Next.js Localhost
Your npm run dev spins up clean. You type localhost:3000/blog/my-first-post. The page returns a crisp 404. Meanwhile, Vercel deploys the same code without a hitch.
This isn’t random. It’s a structural mismatch between how Next.js handles static generation in dev mode versus production. In development, Next.js skips certain optimization passes — including the filesystem-based route resolution that makes your blog posts exist in the first place.
Unexpected 404s during local development are a common Next.js complaint. The blog route pattern amplifies this because it relies on getStaticPaths + filesystem imports.
Definition:
getStaticPaths= Next.js function that pre-generates page routes at build time based on your data source (filesystem, API, database).
What Causes the 404 Error in Development Mode?
The 404 happens when Next.js can’t resolve a dynamic route to an actual page file during development. Unlike production (where next build pre-renders everything), dev mode is lazy — it only generates pages when you visit them.
Here’s the root cause chain:
- Your
[slug].tsxfile exists in/pages/blog/ - You import markdown files using
fs.readdirSync()or similar - In dev mode, the filesystem import timing differs
- Next.js can’t match the URL to a pre-generated path
- Result: 404
This is different from a missing file. Your code is correct. The runtime behavior just changes between modes.
5-Step Fix: Serve Static Articles in Next.js Dev
Here’s the exact debugging sequence:
Step 1: Verify Your File Structure
Next.js expects a specific folder hierarchy. Blog markdown files should live in one of two places:
/public/content/blog/(accessible via URL)/src/content/blog/(import-only)
Do not put markdown files in /pages/blog/ alongside your route files. This creates import conflicts.
Step 2: Check getStaticPaths Export
Your [slug].tsx must export a properly typed getStaticPaths function. Missing the export — or typing it as any — silently breaks dev resolution.
// pages/blog/[slug].tsx
export async function getStaticPaths() {
const posts = getAllPostSlugs(); // your slug-fetching function
return {
paths: posts.map((slug) => ({ params: { slug } })),
fallback: false, // or 'blocking' — never true for blogs
};
}
Key detail: fallback: false tells Next.js to 404 on unknown slugs immediately. fallback: 'blocking' generates the page on first visit. For blogs with finite content, false is cleaner.
Step 3: Fix Your Import Path Resolution
The most common silent failure: relative imports that resolve in production but not in dev.
If you’re using fs.readFileSync() to load markdown:
// WRONG — works in production, fails in dev
const content = fs.readFileSync(`./content/blog/${slug}.md`);
// RIGHT — use path.join with process.cwd()
import path from 'path';
const contentDir = path.join(process.cwd(), 'content', 'blog');
const content = fs.readFileSync(path.join(contentDir, `${slug}.md`), 'utf8');
process.cwd() always points to your project root. Relative paths can shift based on where Node.js thinks it’s running from.
Step 4: Clear the .next Cache
Next.js caches aggressively. Old build artifacts can mask your fixes.
Run this after any structural change:
rm -rf .next && npm run dev
On Windows, use rd /s /q .next instead. This forces a clean dev server start.
Step 5: Verify next.config.js Static File Rules
If you’re serving markdown from /public/, check your config doesn’t block static file serving:
// next.config.js
module.exports = {
// Only needed if you have custom webpack rules
// that accidentally exclude .md files
webpack: (config) => {
return config;
},
};
Most projects don’t need webpack customization for markdown. If you added custom rules for syntax highlighting (like next-remote-mdx), verify .md isn’t excluded.
How Does Development Mode Differ from Production?
Understanding this distinction prevents future headaches:
| Aspect | Development (npm run dev) |
Production (next build + start) |
|---|---|---|
| Static generation | On-demand (lazy) | Pre-rendered at build time |
| Filesystem caching | Minimal | Aggressive |
| Error visibility | Detailed stack traces | Optimized bundles |
getStaticPaths |
Called per-request | Called once at build |
| Performance | Slower (unoptimized) | Fast (compiled) |
The takeaway: development mode is a simulation, not a replica. Things that “just work” in production may require explicit configuration in dev.
When Should You Use getStaticProps vs getServerSideProps?
This is a related decision that affects your 404 behavior:
getStaticProps: Content that rarely changes (blog posts, documentation). Pre-rendered at build time. Best for SEO.getServerSideProps: Content that changes per-request (user dashboards, real-time data). Rendered on every visit.
For blog articles, always use getStaticProps. If your content updates frequently, consider revalidate in ISR (Incremental Static Regeneration) instead of switching to SSR.
Switching from getStaticProps to getServerSideProps to “fix” dev 404s only trades a local annoyance for server costs that climb every month. The 404 is the symptom, not the disease.
FAQ: Common Next.js Blog 404 Questions
Why does my blog work on Vercel but not locally?
Vercel runs next build automatically, which pre-renders all static pages. Local development skips this step. Your code is identical — the runtime behavior differs.
Do I need getStaticPaths for every dynamic route?
Yes, if the route uses a bracket parameter like [slug] or [id]. Without it, Next.js can’t know which pages to generate.
Can I debug getStaticPaths execution?
Add console.log() inside the function. Next.js logs these to your terminal during dev mode. You’ll see exactly what paths are being generated.
The Real Issue: Dev ≠ Production
Here’s the insight most tutorials miss: Next.js development mode is intentionally different from production. It prioritizes fast refresh over accurate static generation. This is a feature, not a bug.
The 404 you’re seeing isn’t a problem with your code. It’s Next.js telling you that your static generation setup needs to be production-ready before dev mode will cooperate.
Fix the code. Clear the cache. Verify the imports. Your blog posts will load in localhost every time.
Building a SaaS or custom web app and need engineering support? Trove Deck builds SaaS and custom web apps, including Next.js deployments. Let’s talk through your architecture.
