Source profileQuality 91/100

JoviDeCroock/pracht/skills/migrate-nextjs/SKILL.md

migrate-nextjs

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 to pracht", "nextjs migration", "switch from next".

Source repository stars
94
Declared platforms
0
Static risk flags
2
Last source update
2026-08-28
Source checked
2026-08-28

Decision brief

What it does: where it fits

Migrate in phases: setup → shells → routes → client components → API → middleware → manifest → patterns → cleanup. Read each Next.js source file before converting it; never infer from the filename. Prefer the simplest pracht equivalent, and when a Next.js feature has no equivale…

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/migrate-nextjs"
    Safe inspection promptEditorial

    Inspect the Agent Skill "migrate-nextjs" from https://github.com/JoviDeCroock/pracht/blob/43a8e8dcccb137abcc7c78d4a238edc01829e5a9/skills/migrate-nextjs/SKILL.md at commit 43a8e8dcccb137abcc7c78d4a238edc01829e5a9. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      Step 0: Assess the source

      Read next.config. and package.json (React/Next versions, deps), then map the tree: app/ (App Router), pages/ (Pages Router), middleware.ts, app/api/ or pages/api/. Note which patterns are in use — "use client", async server components, generateStaticParams, generateMetadata/meta…

      Read next.config. and package.json (React/Next versions, deps), then map the tree: app/ (App Router), pages/ (Pages Router), middleware.ts, app/api/ or pages/api/. Note which patterns are in use — "use client", async se…
    2. 02

      Phase 1: Project setup

      Create src/routes.ts (manifest), src/routes/, src/shells/, src/middleware/, src/api/, and a vite.config.ts whose plugins array contains pracht() from @pracht/vite-plugin. Then:

      Dependencies: drop react/react-dom for preact; drop next forScripts: dev → pracht dev, build → pracht build, start →Delete next.config., next-env.d.ts, .next/.
    3. 03

      Phase 2: Layouts → shells

      Shells must NOT render , , or — the framework owns the document, so move anything from RootLayout's document tags into head(). Register as defineApp({ shells: { main: "./shells/main.tsx" } }).

      Shells must NOT render , , or — the framework owns the document, so move anything from RootLayout's document tags into head(). Register as defineApp({ shells: { main: "./shells/main.tsx" } }).
    4. 04

      Phase 3: Pages → route modules

      An async page that fetches and exports generateMetadata becomes three exports — the fetch moves to loader, the metadata to head, the JSX stays in the default export:

      An async page that fetches and exports generateMetadata becomes three exports — the fetch moves to loader, the metadata to head, the JSX stays in the default export:Components are never async — data arrives as the data prop.
    5. 05

      Phase 4: Client components

      Drop the "use client" directive (pracht has no such concept), and repoint imports: react → preact/hooks for hooks, react/react-dom → preact/compat for everything else. The component body is otherwise unchanged.

      Drop the "use client" directive (pracht has no such concept), and repoint imports: react → preact/hooks for hooks, react/react-dom → preact/compat for everything else. The component body is otherwise unchanged.Islands: if the source is mostly server components with a handful of "use client" leaves, do not silently regress those pages to full-page hydration. Set hydration: "islands" on the route (or export const HYDRATION = "i…

    Permission review

    Static risk signals and limitations

    Reads files

    low · line 5

    The documentation asks the agent to read local files, directories, or repositories.

    middleware → manifest → patterns → cleanup. Read each Next.js source file

    Network access

    medium · line 60

    The documentation includes network, browsing, or remote request actions.

    | `useSearchParams()` | `useSearchParams()` | Reactive read-only params; SSG gets the browser query after hydration, loaders use `url.searchParams` |

    Network access

    medium · line 68

    The documentation includes network, browsing, or remote request actions.

    | `cookies()` / `headers()` | `request.headers` in loader/middleware/API args | No separate API — read the standard `Request` |

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score91/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars94SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    JoviDeCroock/pracht
    Skill path
    skills/migrate-nextjs/SKILL.md
    Commit
    43a8e8dcccb137abcc7c78d4a238edc01829e5a9
    License
    MIT
    Collected
    2026-08-28
    Default branch
    main
    View the original SKILL.md

    Migrate Next.js to Pracht

    Migrate in phases: setup → shells → routes → client components → API → middleware → manifest → patterns → cleanup. Read each Next.js source file before converting it; never infer from the filename. Prefer the simplest pracht equivalent, and when a Next.js feature has no equivalent, say so and propose an alternative instead of inventing one.

    MCP: when the pracht MCP server is registered (docs/MCP.md), use generate_route/generate_shell/generate_middleware/generate_api to scaffold and inspect_routes/inspect_api/doctor/verify to check progress, instead of Bash. pracht inspect needs the pracht plugin in the vite config; inspect_build needs a prior pracht build.

    Step 0: Assess the source

    Read next.config.* and package.json (React/Next versions, deps), then map the tree: app/ (App Router), pages/ (Pages Router), middleware.ts, app/api/ or pages/api/. Note which patterns are in use — "use client", async server components, generateStaticParams, generateMetadata/metadata, "use server" actions — and the third-party integrations (auth, CMS, DB, analytics). Confirm scope with the user if the app has more than ~20 routes.

    Fast path: Pages Router

    pagesDir makes a pages-router source near-drop-in — Phase 7 is then automatic:

    1. pracht({ pagesDir: "/src/pages" }) in vite.config.ts; copy pages/ to src/pages/.
    2. _app.tsx → pracht shell shape (Shell export taking children).
    3. getServerSideProps/getStaticPropsloader export.
    4. export const RENDER_MODE = "ssg" on static pages ("ssr" is the default). For time-revalidated pages export RENDER_MODE = "isg" plus a positive integer REVALIDATE in seconds; webhook policies require ejecting to a manifest.
    5. Run the dev server, iterate, and optionally eject later with generateRoutesFile.

    Concept mapping

    Next.jsPrachtNotes
    pages/ directorypagesDir plugin optionAuto-discovers routes from the file system
    app/page.tsxsrc/routes/*.tsx + route() in manifestFile is a module; wiring is explicit
    app/layout.tsxsrc/shells/*.tsx + shells in defineAppShells are named, not directory-nested
    app/loading.tsxLoading export on the shellSSR placeholder for SPA routes until the client router takes over
    app/error.tsxErrorBoundary export in route moduleSame concept, different wiring
    app/not-found.tsxnotFound: in defineApp (or pages/404.tsx in pagesDir mode)Not a route — never matches a URL, so it cannot shadow static assets
    middleware.tssrc/middleware/*.ts + middleware in defineAppNamed, applied per route/group
    app/api/*/route.tssrc/api/*.ts with GET/POST exportsAuto-discovered, no manifest entry
    generateStaticParamsgetStaticPaths() exportReturns RouteParams[] of param objects
    generateMetadatahead() exportReturns { title, meta }
    Server Componentsloader() exportData fetching moves to the loader; the component is a Preact component
    "use server" actionsAPI routes + <Form> / fetchMutations move to src/api/*; return Response objects
    "use client" (few, mostly-server app)hydration: "islands" + src/islands/Only islands ship JS; see Phase 4
    revalidatePath / res.revalidate()webhookRevalidate() + POST /__pracht/revalidateOn-demand ISG; combinable with timeRevalidate(seconds)
    useRouter() (next/navigation)useNavigate()Takes a path, or { route: "id" } after pracht typegen
    useSearchParams()useSearchParams()Reactive read-only params; SSG gets the browser query after hydration, loaders use url.searchParams
    useParams()useParams()Direct equivalent; also params in loader args
    next/link <Link><Link route="..." params={{…}}> or plain <a>Prefer typed <Link> after pracht typegen; the router intercepts same-origin anchors
    next/link prefetch={false}<Link prefetch="none">Default "intent" (hover/focus); also "viewport", "render"
    useLinkStatus() / pending UIuseNavigation(){ state, location, formData } — progress bars, optimistic UI
    next/image<Image> from @pracht/imageResponsive srcsets; Node, Cloudflare, Vercel, or passthrough loaders
    next/head or Metadata APIhead() export on route/shellPer-route and per-shell head merging
    next/script <Script><Script> from @pracht/corebeforeHydration (≈ beforeInteractive), afterHydration (≈ afterInteractive, default), idle (≈ lazyOnload), visible
    cookies() / headers()request.headers in loader/middleware/API argsNo separate API — read the standard Request
    classNameclassPreact uses the class attribute
    react / react-dom importspreact/hooks, preact/compatSame hook APIs
    import React from "react"RemoveThe Vite plugin handles JSX

    Scroll restoration on back/forward works out of the box. <Link> also takes preserveScroll (skip the scroll-to-top reset) and viewTransition (wrap the navigation in document.startViewTransition() where supported).

    Phase 1: Project setup

    Create src/routes.ts (manifest), src/routes/, src/shells/, src/middleware/, src/api/, and a vite.config.ts whose plugins array contains pracht() from @pracht/vite-plugin. Then:

    • Dependencies: drop react/react-dom for preact; drop next for @pracht/core (runtime), @pracht/cli (the pracht bin), @pracht/vite-plugin, and a target adapter such as @pracht/adapter-node. There is no package named pracht. Add @pracht/image if the app used next/image, and sharp only for the built-in Node optimization endpoint or build-time ?pracht imports.
    • Scripts: devpracht dev, buildpracht build, startnode dist/server/server.js (Node) or the platform deploy command; add previewpracht preview.
    • Delete next.config.*, next-env.d.ts, .next/.
    • In tsconfig.json, "jsx": "preserve""jsx": "react-jsx" with "jsxImportSource": "preact".

    Phase 2: Layouts → shells

    import type { ShellProps } from "@pracht/core";
    
    export function Shell({ children }: ShellProps) {
      return <div class="root"><main>{children}</main></div>;
    }
    
    export function head() {
      return { title: "My App" };
    }
    

    Shells must NOT render <html>, <head>, or <body> — the framework owns the document, so move anything from RootLayout's document tags into head(). Register as defineApp({ shells: { main: "./shells/main.tsx" } }).

    Phase 3: Pages → route modules

    An async page that fetches and exports generateMetadata becomes three exports — the fetch moves to loader, the metadata to head, the JSX stays in the default export:

    import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
    
    export async function loader(_args: LoaderArgs) {
      const res = await fetch("https://api.example.com/data");
      return res.json();
    }
    
    export function head({ data }: { data: Awaited<ReturnType<typeof loader>> }) {
      return { title: data.title };
    }
    
    export default function Page({ data }: RouteComponentProps<typeof loader>) {
      return <div class="page">{data.title}</div>;
    }
    

    Components are never async — data arrives as the data prop.

    Phase 4: Client components

    Drop the "use client" directive (pracht has no such concept), and repoint imports: reactpreact/hooks for hooks, react/react-dompreact/compat for everything else. The component body is otherwise unchanged.

    Islands: if the source is mostly server components with a handful of "use client" leaves, do not silently regress those pages to full-page hydration. Set hydration: "islands" on the route (or export const HYDRATION = "islands" in pages mode) and move the interactive components into src/islands/ — the rest renders as inert HTML. See docs/ISLANDS.md.

    Phase 5: API routes

    app/api/users/route.tssrc/api/users.ts, dynamic segments included (app/api/users/[id]/route.tssrc/api/users/[id].ts). Handlers take ApiRouteArgs and use web standards throughout:

    import type { ApiRouteArgs } from "@pracht/core";
    
    export async function GET({ request }: ApiRouteArgs) {
      return Response.json(await getUsers());
    }
    

    NextRequest → the standard Request from ApiRouteArgs; NextResponse.json()Response.json(). No manifest wiring — API routes are auto-discovered.

    Phase 6: Middleware

    import { redirect, type MiddlewareFn } from "@pracht/core";
    
    export const middleware: MiddlewareFn = async ({ request }, next) => {
      const session = request.headers.get("cookie")?.includes("session");
      if (!session) return redirect("/login", { request });
      return next();
    };
    

    NextResponse.redirect()return redirect("/path", { request }); NextResponse.next()return next(). Path matching moves out of config.matcher and into manifest assignment: group({ middleware: ["auth"] }, [route("/dashboard", …)]). Pracht middleware is wrap-around (Hono/Koa/Astro shape), so you can await next() and observe the response — useful for tracing.

    Phase 7: Route manifest

    Skip this phase for pagesDir projects. Prefer pracht generate route --path ... --render ... (plus --shell/--middleware/ --loader) per page — it creates a wired skeleton and updates src/routes.ts — then port the Next.js bodies into the generated files. Hand-write manifest entries only for shapes the generator cannot express.

    import { defineApp, group, route } from "@pracht/core";
    
    export const app = defineApp({
      shells: { main: () => import("./shells/main.tsx") },
      middleware: { auth: () => import("./middleware/auth.ts") },
      routes: [
        group({ shell: "main" }, [
          route("/", () => import("./routes/home.tsx"), { render: "ssg" }),
          route("/dashboard", () => import("./routes/dashboard.tsx"), {
            render: "ssr",
            middleware: ["auth"],
          }),
          route("/blog/:slug", () => import("./routes/blog-post.tsx"), { render: "isg" }),
        ]),
      ],
      notFound: { component: () => import("./routes/not-found.tsx"), shell: "main" },
    });
    

    Module references accept () => import("./path") (better IDE navigation) or a plain "./path" string. Pick render modes from the Next.js original:

    Next.js originalRender mode
    No data fetching, or generateStaticParams"ssg"
    cookies(), headers(), per-request data"ssr"
    revalidate option"isg" + timeRevalidate(seconds)
    revalidatePath / res.revalidate()"isg" + webhookRevalidate(), triggered by POST /__pracht/revalidate
    Client-only"spa"

    Phase 8: Remaining patterns

    Links and navigation. After the manifest exists, run pracht typegen and switch known app routes to route ids: <Link route="product" params={{ id }}> and navigate({ route: "dashboard" }). Plain <a href="/about"> and navigate("/dashboard") keep working for simple, external, or user-provided URLs.

    Images. <Image> from @pracht/image takes the same width/height/fill/sizes/quality and priority intent — preserve them. Pick the loader for the deployment target:

    TargetLoader
    NodecreateImageHandler() from @pracht/image/node + sharp; set its localOrigin to the same trusted value as nodeAdapter({ canonicalOrigin })
    Cloudflare WorkerscloudflareLoader — never bundle the Node handler, sharp does not run in Workers
    Vercel EdgevercelLoader, with Vercel's allowed image sizes aligned to the pracht breakpoints
    Static hostspassthroughLoader

    Static imports and blur placeholders migrate too: import photo from "./photo.jpg""./photo.jpg?pracht", add prachtImage() (from @pracht/image/vite) to the Vite plugins, reference the @pracht/image/client types once in a .d.ts, and keep <Image src={photo} placeholder="blur" /> as-is — the import supplies width/height/blurDataURL exactly like Next's static imports, though pracht's blur is CSS-only (no fade, no inline handlers). Where next/image produced files during a static export, use ?pracht&pracht-static: it emits cached responsive WebP variants and bypasses the runtime loader while keeping plain hydration-free <img> markup. For relative images in Markdown, defineMarkdownCollection() from @pracht/markdown applies the same pipeline to ![alt](./photo.jpg). Leave public/ and remote URLs unchanged, and use an absolute Vite base for static variants. See docs/IMAGES.md.

    Server Actions. A "use server" mutation becomes an API route; the revalidatePath half becomes an authenticated webhook call:

    import { withBase, type ApiRouteArgs } from "@pracht/core";
    
    export async function POST({ request }: ApiRouteArgs) {
      const form = await request.formData();
      await db.insert({ title: form.get("title") });
      await fetch(new URL(withBase("/__pracht/revalidate"), request.url), {
        method: "POST",
        headers: {
          authorization: `Bearer ${process.env.PRACHT_REVALIDATE_TOKEN}`,
          "content-type": "application/json",
        },
        body: JSON.stringify({ paths: ["/posts"] }),
      });
      return new Response(null, { status: 303, headers: { location: withBase("/posts") } });
    }
    

    That call only does something if /posts is render: "isg" and opts in with revalidate: webhookRevalidate() (or [timeRevalidate(seconds), webhookRevalidate()], both imported from @pracht/core), and PRACHT_REVALIDATE_TOKEN is set in the runtime environment. If /posts is a plain SSR route, drop the call — the redirect re-renders it fresh.

    Phase 9: Clean up and verify

    Sweep for leftovers: "use client"/"use server" directives, next/* imports, className, react imports, and next.config.*/next-env.d.ts/ .next/. Run pracht typegen if route ids or paths changed, then pracht dev and fix errors iteratively.

    Dependency mapping

    Next.js packagePracht equivalent
    next@pracht/core + @pracht/cli + @pracht/vite-plugin + a target adapter
    next/image@pracht/image
    react, react-dompreact
    next/font/localdefineFont() from @pracht/core — register via head() { return { fonts: [font] } }, use font.className/font.style
    next/font/googleDownload the woff2 files into public/fonts/ (e.g. via google-webfonts-helper), then defineFont() — pracht never fetches fonts at build time
    @next/mdx@mdx-js/rollup (Vite plugin)
    next-authDirect integration in middleware/loaders
    next/og@vercel/og or a custom solution

    Most React libraries work through preact/compat, and the pracht Vite plugin already aliases react/react-dom/react/jsx-runtime for you. Add manual resolve.alias entries only when a dependency still fails to resolve — and flag those libraries to the user.

    $ARGUMENTS

    Frequently asked questions

    What to verify before installation and use

    What does the migrate-nextjs source document cover?

    Migrate in phases: setup → shells → routes → client components → API → middleware → manifest → patterns → cleanup. Read each Next.js source file before converting it; never infer from the filename. Prefer the simplest pracht equivalent, and when a Next.js feature has no equivale…

    How do I install migrate-nextjs?

    The source record exposes this install command: npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/migrate-nextjs". Inspect the command and pinned source before running it.

    Which permission-related actions were detected?

    Static rules flagged read-files, network in the source; the page lists the matching lines and excerpts.

    Alternatives

    Compare before choosing

    Computed 961,113

    fcakyon/claude-codex-settings

    vercel-react-view-transitions

    Guide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view

    Computed 96224

    yonatangross/orchestkit

    json-render-catalog

    json-render component catalog patterns for AI-safe generative UI. Define Zod-typed catalogs that constrain what AI can generate, use @json-render/shadcn for 36 pre-built components, optimize specs with YAML mode, and apply the three edit modes (patch/merge/diff) for progressive updates. Use when building AI-generated UIs, defining component catalogs, or integrating json-render into React/Vue/Svelte/React Native/Ink/Next.js projects.

    Computed 96224

    yonatangross/orchestkit

    react-server-components-framework

    Use when building Next.js 16+ apps with React Server Components. Covers App Router, Cache Components (replacing experimental_ppr), streaming SSR, Server Actions, and React 19 patterns for server-first architecture.

    Computed 96207

    theBGuy/GitDesktop

    vercel-react-view-transitions

    Guide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view