con un clic
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.
// 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.
| name | migrate-page |
| description | 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. |
| argument-hint | ["pages-router-file-path"] |
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.
apps/web/src/pages/[locale]/ to the App RouterThis 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 localesgenerateMetadata for base metadataLegacy pages use getStaticProps/getStaticPaths, useTranslations(),
HTMLHead, and a Layout component. All of these must be replaced with App
Router equivalents.
Read the legacy page file completely. Identify:
pages/[locale]/solutions/defi.tsx → app/[locale]/solutions/defi/)getStaticProps does (API calls, message loading,
static data)useState, useEffect, useRef, event handlers,
dynamic imports with ssr: false, browser APIst(), t.rich(), t.raw() callsHTMLHead props (title, description, socialShare)Layout component usage and its propsBased 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.
page.tsx (Server Component)Create apps/web/src/app/[locale]/<route>/page.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,
});
}
page.tsx:"use client", no hooksgetTranslations from next-intl/server — NOT useTranslations
from next-intlt() calls happen here, passed to client component via
translations propgetStaticProps — do it directly in the async
component bodygenerateMetadata replaces HTMLHead — use getIndexMetadata from
@/app/metadata{ params: Promise<{ locale: string }> }generateStaticParams needed per-page — the root layout handles itNextIntlClientProvider
handles it<page-name>.tsx)Only if the page needs client-side features. Create
apps/web/src/app/[locale]/<route>/<page-name>.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
);
}
"use client" directive at the toppage.tsxtranslations interface — all strings are string, arrays use
proper typesuseTranslations() — all translations come via propsLayout wrapper — the root layout already provides <Header> and
<Footer>HTMLHead — metadata is handled by generateMetadata in page.tsxLayout wrapperThe 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.
HTMLHead with generateMetadataMap HTMLHead props to generateMetadata:
title → titleKey in getIndexMetadatadescription → descriptionKey in getIndexMetadatasocialShare → add as openGraph.images in the metadata return if customIf the page uses a custom social share image, extend the metadata:
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"],
},
};
}
getStaticProps data fetchingPage async function bodyrevalidate: 60 from getStaticProps is not needed — App Router uses
different caching strategies. For ISR-equivalent behavior, use route segment
config:
export const revalidate = 60;
Add this export at the top level of page.tsx if the page fetched dynamic
data in getStaticProps.useTranslations() with server translationsconst t = useTranslations() (client-side hook)page.tsx: const t = await getTranslations() (server-side)t() calls into the translations object in page.tsxt.rich() calls with JSX formatters:
<span>), keep it in the
client component using the raw translation stringReactNode in page.tsx and type as React.ReactNode
in the interfacedynamic() imports with ssr: falseReplace next/dynamic with React.lazy + Suspense, or use "use client"
boundary naturally:
// 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
withLocales() / getStaticPathsRemove entirely. The root layout's generateStaticParams handles locale
generation. No per-page getStaticPaths or generateStaticParams is needed.
import locales)Remove entirely. The root layout's NextIntlClientProvider already loads
and provides messages.
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:
// 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;
};
}
Add the _m_ prefix to the source file.
Summarize:
getStaticProps → server componentpage.tsx is an async server component (no "use client")getTranslations from next-intl/server used (not useTranslations)t() calls are in page.tsx, not in the client componentgenerateMetadata replaces HTMLHeadLayout wrapper (root layout provides Header/Footer)getStaticPaths / withLocales / generateStaticParamsimport locales)"use client" directive (if created)revalidate export added if page had dynamic data in getStaticPropsuseTranslations() in page.tsx — it's a client hook. Use
getTranslations() from next-intl/server.Layout — the root layout already has Header/Footer.
Duplicating them causes double headers.generateStaticParams — the root layout handles all locale
params.page.tsx import pattern.HTMLHead in the client component — it uses next/head
which doesn't work in App Router.next/router — use next/navigation in App Router.
(useRouter, usePathname, useSearchParams)Study these for patterns:
apps/web/src/app/[locale]/universities/page.tsx + universities.tsxapps/web/src/app/[locale]/x402/page.tsx + x402.tsxapps/web/src/app/[locale]/privacyhack/page.tsx + privacyhack.tsxScaffold 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.
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.
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.