ssr-migration
$
npx mdskill add TerminalSkills/skills/ssr-migrationMigrate client-side apps to SSR/SSG for better SEO and performance.
- Fixes blank pages for crawlers and improves Core Web Vitals.
- Integrates with Next.js, Nuxt, and Astro frameworks.
- Analyzes code for SSR-incompatible patterns like window access.
- Delivers step-by-step migration plans with deployment configs.
SKILL.md
.github/skills/ssr-migrationView on GitHub ↗
---
name: ssr-migration
description: >-
Migrate client-side rendered (CSR) React/Vue applications to server-side
rendering (SSR) or static site generation (SSG) using Next.js, Nuxt, or
Astro. Use when you need to improve SEO, reduce time-to-first-byte, fix
blank page issues for crawlers, or improve Core Web Vitals. Covers
incremental adoption, data fetching patterns, hydration debugging, and
deployment configuration. Trigger words: SSR, SSG, server-side rendering,
static generation, Next.js migration, SEO, hydration, TTFB, Core Web Vitals.
license: Apache-2.0
compatibility: "Node.js 18+. React 18+ for Next.js, Vue 3+ for Nuxt 3."
metadata:
author: terminal-skills
version: "1.0.0"
category: development
tags: ["ssr", "ssg", "nextjs", "performance", "seo"]
---
# SSR Migration
## Overview
This skill guides the migration of client-side rendered single-page applications to server-side rendering or static site generation. It covers the incremental migration strategy (not a rewrite), identifying which pages benefit from SSR vs SSG vs CSR, fixing hydration mismatches, adapting data fetching patterns, and configuring deployment for SSR workloads.
## Instructions
### 1. Audit the current CSR application
Identify what needs to change before migrating:
```bash
# Check for SSR-incompatible patterns:
# 1. Direct window/document access at module level
grep -rn "window\." src/ --include="*.tsx" --include="*.ts" | grep -v "typeof window"
# 2. Browser-only libraries imported at top level
grep -rn "import.*from.*('|\")(chart.js|swiper|mapbox)" src/
# 3. localStorage/sessionStorage usage outside useEffect
grep -rn "localStorage\|sessionStorage" src/ --include="*.tsx"
# 4. Dynamic imports that should stay client-only
grep -rn "React.lazy\|dynamic(" src/ --include="*.tsx"
```
Classify each page by rendering strategy:
- **SSG** — content changes rarely, same for all users (marketing pages, blog posts, docs)
- **SSR** — content changes frequently or is user-specific (dashboards, search results)
- **CSR** — highly interactive, no SEO need (admin panels, internal tools)
### 2. Set up Next.js App Router alongside existing code
Migrate incrementally using Next.js App Router:
```typescript
// app/products/page.tsx — SSG with revalidation
export const revalidate = 3600; // Regenerate every hour
async function getProducts() {
const res = await fetch("https://api.example.com/products", {
next: { revalidate: 3600 },
});
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Products</h1>
<ProductGrid products={products} />
</main>
);
}
// app/products/[id]/page.tsx — SSG with dynamic params
export async function generateStaticParams() {
const products = await fetch("https://api.example.com/products").then(r => r.json());
return products.map((p: { id: string }) => ({ id: p.id }));
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`).then(r => r.json());
return <ProductDetail product={product} />;
}
```
### 3. Handle hydration mismatches
The most common migration bugs are hydration errors:
```typescript
// BAD — different output on server vs client
function Greeting() {
return <p>Current time: {new Date().toLocaleTimeString()}</p>;
}
// GOOD — defer client-only content
"use client";
import { useState, useEffect } from "react";
function Greeting() {
const [time, setTime] = useState<string | null>(null);
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <p>Current time: {time ?? "Loading..."}</p>;
}
```
### 4. Migrate data fetching patterns
```typescript
// BEFORE (CSR): useEffect + useState
function Dashboard() {
const [data, setData] = useState(null);
useEffect(() => {
fetch("/api/dashboard").then(r => r.json()).then(setData);
}, []);
if (!data) return <Spinner />;
return <DashboardView data={data} />;
}
// AFTER (SSR): Server Component with async/await
async function Dashboard() {
const data = await fetch("http://internal-api/dashboard", {
headers: { cookie: cookies().toString() },
}).then(r => r.json());
return <DashboardView data={data} />;
}
```
### 5. Configure deployment for SSR
```dockerfile
# Dockerfile for Next.js SSR
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
```
## Examples
### Example 1: Marketing site migration
**Prompt:** "Migrate our React SPA marketing site to Next.js with SSG. We have 12 pages: homepage, 5 product pages, pricing, about, blog listing, blog posts (40 articles), contact, and FAQ."
**Agent output:**
- Creates Next.js App Router structure with static pages for homepage, pricing, about, contact, FAQ
- Blog listing and posts use `generateStaticParams` + ISR (revalidate every 30 minutes)
- Product pages use SSG with on-demand revalidation via webhook
- Identifies 3 components using `window` directly — wraps them in dynamic imports with `ssr: false`
- Adds `next-sitemap` configuration for automatic XML sitemap generation
### Example 2: Dashboard with mixed rendering
**Prompt:** "Our React dashboard app needs SSR for the main feed (SEO matters) but the settings and admin pages can stay client-rendered. How do I set this up incrementally?"
**Agent output:**
- Creates `app/(public)/feed/page.tsx` as a Server Component with SSR
- Creates `app/(private)/settings/page.tsx` with `"use client"` directive, keeping existing CSR logic
- Adds middleware for authentication that redirects unauthenticated users before SSR runs
- Migrates the feed's `useEffect` data fetching to server-side `fetch` with cookie forwarding
- Keeps the real-time notification widget as a Client Component embedded within the SSR layout
## Guidelines
- **Migrate incrementally** — move one route at a time, not the entire app at once.
- **Start with SSG pages** — they're the easiest win and don't require a Node.js server.
- **Use `"use client"` sparingly** — only for components that genuinely need browser APIs or interactivity.
- **Test hydration in development** — React 18's strict mode double-renders to catch mismatches early.
- **Forward cookies for authenticated SSR** — server-side fetch won't include user cookies automatically.
- **Monitor TTFB after migration** — SSR adds server computation time. If TTFB increases, consider caching or ISR.
- **Keep bundle size in check** — SSR doesn't eliminate the need for code splitting. Use dynamic imports for heavy client components.
More from TerminalSkills/skills