writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
ssr-migrationMigrate 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 depl…
Independent GitHub snapshot · Automated classification · Original documentation governs
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.
Identify what needs to change before migrating:
# 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:
Migrate incrementally using Next.js App Router:
// 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} />;
}
The most common migration bugs are hydration errors:
// 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>;
}
// 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} />;
}
# 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"]
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:
generateStaticParams + ISR (revalidate every 30 minutes)window directly — wraps them in dynamic imports with ssr: falsenext-sitemap configuration for automatic XML sitemap generationPrompt: "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:
app/(public)/feed/page.tsx as a Server Component with SSRapp/(private)/settings/page.tsx with "use client" directive, keeping existing CSR logicuseEffect data fetching to server-side fetch with cookie forwarding"use client" sparingly — only for components that genuinely need browser APIs or interactivity.