BetterLink Logo BetterLink Blog
Switch Language
Toggle Theme

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

Complete Guide to Astro SSR cover image

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 node

This single command automatically does three things:

  1. Installs the @astrojs/node package
  2. Modifies the astro.config.mjs configuration file
  3. 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/node

Then 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 build

After 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.mjs

By 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 dev

The dev server automatically supports SSR with real-time code changes—super convenient.

Common Troubleshooting

  1. Port in use: If port 4321 is occupied, set an environment variable:

    PORT=3000 node ./dist/server/entry.mjs
  2. Adapter module not found: Confirm @astrojs/node is installed, run npm install to reinstall dependencies

  3. Page 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 vercel

This 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:

  1. Configure the adapter
  2. Push code to GitHub
  3. Import project in Vercel dashboard
  4. Build command: npm run build (auto-detected)
  5. 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 netlify

Config 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  301

It 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 cloudflare

Config 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

AdapterUse CaseCore AdvantageMain Limitation
Node.jsSelf-hosted server, VPSFull control, no restrictionsRequires self-management, higher cost
VercelPersonal projects, small teamsZero-config, ISR supportFree tier limits (100GB bandwidth/month)
NetlifyStatic + dynamic featuresFast Edge FunctionsBuild time limit (300 minutes/month free)
CloudflareGlobal users, low latencyEdge computing, low priceWorkers 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 configDefault behaviorHow to change individual pages
'hybrid'All pages SSGexport const prerender = false → that page uses SSR
'server'All pages SSRexport 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:

  1. Static pages (articles, homepage) remain lightning fast, Lighthouse score 95+, all served from CDN
  2. Dynamic pages (user dashboard) fetch data in real-time, each user sees different content
  3. API routes provide backend capabilities, no need for a separate backend server
  4. 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:

  1. 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'
        }
      });
    }
  2. Optimize database queries:

    • Add indexes
    • Reduce JOINs
    • Only query needed fields
  3. 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 works

Client-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 server

Issue 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@latest

Generally, keeping both Astro and the adapter on the latest versions avoids issues.

Best Practices Summary

  1. Default to Hybrid mode: Unless all pages need SSR, output: 'hybrid' is the optimal choice
  2. Enable SSR on demand: Only set prerender = false for pages that truly need dynamic rendering
  3. Static assets via CDN: Put images, CSS, JS files in the public/ directory—they’ll automatically use CDN, not SSR
  4. Caching strategy: For dynamic content that doesn’t change often (like news lists), use caching or ISR to reduce server load
  5. Separate environment variables: Use server-side environment variables for sensitive info, PUBLIC_ prefix for public configs
  6. 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:

  1. Try it right now: Open your Astro project, run npx astro add node, and experience SSR in 5 minutes
  2. Think about your needs: List which pages in your project need dynamic rendering and which can stay static
  3. 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