The Hidden Culprits of Migrating from SSG to SSR in Astro
The Hidden Culprits of Migrating from SSG to SSR in Astro
“It should be a simple configuration change” — Famous last words before spending three days debugging why your blog posts are returning 404s.
Last week, we migrated our Astro blog from Static Site Generation (SSG) to Server-Side Rendering (SSR). What seemed like a straightforward config change turned into a debugging odyssey that exposed some non-obvious gotchas. Here’s what we learned the hard way, so you don’t have to.
The Context: Why SSR?
Before diving into the pain points, let’s establish why we made this move. Our blog needed dynamic content capabilities — user-specific recommendations, real-time comments, and A/B testing. SSG’s pre-built pages couldn’t handle these requirements efficiently.
The Real Culprits We Found
1. Content Collection Loaders Break Everything
The Problem: Our content.config.ts had a custom loader that worked perfectly in SSG mode:
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) => z.object({
title: z.string(),
// ... other fields
}),
});
In SSR mode, this loader prevented entry.render() from working, causing all our blog posts to fail rendering.
The Fix: Remove the custom loader entirely. Astro’s default behavior handles Markdown and MDX files automatically:
const blog = defineCollection({
schema: ({ image }) => z.object({
title: z.string(),
// ... other fields
}),
});
Why This Happens: Custom loaders in SSG mode pre-process content at build time. SSR mode needs the flexibility to render content on-demand, and custom loaders can interfere with this process.
2. getStaticPaths() Becomes Your Enemy
The Problem: Our dynamic route [...slug].astro used getStaticPaths() to pre-generate all blog post routes:
export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => ({
params: { slug: entry.slug },
props: { entry },
}));
}
In SSR mode, this function shouldn’t exist at all.
The Fix: Replace static path generation with dynamic route handling:
---
import { getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
const { slug } = Astro.params;
const allEntries = await getCollection('blog');
const entry = allEntries.find(e => e.slug === slug);
if (!entry) {
return Astro.redirect('/404');
}
const { Content } = await entry.render();
---
3. The Slug vs ID Confusion
The Problem: We were using post.id for URL generation, which includes file extensions and doesn’t match how Astro handles slugs in SSR mode.
The Fix: Always use post.slug for clean URLs:
// Before (SSG)
<a href={`/blog/${post.id}/`}>
// After (SSR)
<a href={`/blog/${post.slug}/`}>
10 More Typical Mistakes to Avoid
4. Forgetting to Update astro.config.mjs
The Mistake: Leaving your config in SSG mode while changing your code for SSR.
The Fix:
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: vercel(), // or your preferred adapter
});
5. Client-Side Hydration Assumptions
The Mistake: Assuming all your client-side JavaScript will work the same way.
The Reality: SSR changes when and how your client-side code executes. What worked in SSG might not work in SSR due to different hydration timing.
The Fix: Test all interactive components thoroughly and use proper hydration directives.
6. Image Optimization Breakage
The Mistake: Not updating image imports and optimizations for SSR.
The Problem: Static image processing that worked in SSG might not work in SSR mode.
The Fix: Review all astro:assets usage and ensure images are handled correctly in server environments.
7. Environment Variable Mismanagement
The Mistake: Using import.meta.env variables that should be private on the server.
The Reality: In SSR, some environment variables might be exposed to the client unintentionally.
The Fix: Use import.meta.env.SSR to conditionally handle environment variables.
8. Caching Strategy Blindness
The Mistake: Not implementing proper caching for SSR routes.
The Reality: SSR without caching can be significantly slower than SSG.
The Fix: Implement appropriate caching strategies for your content and API calls.
9. Build Process Assumptions
The Mistake: Assuming your build process will remain the same.
The Reality: SSR builds are fundamentally different from SSG builds.
The Fix: Update your CI/CD pipeline to handle SSR deployment requirements.
10. Memory Leak Accumulation
The Mistake: Not monitoring memory usage in long-running SSR processes.
The Reality: SSR processes can accumulate memory leaks over time, especially with dynamic content.
The Fix: Implement proper memory monitoring and process recycling strategies.
11. Cold Start Performance
The Mistake: Not optimizing for cold starts in serverless environments.
The Reality: SSR on serverless platforms can have significant cold start delays.
The Fix: Optimize bundle size and use appropriate serverless adapters.
12. Session Management Confusion
The Mistake: Not properly handling session state in SSR.
The Reality: SSR introduces server-side session management complexities.
The Fix: Implement proper session handling and state management.
13. SEO Metadata Generation
The Mistake: Assuming dynamic SEO metadata works the same way.
The Reality: SSR changes how and when SEO metadata is generated.
The Fix: Test all SEO metadata generation thoroughly in SSR mode.
The Migration Checklist
Based on our experience, here’s a practical checklist for SSG to SSR migration:
- Update
astro.config.mjswith SSR configuration - Remove custom loaders from content collections
- Replace
getStaticPaths()with dynamic route handling - Update URL generation to use
sluginstead ofid - Test all content collection queries
- Verify image optimization still works
- Update environment variable handling
- Implement caching strategies
- Update build and deployment process
- Test cold start performance
- Verify SEO metadata generation
- Monitor memory usage
- Test all interactive components
The Bottom Line
Migrating from SSG to SSR in Astro isn’t just a configuration change — it’s a fundamental shift in how your application renders content. The debugging time you spend upfront understanding these gotchas is infinitely better than the production incidents you’ll avoid.
The key insight? SSR and SSG handle content, routing, and rendering differently at a fundamental level. What works in one mode might not work in the other, and assumptions about static behavior don’t hold in dynamic environments.
Have you encountered other SSG to SSR migration issues? Share your war stories in the comments below. We’re all learning together.