Complete Guide to Astro SSR: Enable Server-Side Rendering in 3 Steps and End Your Tech Stack Confusion

Introduction
Ever experienced this? Your Astro blog runs blazingly fast with a Lighthouse score of 95+, and just as you’re feeling proud, your boss says, “Let’s add user login functionality.” Suddenly, you’re stuck. How do you implement user login on a static site? You dive into the official documentation, and a flood of concepts like SSR, SSG, Hybrid, and adapters hits you, making your head spin.
To be honest, that’s exactly how I felt when I first encountered Astro SSR. Astro’s selling point is speed, so won’t adding server-side rendering make it slow? With so many adapters like Vercel, Netlify, and Node.js, which one should you choose? What do those output and prerender settings in the config file actually mean?
Actually, configuring Astro SSR isn’t as complicated as you think. In this article, I’ll explain in the most straightforward way: when you absolutely need SSR (instead of sticking with SSG), how to quickly configure various adapters, and how to use SSR and SSG simultaneously in one project (Hybrid mode). After reading this, you’ll be able to independently determine whether your project needs SSR and configure it within 30 minutes.
Chapter 1: SSR Fundamentals and Tech Stack Selection
When Do You Need SSR Instead of SSG?
Let’s start with the simplest criterion: Is your content determined at build time, or does it potentially change with every request?
SSG (Static Site Generation) is like a restaurant’s pre-made set meals. The chef prepares everything in the morning, and when customers arrive, the food is served immediately—super fast. Blog posts, product pages, and “About Us” sections—content that rarely changes—are perfect for SSG.
SSR (Server-Side Rendering) is like cooking to order. After the customer places an order, the chef prepares the dish based on your requirements right then. The “Welcome back, John” message a user sees after logging in, real-time stock prices, the number of items in a shopping cart—these vary for each person and must use SSR.
You might wonder, does my project actually need SSR? If any of these 5 scenarios apply to you, you should consider SSR:
1. User Authentication and Personalized Content
The most typical example is login. You can’t know at build time who will log in or what username to display. For instance, in a learning platform I built, the homepage needed to show “Continue Learning: Lesson 5,” which required SSR to dynamically generate content based on the logged-in user’s progress.
2. Real-Time Data Display
Weather forecasts, stock quotes, sports scores. This data changes every minute—you can’t rebuild your website every minute, right? With SSR, you fetch the latest data every time a user visits.
3. Database Queries
E-commerce product search—each keyword yields different results, and you can’t pre-generate every possible search result page. With SSR, you query the database in real-time when users search and return results.
4. API Routes
Form submissions, file uploads, third-party API calls—all require backend logic. Astro’s SSR mode supports creating API routes (src/pages/api/xxx.js), so you don’t need a separate backend server.
5. A/B Testing and Personalized Recommendations
Displaying different content based on user location, visit time, or browsing history. For example, Taobao’s homepage shows different recommended products for each user—this kind of personalization requires SSR.
At this point, someone might ask: “Can I use SSR for blog post detail pages?” You can, but there’s no need to. Article content is fixed—SSG generates static HTML that’s served directly from a CDN, resulting in faster access and lower server costs. SSR isn’t a silver bullet; don’t use it just for the sake of using it.
Hybrid Mode: The Best of Both Worlds
Astro 2.0’s Hybrid mode is pretty clever—it lets you use SSG for static pages and SSR for dynamic pages in the same project. For example, in an e-commerce site:
- Homepage, About page, Help docs → SSG (fast loading)
- Login page, User dashboard, Shopping cart → SSR (dynamic content)
- Product detail pages → SSG (fixed content)
- Search results pages → SSR (real-time queries)
This setup keeps static pages blazingly fast while perfectly implementing dynamic features. A friend’s blog uses this approach—article lists and details use SSG, while the comment section uses SSR, and the Lighthouse score still maintains 95+.
Chapter 2: Quick Start - Enable SSR Mode in 3 Steps
Configuring Astro SSR from Scratch (Node.js Adapter)
Alright, once you’ve determined your project needs SSR, let’s start configuring. I’ll demonstrate with the Node.js adapter first—it’s the most universal solution, suitable for self-hosted servers or VPS deployment.
Step 1: One-Command Adapter Installation
Astro provides a super simple automatic configuration command. Just run this in your project root:
npx astro add nodeThis single command automatically does three things:
- Installs the
@astrojs/nodepackage - Modifies the
astro.config.mjsconfiguration file - Updates dependencies in
package.json
After running the command, you’ll see a bunch of green checkmarks in the terminal, indicating successful configuration. If you want to install manually (for instance, to specify a version), you can also do this:
npm install @astrojs/nodeThen manually modify the config file (covered in the next step).
Step 2: Modify Configuration File
Open astro.config.mjs in your project root. If you used the automatic configuration command, you’ll already see this content:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server', // Enable SSR mode
adapter: node({
mode: 'standalone' // Standalone server mode
}),
});Let me highlight these two configuration options:
output configuration:
'static'(default): All pages use SSG, outputs pure static HTML'server': All pages use SSR, dynamically generated on each request'hybrid': Default SSG, can enable SSR per page (recommended!)
mode configuration:
'standalone': Astro starts an independent Node.js server, suitable for direct deployment'middleware': Generates middleware, can integrate into Express, Koa, and other frameworks
I usually use standalone because Astro’s built-in server is sufficient—no need for additional integration. If your project already has an Express backend and you want Astro as part of it, use middleware.
Step 3: Build and Run
After configuration, build the project:
npm run buildAfter building, you’ll find a server/ folder in the dist/ directory with an entry.mjs file—this is the SSR server’s entry point.
Run the SSR server:
node ./dist/server/entry.mjsBy default, it starts at http://localhost:4321. Visit your site, and all pages are now SSR!
Development Environment Debugging
During development, you don’t need to build every time. Just use:
npm run devThe dev server automatically supports SSR with real-time code changes—super convenient.
Common Troubleshooting
Port in use: If port 4321 is occupied, set an environment variable:
PORT=3000 node ./dist/server/entry.mjsAdapter module not found: Confirm
@astrojs/nodeis installed, runnpm installto reinstall dependenciesPage 404: Check if files in the
src/pages/directory are correct—SSR mode still follows Astro’s routing rules
Honestly, configuring SSR is really this simple. My first time, from start to running successfully, took less than 5 minutes. If you’re deploying to Vercel or Netlify, there are dedicated adapters with even simpler configuration—I’ll cover that in detail in the next chapter.
Chapter 3: Detailed Configuration of Mainstream Adapters
How to Choose Between Vercel, Netlify, and Cloudflare?
If your project is hosted on Vercel, Netlify, or Cloudflare, congratulations—configuring SSR will be even easier. These platforms have officially maintained Astro adapters with zero-config deployment.
Vercel Adapter - The King of Serverless Functions
Vercel is my most-used deployment platform. The free tier is sufficient for personal projects, and configuration is super simple:
npx astro add vercelThis command automatically configures everything. The config file looks like this:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel(),
});Vercel’s Special Feature: ISR (Incremental Static Regeneration)
This is a Vercel-exclusive feature that makes your SSR pages as fast as SSG. Simply put, the first visit generates the page with SSR, then caches it for a period. Subsequent visits use the cache, and it regenerates when expired.
adapter: vercel({
isr: {
expiration: 60, // Cache for 60 seconds
},
}),For example, on a news site, article detail pages updating once per minute is sufficient—no need to query the database on every request. With ISR, you get SSR’s flexibility and SSG’s speed.
Vercel Deployment Process:
- Configure the adapter
- Push code to GitHub
- Import project in Vercel dashboard
- Build command:
npm run build(auto-detected) - Click deploy, done!
Netlify Adapter - Master of Edge Functions
Netlify is also a popular deployment platform, especially suitable for static sites with dynamic features.
npx astro add netlifyConfig file:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify';
export default defineConfig({
output: 'server',
adapter: netlify({
edgeMiddleware: true, // Enable Edge middleware
}),
});What is edgeMiddleware?
Simply put, it runs middleware logic (like authentication, redirects) on edge nodes for faster responses. If your site has location-based features (like displaying different languages based on user location), edge is very useful.
Netlify Redirect Configuration
One convenient aspect of Netlify is automatic redirect handling. For example, if you want to redirect /old-page to /new-page, just create a _redirects file in your project root:
/old-page /new-page 301It takes effect automatically after deployment, no code changes needed.
Cloudflare Adapter - Global CDN Acceleration
If your users are spread across the globe, Cloudflare is the best choice. Its Workers run in 300+ data centers worldwide with extremely low latency.
npx astro add cloudflareConfig file:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare(),
});Cloudflare Limitations
Note that Cloudflare Workers’ runtime environment isn’t completely identical to Node.js—some Node.js APIs won’t work (like fs file system). If your project depends on these APIs, Cloudflare might not be suitable.
Adapter Comparison Table
| Adapter | Use Case | Core Advantage | Main Limitation |
|---|---|---|---|
| Node.js | Self-hosted server, VPS | Full control, no restrictions | Requires self-management, higher cost |
| Vercel | Personal projects, small teams | Zero-config, ISR support | Free tier limits (100GB bandwidth/month) |
| Netlify | Static + dynamic features | Fast Edge Functions | Build time limit (300 minutes/month free) |
| Cloudflare | Global users, low latency | Edge computing, low price | Workers environment restrictions, some Node APIs unavailable |
My Selection Recommendations:
- Blog, documentation sites: Prioritize Vercel or Netlify—free tier sufficient, easy deployment
- E-commerce, SaaS apps: Vercel (ISR is great), or self-hosted Node.js server (full control)
- International products: Cloudflare (global acceleration)
- Enterprise projects: Self-hosted Node.js (data privacy, full control)
There’s no absolute standard for which to choose—it depends on your project needs and budget. I use Vercel for my personal blog and self-hosted servers for client enterprise sites—both work great.
Chapter 4: Hybrid Mixed Rendering in Practice
Using SSR and SSG Simultaneously in One Project
Alright, we’ve covered pure SSR configuration. Now for the key point: Hybrid mode. This is Astro’s killer feature, letting you enjoy SSG’s speed and SSR’s flexibility in the same project.
Configuring Hybrid Mode
Just change output to 'hybrid':
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'hybrid', // Default SSG, SSR on demand
adapter: node(),
});After configuration, all pages default to SSG, then you can add a single line of code to pages that need SSR to enable it.
Page-Level Rendering Control
Here’s the key—how do you make a specific page use SSR? Add one line to the page file’s frontmatter:
// src/pages/dashboard.astro (SSR)
---
export const prerender = false; // Disable prerendering, use SSR
const user = Astro.cookies.get('user');
---
<h1>Welcome back, {user?.name}</h1>
<p>You have {user?.notifications} unread messages</p>That’s it! prerender = false means “don’t generate at build time, generate dynamically when users visit.”
Conversely, if you set output to 'server' (all SSR) and want a specific page to use SSG, do this:
// src/pages/about.astro (SSG)
---
export const prerender = true; // Force generation at build time
---
<h1>About Us</h1>
<p>This page's content doesn't change—pre-generated for ultra-fast access.</p>Key Summary (Don’t Get Confused):
| output config | Default behavior | How to change individual pages |
|---|---|---|
'hybrid' | All pages SSG | export const prerender = false → that page uses SSR |
'server' | All pages SSR | export const prerender = true → that page uses SSG |
I used to get this backwards all the time. Then I remembered: hybrid prioritizes SSG, server prioritizes SSR.
Real-World Case: Blog + User System
Suppose you’re building a blog platform with article display and user login functionality. The ideal configuration:
Project Structure:
src/pages/
├── index.astro // Homepage (SSG)
├── about.astro // About page (SSG)
├── blog/
│ ├── [slug].astro // Article detail (SSG)
│ └── index.astro // Article list (SSG)
├── login.astro // Login page (SSR)
├── dashboard.astro // User dashboard (SSR)
└── api/
└── comments.js // Comments API (SSR)Configuration File:
// astro.config.mjs
export default defineConfig({
output: 'hybrid', // Default SSG
adapter: vercel(), // Deploy to Vercel
});Static Pages (No Special Configuration Needed):
// src/pages/blog/[slug].astro
---
// No prerender setting, defaults to SSG
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
---
<article>
<h1>{post.data.title}</h1>
<div set:html={post.body} />
</article>Dynamic Pages (Require SSR):
// src/pages/dashboard.astro
---
export const prerender = false; // Enable SSR
// Check user login status
const token = Astro.cookies.get('token')?.value;
if (!token) {
return Astro.redirect('/login');
}
// Fetch user info from database
const user = await fetch(`https://api.example.com/user`, {
headers: { Authorization: `Bearer ${token}` }
}).then(res => res.json());
---
<div>
<h1>Welcome, {user.name}</h1>
<p>Email: {user.email}</p>
<p>Last login: {user.lastLogin}</p>
</div>API Routes (Automatically SSR):
// src/pages/api/comments.js
export async function POST({ request }) {
const { articleId, content } = await request.json();
// Save comment to database
await db.comments.insert({
articleId,
content,
createdAt: new Date(),
});
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
export async function GET({ url }) {
const articleId = url.searchParams.get('articleId');
// Read comments from database
const comments = await db.comments.findMany({
where: { articleId },
orderBy: { createdAt: 'desc' }
});
return new Response(JSON.stringify(comments), {
headers: { 'Content-Type': 'application/json' }
});
}Benefits of This Configuration:
- Static pages (articles, homepage) remain lightning fast, Lighthouse score 95+, all served from CDN
- Dynamic pages (user dashboard) fetch data in real-time, each user sees different content
- API routes provide backend capabilities, no need for a separate backend server
- Short build time, only static pages need prerendering, dynamic pages don’t count toward build time
In a project I worked on—50 blog posts plus a user system—build time was only 20 seconds. After deployment, static pages loaded instantly, and dynamic pages responded in under 100ms. Hybrid mode really is the best practice.
Chapter 5: Common Issues and Best Practices
Pitfalls in SSR Configuration and Solutions
During SSR configuration, I’ve stepped on plenty of landmines. Here’s a compilation of common issues and solutions to help you avoid them.
Issue 1: Error Astro.clientAddress is only available when using output: 'server'
Cause: You’re using Astro.clientAddress (to get user IP) in your code, but output in the config file is still 'static'.
Solution:
// astro.config.mjs
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: node(),
});Dynamic APIs like Astro.clientAddress, Astro.cookies, and Astro.redirect() only work in SSR mode.
Issue 2: Page 404 After Deployment, Works Fine Locally
Cause: Adapter misconfigured, or deployment platform’s build command/output directory settings are wrong.
Solution:
Vercel Deployment:
- Build command:
npm run build - Output directory:
.vercel/output(automatic) - Ensure you don’t manually configure routes in
vercel.json—let Astro handle it
Netlify Deployment:
- Build command:
npm run build - Publish directory:
dist(for static) or.netlify(for SSR) - If still 404, check
netlify.toml:[build] command = "npm run build" publish = "dist"
Issue 3: SSR Pages Load Very Slowly, Over 2 Seconds
Cause: Insufficient server performance, or database queries too slow.
Solution:
Use caching:
// src/pages/api/news.js export async function GET() { const cached = await redis.get('news'); if (cached) { return new Response(cached, { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60' // Cache for 60 seconds } }); } const news = await fetchNewsFromDB(); await redis.set('news', JSON.stringify(news), 'EX', 60); return new Response(JSON.stringify(news), { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=60' } }); }Optimize database queries:
- Add indexes
- Reduce JOINs
- Only query needed fields
Consider ISR (if using Vercel):
adapter: vercel({ isr: { expiration: 300 } // Cache for 5 minutes }),
Issue 4: Can’t Access Environment Variables on Client Side
Cause: Astro’s environment variables are split between client and server.
Solution:
Server-side use (SSR pages, API routes):
const secret = import.meta.env.SECRET_KEY; // Any environment variable worksClient-side use (JavaScript in the browser):
const apiUrl = import.meta.env.PUBLIC_API_URL; // Must start with PUBLIC_.env file configuration:
SECRET_KEY=abc123 # Server-side only
PUBLIC_API_URL=https://api.example.com # Both client and serverIssue 5: adapter.setApp is not a function Error
Cause: Incompatible Astro and adapter versions.
Solution:
# Update to latest versions
npm update astro @astrojs/node
# Or specify compatible versions (check official docs)
npm install astro@latest @astrojs/node@latestGenerally, keeping both Astro and the adapter on the latest versions avoids issues.
Best Practices Summary
- Default to Hybrid mode: Unless all pages need SSR,
output: 'hybrid'is the optimal choice - Enable SSR on demand: Only set
prerender = falsefor pages that truly need dynamic rendering - Static assets via CDN: Put images, CSS, JS files in the
public/directory—they’ll automatically use CDN, not SSR - Caching strategy: For dynamic content that doesn’t change often (like news lists), use caching or ISR to reduce server load
- Separate environment variables: Use server-side environment variables for sensitive info,
PUBLIC_prefix for public configs - Monitor performance: Use Vercel Analytics or Google Analytics to monitor SSR page response times and optimize promptly
Conclusion
After all that, it really boils down to three key points:
1. SSR isn’t a silver bullet—use it where it makes sense
Don’t get excited just because you see SSR, and don’t think SSG is outdated. Use SSG for static pages, SSR for dynamic ones, and Hybrid mode for most projects. I’ve seen people convert an entire blog to SSR, only to see performance decline—after all, blog content doesn’t change, so SSG via CDN is definitely faster.
2. Choose adapters based on deployment platform—configuration is actually simple
If you’re using Vercel/Netlify/Cloudflare, one line npx astro add [platform] and you’re done. If self-hosting, npx astro add node takes about 5 minutes. Don’t be intimidated by the documentation—it’s much simpler in practice than it looks.
3. Hybrid mode gives you the best of both worlds
This is Astro’s essence. Static pages maintain 95+ Lighthouse scores, dynamic pages enable personalized features, build time doesn’t increase, and server costs don’t explode. When I start projects now, Hybrid mode is my first choice.
Next Steps
After reading this article, you can:
- Try it right now: Open your Astro project, run
npx astro add node, and experience SSR in 5 minutes - Think about your needs: List which pages in your project need dynamic rendering and which can stay static
- Dive deeper: Astro’s recently released Server Islands feature lets you embed SSR components in SSG pages for even more flexibility
By the way, if you encounter issues during configuration, head to Astro’s official Discord community to ask questions—responses are super fast, and the community vibe is great.
One final reminder: don’t over-optimize. If your site doesn’t get much traffic (daily PV < 10,000), a static site is probably sufficient—no need to add SSR complexity. Tech choices should serve the business, not be used for their own sake.
Best of luck with your configuration, and feel free to leave comments with any questions!
Published on: Dec 2, 2025 · Modified on: Dec 4, 2025
Related Posts

Complete Guide to Deploying Astro on Cloudflare: SSR Configuration + 3x Speed Boost for China

Building an Astro Blog from Scratch: Complete Guide from Homepage to Deployment in 1 Hour
