{"slug":"migrate-nextjs","title":"migrate-nextjs","summary":"Migrate a Next.js app to pracht: App or Pages Router pages, layouts, middleware, API routes, data fetching, and metadata — plus React→Preact, `className`→`class`, server components→loaders, and manifest wiring. Use for \"migrate from next\", \"convert next.js app\", \"port from next t","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:25.848067Z","repo":{"url":"https://github.com/JoviDeCroock/pracht","stars":99,"forks":5,"license":"MIT","updatedAt":"2026-10-19T16:43:31Z"},"bodyHtml":"<hr>\n<p>name: migrate-nextjs\nversion: 1.8.1\ndescription: |\nMigrate a Next.js app to pracht: App or Pages Router pages, layouts, middleware,\nAPI routes, data fetching, and metadata — plus React→Preact, <code>className</code>→<code>class</code>,\nserver components→loaders, and manifest wiring.\nUse for \"migrate from next\", \"convert next.js app\", \"port from next to pracht\",\n\"nextjs migration\", \"switch from next\".\nallowed-tools:</p>\n<ul>\n<li>Bash</li>\n<li>Read</li>\n<li>Write</li>\n<li>Edit</li>\n<li>Grep</li>\n<li>Glob</li>\n<li>AskUserQuestion</li>\n</ul>\n<hr>\n<h1>Migrate Next.js to Pracht</h1>\n<p>Migrate in phases: setup → shells → routes → client components → API →\nmiddleware → manifest → patterns → cleanup. Read each Next.js source file\nbefore converting it; never infer from the filename. Prefer the simplest\npracht equivalent, and when a Next.js feature has no equivalent, say so and\npropose an alternative instead of inventing one.</p>\n<p>With the Pracht MCP server, prefer its generate/inspect/doctor/verify tools;\n<code>inspect_build</code> needs a prior <code>pracht build</code>.</p>\n<h2>Step 0: Assess the source</h2>\n<p>Read <code>next.config.*</code> and <code>package.json</code> (React/Next versions, deps), then map\nthe tree: <code>app/</code> (App Router), <code>pages/</code> (Pages Router), <code>middleware.ts</code>,\n<code>app/api/</code> or <code>pages/api/</code>. Note which patterns are in use — <code>\"use client\"</code>,\n<code>async</code> server components, <code>generateStaticParams</code>, <code>generateMetadata</code>/<code>metadata</code>,\n<code>\"use server\"</code> actions — and the third-party integrations (auth, CMS, DB,\nanalytics). Confirm scope with the user if the app has more than ~20 routes.</p>\n<p>Agent tools alone do not require a router migration; use the standalone host:\n<a href=\"https://pracht.resynapse.dev/docs/standalone-capabilities\">https://pracht.resynapse.dev/docs/standalone-capabilities</a>.</p>\n<h2>Fast path: Pages Router</h2>\n<p><code>pagesDir</code> makes a pages-router source near-drop-in — <strong>Phase 7 is then\nautomatic</strong>:</p>\n<ol>\n<li><code>pracht({ pagesDir: \"/src/pages\" })</code> in <code>vite.config.ts</code>; copy <code>pages/</code> to\n<code>src/pages/</code>.</li>\n<li><code>_app.tsx</code> → pracht shell shape (<code>Shell</code> export taking <code>children</code>). A\nsubdirectory <code>_app</code> scopes a shell to that subtree (<code>blog/_app.tsx</code> →\n<code>\"pages:blog\"</code>), where App Router <code>layout.tsx</code> lands — but shells replace\nrather than nest, so it repeats the chrome, <code>head()</code>, and <code>headers()</code> it\nneeds.</li>\n<li><code>getServerSideProps</code>/<code>getStaticProps</code> → <code>loader</code> export.</li>\n<li><code>export const RENDER_MODE = \"ssg\"</code> on static pages (<code>\"ssr\"</code> is the\ndefault). For time-revalidated pages export <code>RENDER_MODE = \"isg\"</code> plus a\npositive integer <code>REVALIDATE</code> in seconds; webhook policies require ejecting\nto a manifest.</li>\n<li><code>middleware.ts</code> → a root-level <code>src/pages/_middleware.ts</code> exporting a\n<code>MiddlewareFn</code> (Phase 6). It runs on every page route; API routes are not\nwrapped. Move <code>config.matcher</code> checks into the body and compare\n<code>stripBase(url.pathname)</code>. A nested <code>_middleware</code>, a <code>_middleware/</code>\ndirectory, and a missing export are build/doctor/verify errors.</li>\n<li>Other <code>_</code>-prefixed files and directories are reserved: pracht ignores the\nsubtree. Capabilities live in <code>src/capabilities/</code> (auto-discovered,\nself-named); <code>agents</code> / <code>constraints</code> in <code>src/pages/_app.config.ts</code>.</li>\n<li>Run the dev server, iterate, and optionally eject later with\n<code>generateRoutesFile</code>.</li>\n</ol>\n<h2>Concept mapping</h2>\n<table>\n<thead>\n<tr>\n<th>Next.js</th>\n<th>Pracht</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>pages/</code> directory</td>\n<td><code>pagesDir</code> plugin option</td>\n<td>Auto-discovers routes from the file system</td>\n</tr>\n<tr>\n<td><code>app/page.tsx</code></td>\n<td><code>src/routes/*.tsx</code> + <code>route()</code> in manifest</td>\n<td>File is a module; wiring is explicit</td>\n</tr>\n<tr>\n<td><code>app/layout.tsx</code></td>\n<td><code>src/shells/*.tsx</code> + <code>shells</code> in <code>defineApp</code> (pages: <code>_app.tsx</code> per directory)</td>\n<td>Named shells; a directory <code>_app</code> replaces its parent. Layout data: shell <code>loader</code> + <code>useShellData()</code></td>\n</tr>\n<tr>\n<td><code>app/loading.tsx</code></td>\n<td><code>Loading</code> export on the shell</td>\n<td>SSR placeholder for SPA routes until the client router takes over</td>\n</tr>\n<tr>\n<td><code>app/error.tsx</code></td>\n<td><code>ErrorBoundary</code> export in route module</td>\n<td>Same concept, different wiring</td>\n</tr>\n<tr>\n<td><code>app/not-found.tsx</code></td>\n<td><code>notFound:</code> in <code>defineApp</code> (or <code>pages/404.tsx</code> in pagesDir mode)</td>\n<td>Not a route — never matches a URL, so it cannot shadow static assets</td>\n</tr>\n<tr>\n<td><code>middleware.ts</code></td>\n<td><code>src/middleware/*.ts</code> + <code>middleware</code> in <code>defineApp</code> (or <code>src/pages/_middleware.ts</code> in pagesDir mode)</td>\n<td>Named, applied per route/group; the pages-mode file runs on every page route</td>\n</tr>\n<tr>\n<td><code>app/api/*/route.ts</code></td>\n<td><code>src/api/*.ts</code> with <code>GET</code>/<code>POST</code> exports</td>\n<td>Auto-discovered, no manifest entry</td>\n</tr>\n<tr>\n<td><code>generateStaticParams</code></td>\n<td><code>getStaticPaths()</code> export</td>\n<td>Returns <code>RouteParams[]</code> of param objects</td>\n</tr>\n<tr>\n<td><code>generateMetadata</code></td>\n<td><code>head()</code> export</td>\n<td>Returns <code>{ title, meta }</code></td>\n</tr>\n<tr>\n<td>Server Components</td>\n<td><code>loader()</code> export</td>\n<td>Data fetching moves to the loader; the component is a Preact component</td>\n</tr>\n<tr>\n<td><code>\"use server\"</code> actions</td>\n<td>API routes + <code>&lt;Form&gt;</code> / <code>fetch</code></td>\n<td>Mutations move to <code>src/api/*</code>; return <code>Response</code> objects</td>\n</tr>\n<tr>\n<td><code>\"use client\"</code> (few, mostly-server app)</td>\n<td><code>hydration: \"islands\"</code> + <code>src/islands/</code></td>\n<td>Only islands ship JS; see Phase 4</td>\n</tr>\n<tr>\n<td><code>revalidatePath</code> / <code>res.revalidate()</code></td>\n<td><code>webhookRevalidate()</code> + <code>POST /__pracht/revalidate</code></td>\n<td>On-demand ISG; combinable with <code>timeRevalidate(seconds)</code></td>\n</tr>\n<tr>\n<td><code>useRouter()</code> (next/navigation)</td>\n<td><code>useNavigate()</code></td>\n<td>Takes a path, or <code>{ route: \"id\" }</code> after <code>pracht typegen</code></td>\n</tr>\n<tr>\n<td><code>useSearchParams()</code></td>\n<td><code>useSearchParams()</code></td>\n<td>Read-only; set with <code>useSetSearchParams()</code>. SSG gets the browser query after hydration. For typed values export a <code>search</code> schema and read <code>args.search</code> / <code>useSearch()</code></td>\n</tr>\n<tr>\n<td><code>useParams()</code></td>\n<td><code>useParams()</code></td>\n<td>Direct equivalent; also <code>params</code> in loader args</td>\n</tr>\n<tr>\n<td><code>next/link</code> <code>&lt;Link&gt;</code></td>\n<td><code>&lt;Link route=\"...\" params={{…}}&gt;</code> or plain <code>&lt;a&gt;</code></td>\n<td>Prefer typed <code>&lt;Link&gt;</code> after <code>pracht typegen</code>; the router intercepts same-origin anchors</td>\n</tr>\n<tr>\n<td><code>next/link</code> <code>prefetch={false}</code></td>\n<td><code>&lt;Link prefetch=\"none\"&gt;</code></td>\n<td>Default <code>\"intent\"</code> (hover/focus); also <code>\"viewport\"</code>, <code>\"render\"</code></td>\n</tr>\n<tr>\n<td><code>useLinkStatus()</code> / pending UI</td>\n<td><code>useNavigation()</code></td>\n<td><code>{ state, location, formData }</code> — progress bars, optimistic UI</td>\n</tr>\n<tr>\n<td><code>next/image</code></td>\n<td><code>&lt;Image&gt;</code> from <code>@pracht/image</code></td>\n<td>Responsive srcsets; Node, Cloudflare, Vercel, or passthrough loaders</td>\n</tr>\n<tr>\n<td><code>next/head</code> or Metadata API</td>\n<td><code>head()</code> export on route/shell</td>\n<td>Per-route and per-shell head merging</td>\n</tr>\n<tr>\n<td><code>next/script</code> <code>&lt;Script&gt;</code></td>\n<td><code>&lt;Script&gt;</code> from <code>@pracht/core</code></td>\n<td><code>beforeHydration</code> (≈ <code>beforeInteractive</code>), <code>afterHydration</code> (≈ <code>afterInteractive</code>, default), <code>idle</code> (≈ <code>lazyOnload</code>), <code>visible</code></td>\n</tr>\n<tr>\n<td><code>cookies()</code> / <code>headers()</code></td>\n<td><code>request.headers</code> in loader/middleware/API args</td>\n<td>No separate API — read the standard <code>Request</code></td>\n</tr>\n<tr>\n<td><code>className</code></td>\n<td><code>class</code></td>\n<td>Preact uses the <code>class</code> attribute</td>\n</tr>\n<tr>\n<td><code>react</code> / <code>react-dom</code> imports</td>\n<td><code>preact/hooks</code>, <code>preact/compat</code></td>\n<td>Same hook APIs</td>\n</tr>\n<tr>\n<td><code>import React from \"react\"</code></td>\n<td>Remove</td>\n<td>The Vite plugin handles JSX</td>\n</tr>\n</tbody>\n</table>\n<p>Scroll restoration on back/forward works out of the box. <code>&lt;Link&gt;</code> also takes\n<code>preserveScroll</code> (skip the scroll-to-top reset) and <code>viewTransition</code> (wrap the\nnavigation in <code>document.startViewTransition()</code> where supported).</p>\n<h2>Phase 1: Project setup</h2>\n<p>Create <code>src/routes.ts</code> (manifest), <code>src/routes/</code>, <code>src/shells/</code>,\n<code>src/middleware/</code>, <code>src/api/</code>, and a <code>vite.config.ts</code> whose <code>plugins</code> array\ncontains <code>pracht()</code> from <code>@pracht/vite-plugin</code>. Then:</p>\n<ul>\n<li>Dependencies: drop <code>react</code>/<code>react-dom</code> for <code>preact</code>; drop <code>next</code> for\n<code>@pracht/core</code> (runtime), <code>@pracht/cli</code> (the <code>pracht</code> bin),\n<code>@pracht/vite-plugin</code>, and a target adapter such as <code>@pracht/adapter-node</code>.\nThere is no package named <code>pracht</code>. Add <code>@pracht/image</code> if the app used\n<code>next/image</code>, and <code>sharp</code> only for the built-in Node optimization endpoint\nor build-time <code>?pracht</code> imports.</li>\n<li>Scripts: <code>dev</code> → <code>pracht dev</code>, <code>build</code> → <code>pracht build</code>, <code>start</code> →\n<code>node dist/server/server.js</code> (Node) or the platform deploy command; add\n<code>preview</code> → <code>pracht preview</code>.</li>\n<li>Delete <code>next.config.*</code>, <code>next-env.d.ts</code>, <code>.next/</code>.</li>\n<li>In <code>tsconfig.json</code>, <code>\"jsx\": \"preserve\"</code> → <code>\"jsx\": \"react-jsx\"</code> with\n<code>\"jsxImportSource\": \"preact\"</code>.</li>\n</ul>\n<h2>Phase 2: Layouts → shells</h2>\n<pre><code>import type { ShellProps } from \"@pracht/core\";\n\nexport function Shell({ children }: ShellProps) {\n  return &lt;div class=\"root\"&gt;&lt;main&gt;{children}&lt;/main&gt;&lt;/div&gt;;\n}\n\nexport function head() {\n  return { title: \"My App\" };\n}\n</code></pre>\n<p>Shells must NOT render <code>&lt;html&gt;</code>, <code>&lt;head&gt;</code>, or <code>&lt;body&gt;</code> — the framework owns\nthe document, so move anything from <code>RootLayout</code>'s document tags into <code>head()</code>.\nRegister as <code>defineApp({ shells: { main: \"./shells/main.tsx\" } })</code>.</p>\n<h2>Phase 3: Pages → route modules</h2>\n<p>An <code>async</code> page that fetches and exports <code>generateMetadata</code> becomes three\nexports — the fetch moves to <code>loader</code>, the metadata to <code>head</code>, the JSX stays\nin the default export:</p>\n<pre><code>import type { LoaderArgs, RouteComponentProps } from \"@pracht/core\";\n\nexport async function loader(_args: LoaderArgs) {\n  const res = await fetch(\"https://api.example.com/data\");\n  return res.json();\n}\n\nexport function head({ data }: { data: Awaited&lt;ReturnType&lt;typeof loader&gt;&gt; }) {\n  return { title: data.title };\n}\n\nexport default function Page({ data }: RouteComponentProps&lt;typeof loader&gt;) {\n  return &lt;div class=\"page\"&gt;{data.title}&lt;/div&gt;;\n}\n</code></pre>\n<p>Components are never <code>async</code> — data arrives as the <code>data</code> prop.</p>\n<h2>Phase 4: Client components</h2>\n<p>Drop the <code>\"use client\"</code> directive (pracht has no such concept), and repoint\nimports: <code>react</code> → <code>preact/hooks</code> for hooks, <code>react</code>/<code>react-dom</code> →\n<code>preact/compat</code> for everything else. The component body is otherwise unchanged.</p>\n<p><strong>Islands:</strong> if the source is mostly server components with a handful of\n<code>\"use client\"</code> leaves, do not silently regress those pages to full-page\nhydration. Set <code>hydration: \"islands\"</code> on the route (or\n<code>export const HYDRATION = \"islands\"</code> in pages mode) and move the interactive\ncomponents into <code>src/islands/</code> — the rest renders as inert HTML.</p>\n<h2>Phase 5: API routes</h2>\n<p><code>app/api/users/route.ts</code> → <code>src/api/users.ts</code>, dynamic segments included\n(<code>app/api/users/[id]/route.ts</code> → <code>src/api/users/[id].ts</code>). Handlers take\n<code>ApiRouteArgs</code> and use web standards throughout:</p>\n<pre><code>import type { ApiRouteArgs } from \"@pracht/core\";\n\nexport async function GET({ request }: ApiRouteArgs) {\n  return Response.json(await getUsers());\n}\n</code></pre>\n<p><code>NextRequest</code> → the standard <code>Request</code> from <code>ApiRouteArgs</code>;\n<code>NextResponse.json()</code> → <code>Response.json()</code>. No manifest wiring — API routes are\nauto-discovered.</p>\n<h2>Phase 6: Middleware</h2>\n<pre><code>import { redirect, type MiddlewareFn } from \"@pracht/core\";\n\nexport const middleware: MiddlewareFn = async ({ request }, next) =&gt; {\n  const session = request.headers.get(\"cookie\")?.includes(\"session\");\n  if (!session) return redirect(\"/login\", { request });\n  return next();\n};\n</code></pre>\n<p><code>NextResponse.redirect()</code> → <code>return redirect(\"/path\", { request })</code>;\n<code>NextResponse.next()</code> → <code>return next()</code>. Path matching moves out of\n<code>config.matcher</code> and into manifest assignment:\n<code>group({ middleware: [\"auth\"] }, [route(\"/dashboard\", …)])</code>. Pracht middleware\nis wrap-around (Hono/Koa/Astro shape), so you can <code>await next()</code> and observe\nthe response — useful for tracing.</p>\n<p>In <code>pagesDir</code> mode the same <code>MiddlewareFn</code> goes in a root-level\n<code>src/pages/_middleware.ts</code>, applied to every page route (API routes stay\nunwrapped). Do not migrate a per-request auth matcher onto an <code>ssg</code>/<code>isg</code> page\nand call it protected: that document runs middleware only at\nbuild/revalidation, with a sanitized request. Keep such pages <code>ssr</code>/<code>spa</code>, or\nkeep a separately verified edge gate. Pure static exports have no request\nruntime.</p>\n<h2>Phase 7: Route manifest</h2>\n<p>Skip this phase for <code>pagesDir</code> projects. Prefer\n<code>pracht generate route --path ... --render ...</code> (plus <code>--shell</code>/<code>--middleware</code>/\n<code>--loader</code>) per page — it creates a wired skeleton <strong>and</strong> updates\n<code>src/routes.ts</code> — then port the Next.js bodies into the generated files.\nHand-write manifest entries only for shapes the generator cannot express.</p>\n<pre><code>import { defineApp, group, route } from \"@pracht/core\";\n\nexport const app = defineApp({\n  shells: { main: () =&gt; import(\"./shells/main.tsx\") },\n  middleware: { auth: () =&gt; import(\"./middleware/auth.ts\") },\n  routes: [\n    group({ shell: \"main\" }, [\n      route(\"/\", () =&gt; import(\"./routes/home.tsx\"), { render: \"ssg\" }),\n      route(\"/dashboard\", () =&gt; import(\"./routes/dashboard.tsx\"), {\n        render: \"ssr\",\n        middleware: [\"auth\"],\n      }),\n      route(\"/blog/:slug\", () =&gt; import(\"./routes/blog-post.tsx\"), { render: \"isg\" }),\n    ]),\n  ],\n  notFound: { component: () =&gt; import(\"./routes/not-found.tsx\"), shell: \"main\" },\n});\n</code></pre>\n<p>Module references accept <code>() =&gt; import(\"./path\")</code> (better IDE navigation) or a\nplain <code>\"./path\"</code> string. Pick render modes from the Next.js original:</p>\n<table>\n<thead>\n<tr>\n<th>Next.js original</th>\n<th>Render mode</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>No data fetching, or <code>generateStaticParams</code></td>\n<td><code>\"ssg\"</code></td>\n</tr>\n<tr>\n<td><code>cookies()</code>, <code>headers()</code>, per-request data</td>\n<td><code>\"ssr\"</code></td>\n</tr>\n<tr>\n<td><code>revalidate</code> option</td>\n<td><code>\"isg\"</code> + <code>timeRevalidate(seconds)</code></td>\n</tr>\n<tr>\n<td><code>revalidatePath</code> / <code>res.revalidate()</code></td>\n<td><code>\"isg\"</code> + <code>webhookRevalidate()</code>, triggered by <code>POST /__pracht/revalidate</code></td>\n</tr>\n<tr>\n<td>Client-only</td>\n<td><code>\"spa\"</code></td>\n</tr>\n</tbody>\n</table>\n<h2>Phase 8: Remaining patterns</h2>\n<p><strong>Links and navigation.</strong> After the manifest exists, run <code>pracht typegen</code> and\nswitch known app routes to route ids: <code>&lt;Link route=\"product\" params={{ id }}&gt;</code>\nand <code>navigate({ route: \"dashboard\" })</code>. Plain <code>&lt;a href=\"/about\"&gt;</code> and\n<code>navigate(\"/dashboard\")</code> keep working for simple, external, or user-provided\nURLs.</p>\n<p><strong>Images.</strong> <code>&lt;Image&gt;</code> from <code>@pracht/image</code> takes the same\n<code>width</code>/<code>height</code>/<code>fill</code>/<code>sizes</code>/<code>quality</code> and priority intent — preserve them.\nPick the loader for the deployment target:</p>\n<table>\n<thead>\n<tr>\n<th>Target</th>\n<th>Loader</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Node</td>\n<td><code>createImageHandler()</code> from <code>@pracht/image/node</code> + <code>sharp</code>; set its <code>localOrigin</code> to the same trusted value as <code>nodeAdapter({ canonicalOrigin })</code></td>\n</tr>\n<tr>\n<td>Cloudflare Workers</td>\n<td><code>cloudflareLoader</code> — never bundle the Node handler, <code>sharp</code> does not run in Workers</td>\n</tr>\n<tr>\n<td>Vercel Edge</td>\n<td><code>vercelLoader</code>, with Vercel's allowed image sizes aligned to the pracht breakpoints</td>\n</tr>\n<tr>\n<td>Static hosts</td>\n<td><code>passthroughLoader</code></td>\n</tr>\n</tbody>\n</table>\n<p>Static imports and blur placeholders migrate too: <code>import photo from \"./photo.jpg\"</code> → <code>\"./photo.jpg?pracht\"</code>, add <code>prachtImage()</code> (from\n<code>@pracht/image/vite</code>) to the Vite plugins, reference the <code>@pracht/image/client</code>\ntypes once in a <code>.d.ts</code>, and keep <code>&lt;Image src={photo} placeholder=\"blur\" /&gt;</code>\nas-is — the import supplies <code>width</code>/<code>height</code>/<code>blurDataURL</code> exactly like Next's\nstatic imports, though pracht's blur is CSS-only (no fade, no inline handlers).\nWhere <code>next/image</code> produced files during a static export, use\n<code>?pracht&amp;pracht-static</code>: it emits cached responsive WebP variants and bypasses\nthe runtime loader while keeping plain hydration-free <code>&lt;img&gt;</code> markup. For\nrelative images in Markdown, <code>defineMarkdownCollection()</code> from\n<code>@pracht/markdown</code> applies the same pipeline to <code>![alt](./photo.jpg)</code>. Leave\n<code>public/</code> and remote URLs unchanged, and use an absolute Vite <code>base</code> for static\nvariants. See <a href=\"https://pracht.resynapse.dev/docs/images\">https://pracht.resynapse.dev/docs/images</a>.</p>\n<p><strong>Server Actions.</strong> A <code>\"use server\"</code> mutation becomes an API route; the\n<code>revalidatePath</code> half becomes an authenticated webhook call:</p>\n<pre><code>import { withBase, type ApiRouteArgs } from \"@pracht/core\";\n\nexport async function POST({ request }: ApiRouteArgs) {\n  const form = await request.formData();\n  await db.insert({ title: form.get(\"title\") });\n  await fetch(new URL(withBase(\"/__pracht/revalidate\"), request.url), {\n    method: \"POST\",\n    headers: {\n      authorization: `Bearer ${process.env.PRACHT_REVALIDATE_TOKEN}`,\n      \"content-type\": \"application/json\",\n    },\n    body: JSON.stringify({ paths: [\"/posts\"] }),\n  });\n  return new Response(null, { status: 303, headers: { location: withBase(\"/posts\") } });\n}\n</code></pre>\n<p>That call only does something if <code>/posts</code> is <code>render: \"isg\"</code> and opts in with\n<code>revalidate: webhookRevalidate()</code> (or <code>[timeRevalidate(seconds), webhookRevalidate()]</code>, both imported from <code>@pracht/core</code>), and\n<code>PRACHT_REVALIDATE_TOKEN</code> is set in the runtime environment. If <code>/posts</code> is a\nplain SSR route, drop the call — the redirect re-renders it fresh.</p>\n<h2>Phase 9: Clean up and verify</h2>\n<p>Sweep for leftovers: <code>\"use client\"</code>/<code>\"use server\"</code> directives, <code>next/*</code>\nimports, <code>className</code>, <code>react</code> imports, and <code>next.config.*</code>/<code>next-env.d.ts</code>/\n<code>.next/</code>. Run <code>pracht typegen</code> if route ids or paths changed, then <code>pracht dev</code>\nand fix errors iteratively.</p>\n<h2>Dependency mapping</h2>\n<table>\n<thead>\n<tr>\n<th>Next.js package</th>\n<th>Pracht equivalent</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>next</code></td>\n<td><code>@pracht/core</code> + <code>@pracht/cli</code> + <code>@pracht/vite-plugin</code> + a target adapter</td>\n</tr>\n<tr>\n<td><code>next/image</code></td>\n<td><code>@pracht/image</code></td>\n</tr>\n<tr>\n<td><code>react</code>, <code>react-dom</code></td>\n<td><code>preact</code></td>\n</tr>\n<tr>\n<td><code>next/font/local</code></td>\n<td>Import WOFF2 with <code>?url&amp;no-inline</code> into <a href=\"https://pracht.resynapse.dev/docs/fonts\"><code>defineFont()</code></a>; register <code>head().fonts</code>, apply <code>font.className</code>/<code>font.style</code></td>\n</tr>\n<tr>\n<td><code>next/font/google</code></td>\n<td>Self-host WOFF2 in <code>src/fonts/</code>, then follow <code>next/font/local</code>; pracht never fetches fonts at build time</td>\n</tr>\n<tr>\n<td><code>@next/mdx</code></td>\n<td><code>@mdx-js/rollup</code> (Vite plugin)</td>\n</tr>\n<tr>\n<td><code>next-auth</code></td>\n<td>Direct integration in middleware/loaders</td>\n</tr>\n<tr>\n<td><code>next/og</code></td>\n<td><code>@vercel/og</code> or a custom solution</td>\n</tr>\n</tbody>\n</table>\n<p>Most React libraries work through <code>preact/compat</code>, and the pracht Vite plugin\nalready aliases <code>react</code>/<code>react-dom</code>/<code>react/jsx-runtime</code> for you. Add manual\n<code>resolve.alias</code> entries only when a dependency still fails to resolve — and\nflag those libraries to the user.</p>\n<p>$ARGUMENTS</p>\n","files":[{"path":"SKILL.md","sizeBytes":20106,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-19T21:18:11.214851Z","sha256":"6F21137D03BB186B7F416218174BCD73DC4A23EC097BFF487970023BD5AEF299","sizeBytes":7409},"review":null,"source":{"repositoryUrl":"https://github.com/JoviDeCroock/pracht","path":"skills/migrate-nextjs","license":"MIT","commit":"8bcdd52989de3e97708e7f16221839d6313cfdac","subtreeSha":"44B9B39E45ADC63177206DE13DD99590B83CA919FF6CBDBA526D2EBA1CDF7D61","lastSyncedAt":"2026-10-19T21:17:21.911347Z"},"reviewedAt":"2026-10-19T21:19:40.25662Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/migrate-nextjs"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart"},{"target":"git","command":"git clone https://github.com/JoviDeCroock/pracht.git"}]}