Fix Localhost 404 Errors for Next.js Blog Articles
You built your Next.js blog. Articles render perfectly in production. Then you hit localhost:3000/blog/my-article in development and get a pristine 404. Your route handler is there. Your markdown files are there. Nothing broke during deploy. You’ve just encountered a silent friction point that costs indie builders hours: dev and production serving static content through different paths.
Why This Happens
Next.js development server and production builds treat static files differently. The dev server runs from your project root and watches for changes in real time. Production builds optimize static asset delivery through the .next/ directory and CDN caches. If your blog articles live in public/blog/ but your API or page component expects them elsewhere, or if the dev server hasn’t been configured to hot-reload those specific files, you see a 404 locally while production works fine.
This gap widens when you’re testing locally without rebuilding, or when your route configuration assumes production optimization that doesn’t exist yet in dev.
Solution 1: Direct Public Folder Serving
The simplest approach: store articles in /public/blog/ and serve them directly.
An article at public/blog/my-first-article.md becomes accessible as http://localhost:3000/blog/my-first-article.md. But if your frontend expects /blog/my-first-article (no .md), you need a catch-all route to intercept and transform it.
Add a dynamic route: app/blog/[slug]/page.js
import fs from 'fs/promises';
import path from 'path';
export default async function BlogPost({ params }) {
const { slug } = params;
const filePath = path.join(process.cwd(), 'public', 'blog', `${slug}.md`);
try {
const raw = await fs.readFile(filePath, 'utf-8');
const html = parseMarkdown(raw); // your markdown parser
return <article>{html}</article>;
} catch {
return <div>404: Article not found</div>;
}
}
This works in dev and production identically. The catch: every request parses the file from disk, so if you have 100+ articles, you’ll want to cache results or preload at build time.
Solution 2: Dedicated API Route (Recommended for Scale)
Instead of relying on static file serving, create an API endpoint that reads articles on demand:
// app/api/blog/[slug]/route.js
import fs from 'fs/promises';
import path from 'path';
export async function GET(request, { params }) {
const { slug } = params;
const filePath = path.join(process.cwd(), '_content', `${slug}.md`);
try {
const content = await fs.readFile(filePath, 'utf-8');
return Response.json({ content, slug });
} catch (error) {
return Response.json({ error: 'Article not found' }, { status: 404 });
}
}
From your page component, fetch the article:
const res = await fetch(`/api/blog/${slug}`);
const { content } = await res.json();
Why this is better:
- Works identically on localhost:3000 dev mode and production builds
- You can add filtering, search, pagination, or access control at the API layer later without changing your page structure
- Separates content storage (_content/ directory) from public assets, reducing confusion
- Easier to monitor and cache at the edge in production
Validation: Test Both Environments
After implementing either solution:
- Dev mode:
npm run dev→ navigate tohttp://localhost:3000/blog/test-article→ article should load. Check your terminal for any 404 errors. - Production build:
npm run build && npm start→ same URL → article should load identically. No runtime differences. - DevTools Network tab: confirm articles load from
/api/blog/or/public/blog/, not from a 404 response. - Hard refresh (
Ctrl+Shift+RorCmd+Shift+R): clear cache and reload to catch any browser caching issues.
If you see a 404 in dev but not in production (or vice versa), the mismatch is in your route definition or file path. Check:
- File exists at the path you’re reading (use
ls -la _content/to verify) - Route slug matches filename (if slug is
my-article, file should bemy-article.md, notmy_article.md) - No trailing/leading slashes in paths
Why This Matters for Your Product
Your blog is part of your SaaS distribution strategy. Readers arrive from search, social, or links and expect a frictionless experience. A 404 in development that’s fixed in production is a hidden regression—you won’t catch broken links, corrupted markdown, missing metadata, or performance issues until users do. And they’ll leave.
This is especially critical if you’re using your blog as a lead-generation channel or customer onboarding tool. Every 404 is a dropped conversion and a hit to your credibility.
Next Steps
When Trove Deck Solution builds custom SaaS products, we architect content layers—blogs, documentation, help centers—so dev and production environments are indistinguishable. This kind of dev/prod parity is caught early during the architecture review phase, before a line of code ships. If you’re building a SaaS with a content or documentation layer and want to avoid these pitfalls, or you’re ready to turn your product idea into reality, let’s talk. Trove Deck Solution is here to help you ship with confidence.