migrate-page

Migrate a legacy page from apps/web/src/pages/ (Pages Router) to apps/web/src/app/ (App Router). Use when moving any existing page from the pages directory to the app directory.

Best use case

migrate-page is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Migrate a legacy page from apps/web/src/pages/ (Pages Router) to apps/web/src/app/ (App Router). Use when moving any existing page from the pages directory to the app directory.

Teams using migrate-page should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/migrate-page/SKILL.md --create-dirs "https://raw.githubusercontent.com/solana-foundation/solana-com/main/apps/web/openspec/skills/migrate-page/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/migrate-page/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How migrate-page Compares

Feature / Agentmigrate-pageStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Migrate a legacy page from apps/web/src/pages/ (Pages Router) to apps/web/src/app/ (App Router). Use when moving any existing page from the pages directory to the app directory.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Migrate Page (Pages Router → App Router)

Migrate a legacy Next.js page from `apps/web/src/pages/[locale]/` to
`apps/web/src/app/[locale]/` following the project's established App Router
patterns.

## When to Use This Skill

- Migrating any file under `apps/web/src/pages/[locale]/` to the App Router
- The target file or route is provided as: **$ARGUMENTS**
- If no file path or route was provided, ask the user which page to migrate

## Context

This project is mid-migration from Next.js Pages Router to App Router. The root
layout at `apps/web/src/app/[locale]/layout.tsx` already provides:

- `<html>`, `<body>`, GTM, `NextIntlClientProvider`, `PostHogProvider`,
  `ThemeProvider`, `<Header>`, `<Footer>`
- `generateStaticParams` for all locales
- `generateMetadata` for base metadata

Legacy pages use `getStaticProps`/`getStaticPaths`, `useTranslations()`,
`HTMLHead`, and a `Layout` component. All of these must be replaced with App
Router equivalents.

## Migration Rules

### Step 1 — Identify the source page and understand it

Read the legacy page file completely. Identify:

1. **Route**: derive the App Router directory path (e.g.
   `pages/[locale]/solutions/defi.tsx` → `app/[locale]/solutions/defi/`)
2. **Data fetching**: what `getStaticProps` does (API calls, message loading,
   static data)
3. **Client-side features**: `useState`, `useEffect`, `useRef`, event handlers,
   dynamic imports with `ssr: false`, browser APIs
4. **Translations**: all `t()`, `t.rich()`, `t.raw()` calls
5. **Head/SEO**: `HTMLHead` props (title, description, socialShare)
6. **Layout wrapper**: `Layout` component usage and its props
7. **Imports**: which components, data files, and utilities are used

### Step 2 — Determine the file structure

Based on the analysis, decide on the output structure:

**Option A: Server component only (`page.tsx`)** Use when the page has NO
client-side hooks (`useState`, `useEffect`, `useRef`), no event handlers, no
`dynamic()` imports with `ssr: false`, and no browser APIs.

**Option B: Server + Client split (`page.tsx` + `<page-name>.tsx`)** Use when
the page requires any client-side features. This is the common case for most
legacy pages.

### Step 3 — Create `page.tsx` (Server Component)

Create `apps/web/src/app/[locale]/<route>/page.tsx`:

```tsx
import { PageNamePage } from "./<page-name>";
import { getIndexMetadata } from "@/app/metadata";
import { getTranslations } from "next-intl/server";

type Props = { params: Promise<{ locale: string }> };

export default async function Page(_props: Props) {
  const t = await getTranslations();

  // Data fetching that was in getStaticProps goes here
  // (API calls, file reads, etc.)

  const translations = {
    // All t() calls extracted here as flat props
    heroTitle: t("namespace.hero.title"),
    // Use t.rich() for rich text with HTML
    // Use t.raw() with type cast for arrays/objects
  };

  return <PageNamePage translations={translations} /* dataProps */ />;
}

export async function generateMetadata({ params }: Props) {
  const { locale } = await params;
  return await getIndexMetadata({
    titleKey: "namespace.meta.title", // or "namespace.title"
    descriptionKey: "namespace.meta.description", // or "namespace.description"
    path: "/route-path",
    locale,
  });
}
```

#### Key rules for `page.tsx`:

1. **Async server component** — no `"use client"`, no hooks
2. **Use `getTranslations` from `next-intl/server`** — NOT `useTranslations`
   from `next-intl`
3. **All `t()` calls happen here**, passed to client component via
   `translations` prop
4. **Data fetching replaces `getStaticProps`** — do it directly in the async
   component body
5. **`generateMetadata` replaces `HTMLHead`** — use `getIndexMetadata` from
   `@/app/metadata`
6. **Props type** is always `{ params: Promise<{ locale: string }> }`
7. **No `generateStaticParams`** needed per-page — the root layout handles it
8. **No message loading** needed — the root layout's `NextIntlClientProvider`
   handles it

### Step 4 — Create client component (`<page-name>.tsx`)

Only if the page needs client-side features. Create
`apps/web/src/app/[locale]/<route>/<page-name>.tsx`:

```tsx
"use client";

// Import components, data, etc.

interface PageNamePageProps {
  translations: {
    heroTitle: string;
    // ... typed translation props
  };
  // ... data props from server
}

export function PageNamePage({ translations }: PageNamePageProps) {
  // Client-side hooks and logic here
  return (
    // JSX — no Layout wrapper, no HTMLHead
  );
}
```

#### Key rules for client component:

1. **`"use client"` directive** at the top
2. **Named export** (not default) — matches the import in `page.tsx`
3. **Typed `translations` interface** — all strings are `string`, arrays use
   proper types
4. **No `useTranslations()`** — all translations come via props
5. **No `Layout` wrapper** — the root layout already provides `<Header>` and
   `<Footer>`
6. **No `HTMLHead`** — metadata is handled by `generateMetadata` in `page.tsx`

### Step 5 — Handle specific migration patterns

#### 5a. Remove `Layout` wrapper

The legacy `Layout` component (`@/components/solutions/layout`) renders
`<Header>` and `<Footer>`. The App Router root layout already provides these.
**Remove the `Layout` wrapper entirely.**

If the `Layout` had a `className` prop (e.g. `className="bg-nd-bg"`), apply it
to the outermost `<div>` in the client component instead.

#### 5b. Replace `HTMLHead` with `generateMetadata`

Map `HTMLHead` props to `generateMetadata`:

- `title` → `titleKey` in `getIndexMetadata`
- `description` → `descriptionKey` in `getIndexMetadata`
- `socialShare` → add as `openGraph.images` in the metadata return if custom

If the page uses a custom social share image, extend the metadata:

```tsx
export async function generateMetadata({ params }: Props) {
  const { locale } = await params;
  const base = await getIndexMetadata({
    titleKey: "namespace.meta.title",
    descriptionKey: "namespace.meta.description",
    path: "/route-path",
    locale,
  });
  return {
    ...base,
    openGraph: {
      ...base.openGraph,
      images: ["/src/img/route/og-image.webp"],
    },
  };
}
```

#### 5c. Migrate `getStaticProps` data fetching

- Move API calls directly into the `Page` async function body
- Wrap in try/catch as needed
- Pass fetched data as additional props to the client component
- `revalidate: 60` from `getStaticProps` is not needed — App Router uses
  different caching strategies. For ISR-equivalent behavior, use route segment
  config:
  ```tsx
  export const revalidate = 60;
  ```
  Add this export at the top level of `page.tsx` if the page fetched dynamic
  data in `getStaticProps`.

#### 5d. Replace `useTranslations()` with server translations

- In the legacy page: `const t = useTranslations()` (client-side hook)
- In App Router `page.tsx`: `const t = await getTranslations()` (server-side)
- Extract ALL `t()` calls into the `translations` object in `page.tsx`
- For `t.rich()` calls with JSX formatters:
  - If the formatter is simple (e.g., wrapping in `<span>`), keep it in the
    client component using the raw translation string
  - Or pre-render to a `ReactNode` in `page.tsx` and type as `React.ReactNode`
    in the interface

#### 5e. Handle `dynamic()` imports with `ssr: false`

Replace `next/dynamic` with React.lazy + Suspense, or use `"use client"`
boundary naturally:

```tsx
// Legacy:
const Component = dynamic(() => import("./Component"), { ssr: false });

// App Router — in the client component:
import dynamic from "next/dynamic";
const Component = dynamic(() => import("./Component"), { ssr: false });
// dynamic() still works in "use client" files
```

#### 5f. Handle `withLocales()` / `getStaticPaths`

**Remove entirely.** The root layout's `generateStaticParams` handles locale
generation. No per-page `getStaticPaths` or `generateStaticParams` is needed.

#### 5g. Message loading (`import locales`)

**Remove entirely.** The root layout's `NextIntlClientProvider` already loads
and provides messages.

#### 5h. Rich text translations with JSX

For `t.rich()` calls that use JSX formatters, you have two options:

**Option 1 (preferred):** Pass the raw string and handle formatting in the
client component using a helper or inline JSX with the translation string.

**Option 2:** Render in `page.tsx` and pass as `React.ReactNode`:

```tsx
// In page.tsx:
const translations = {
  heroTitle: t.rich("index.hero.title", {
    light: (chunks) => <span className="font-light">{chunks}</span>,
  }),
};

// In interface:
interface Props {
  translations: {
    heroTitle: React.ReactNode;
  };
}
```

### Step 6 — Rename the source file

Add the `_m_` prefix to the source file.

### Step 7 — Verify the migration

1. **Check TypeScript**: Run type checking to ensure no errors
2. **Check imports**: Ensure all component imports resolve correctly from the
   new location
3. **Check route**: Verify the new page will be served at the same URL path
4. **Do NOT delete the legacy file** — the user will handle removal after
   testing

### Step 8 — Report changes

Summarize:

- Files created (with paths)
- What was migrated from `getStaticProps` → server component
- What was moved to the client component
- Any translation keys that may need updating
- Any components that may need adjustment
- Remind the user to test and then delete the legacy page file

## Checklist

- [ ] `page.tsx` is an async server component (no `"use client"`)
- [ ] `getTranslations` from `next-intl/server` used (not `useTranslations`)
- [ ] All `t()` calls are in `page.tsx`, not in the client component
- [ ] `generateMetadata` replaces `HTMLHead`
- [ ] No `Layout` wrapper (root layout provides Header/Footer)
- [ ] No `getStaticPaths` / `withLocales` / `generateStaticParams`
- [ ] No message loading (`import locales`)
- [ ] Client component has `"use client"` directive (if created)
- [ ] Client component uses named export (not default)
- [ ] `revalidate` export added if page had dynamic data in `getStaticProps`
- [ ] TypeScript types are correct
- [ ] Legacy file is NOT deleted (user will handle after testing)

## Common Pitfalls

1. **Don't use `useTranslations()` in `page.tsx`** — it's a client hook. Use
   `getTranslations()` from `next-intl/server`.
2. **Don't wrap in `Layout`** — the root layout already has Header/Footer.
   Duplicating them causes double headers.
3. **Don't add `generateStaticParams`** — the root layout handles all locale
   params.
4. **Don't forget to type the translations interface** — every string prop
   should be typed.
5. **Don't use default export for client component** — use named export to match
   the `page.tsx` import pattern.
6. **Don't leave `HTMLHead` in the client component** — it uses `next/head`
   which doesn't work in App Router.
7. **Don't import from `next/router`** — use `next/navigation` in App Router.
   (`useRouter`, `usePathname`, `useSearchParams`)

## Reference: Existing Migrated Pages

Study these for patterns:

- `apps/web/src/app/[locale]/universities/page.tsx` + `universities.tsx`
- `apps/web/src/app/[locale]/x402/page.tsx` + `x402.tsx`
- `apps/web/src/app/[locale]/privacyhack/page.tsx` + `privacyhack.tsx`

Related Skills

create-page

447
from solana-foundation/solana-com

Scaffold a new page in apps/web/src/app following the project's established pattern. Use when creating any new page or route under the web app.

refresh-agent-context

447
from solana-foundation/solana-com

Audit this Turborepo for stale or missing agent-reference docs, then refresh repo and app-level `AGENTS.md` and related onboarding docs using the bundled workspace inventory script. Use when apps, packages, routes, ports, or shared tooling changed and the repo needs a fresh agent-oriented context pass.

llms-txt-generator

447
from solana-foundation/solana-com

Upgrade llmtxt-generator.py by scanning apps/ for doc structure changes, then regenerate llms.txt and llms-en.txt while only adding missing sections. Use when updating the generator, adding new doc sections, or refreshing LLM text files.

landing-page-generator

31392
from sickn33/antigravity-awesome-skills

Generates high-converting Next.js/React landing pages with Tailwind CSS. Uses PAS, AIDA, and BAB frameworks for optimized copy/components (Heroes, Features, Pricing). Focuses on Core Web Vitals/SEO.

Front-End DevelopmentClaudeCursorGemini

framework-migration-code-migrate

31392
from sickn33/antigravity-awesome-skills

You are a code migration expert specializing in transitioning codebases between frameworks, languages, versions, and platforms. Generate comprehensive migration plans, automated migration scripts, and

Code MigrationClaude

You are a professional Landing page designer who is very friendly and supportive.

5
from Harmeet10000/skills

This AI skill transforms your agent into a friendly landing page design mentor. It guides beginners through planning and designing a conceptual blueprint for their landing page or personal portfolio.

Workflow & Productivity

ui-page

31392
from sickn33/antigravity-awesome-skills

Scaffold a new mobile-first page using StyleSeed Toss layout patterns, section rhythm, and existing shell components.

wiki-page-writer

31392
from sickn33/antigravity-awesome-skills

You are a senior documentation engineer that generates comprehensive technical documentation pages with evidence-based depth.

seo-page

31392
from sickn33/antigravity-awesome-skills

Deep single-page SEO analysis covering on-page elements, content quality, technical meta tags, schema, images, and performance. Use when user says "analyze this page", "check page SEO", or provides a single URL for review.

seo-competitor-pages

31392
from sickn33/antigravity-awesome-skills

Generate SEO-optimized competitor comparison and alternatives pages. Covers "X vs Y" layouts, "alternatives to X" pages, feature matrices, schema markup, and conversion optimization. Use when user says "comparison page", "vs page", "alternatives page", "competitor comparison", or "X vs Y".

seo-aeo-landing-page-writer

31392
from sickn33/antigravity-awesome-skills

Writes complete, structured landing pages optimized for SEO ranking, AEO citation, and visitor conversion. Activate when the user wants to write or generate a landing page for a product, service, or offer.

pagerduty-automation

31392
from sickn33/antigravity-awesome-skills

Automate PagerDuty tasks via Rube MCP (Composio): manage incidents, services, schedules, escalation policies, and on-call rotations. Always search tools first for current schemas.