4.1 KiB
Blog 500 Error — Debug Report
Agent: backend-specialist Date: 2026-03-21 Priority: High Status: RESOLVED
Summary
Individual blog post pages (/blog/<slug>) returned a 500 Internal Server Error because src/pages/blog/[...slug].astro used getStaticPaths() — a Static Site Generation (SSG) API that is not valid in SSR (output: 'server') mode.
Root Cause
The Conflict
| File | Setting |
|---|---|
astro.config.mjs |
output: 'server' (full SSR, Node adapter) |
src/pages/blog/[...slug].astro |
Used getStaticPaths() (SSG-only API) |
Astro's getStaticPaths() is only valid in output: 'static' mode. When the server receives a request for /blog/getting-started-with-astro, Astro attempts to call getStaticPaths() at request time, which is unsupported and causes a runtime error propagated as a 500.
Error Flow
GET /blog/getting-started-with-astro
→ Astro SSR handler invokes [...slug].astro
→ getStaticPaths() is called at request time
→ Astro throws: "getStaticPaths() is not available in server mode"
→ middleware.ts catches and re-throws (line 73)
→ 500 Internal Server Error returned to client
Files Examined
| File | Finding |
|---|---|
astro.config.mjs |
output: 'server' with @astrojs/node standalone adapter |
src/pages/blog/[...slug].astro |
Used getStaticPaths() — incompatible with SSR |
src/pages/blog/index.astro |
Correctly uses getCollection() at the top level (valid in SSR) |
src/content/config.ts |
Schema is valid; blog collection properly defined |
src/content/blog/*.md |
All 3 posts have valid frontmatter matching the schema |
src/middleware.ts |
Re-throws errors from next() — confirms 500 path |
Fix Applied
File: src/pages/blog/[...slug].astro
Before (broken SSG pattern)
import { getCollection, type CollectionEntry } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
type Props = { post: CollectionEntry<'blog'> };
const { post } = Astro.props;
const { Content } = await post.render();
After (correct SSR pattern)
import { getEntry } from 'astro:content';
const { slug } = Astro.params;
if (!slug) {
return Astro.redirect('/blog');
}
const post = await getEntry('blog', slug);
if (!post || post.data.draft) {
return Astro.redirect('/404');
}
const { Content } = await post.render();
Key Changes
- Removed
getStaticPaths()entirely - Replaced
getCollectionimport withgetEntry - Reads
slugdynamically fromAstro.paramsat request time - Handles missing/draft posts with a redirect to
/404 - No
Propstype annotation needed (props come from params now)
Why getEntry() is the Correct SSR Approach
In SSR mode, Astro dynamically serves each request. The URL slug is available at runtime via Astro.params. getEntry('blog', slug) fetches a single content collection entry by its slug — this is the official Astro SSR pattern for content collections.
Secondary Observations
These are not the cause of the 500 but are worth noting:
-
index.astroworks fine — it usesgetCollection()directly in the frontmatter (not ingetStaticPaths()), which is valid in SSR since it runs per-request. -
Content is valid — all 3 markdown posts pass the Zod schema in
config.ts. No frontmatter issues. -
Middleware correctly re-throws — the error handling in
middleware.tswas functioning as designed; the 500 originated from the page handler, not the middleware itself. -
Blog index not affected —
/blog(list page) was working correctly; only individual post routes were broken.
Testing the Fix
After deploying, verify the following URLs return 200:
GET /blog/getting-started-with-astro → 200 OK
GET /blog/ai-transforming-business → 200 OK
GET /blog/cloud-migration-guide → 200 OK
GET /blog/nonexistent-slug → redirect to /404