Best for
- Use when asked to "migrate from next", "convert next.
JoviDeCroock/pracht/skills/migrate-nextjs/SKILL.md
Migrate a Next.js application to Pracht. Converts App Router pages, layouts, middleware, API routes, data fetching, and metadata to pracht equivalents. Handles React→Preact, className→class, server components→loaders, and manifest wiring. Use when asked to "migrate from next", "convert next.js app", "port from next to pracht", "nextjs migration", or "switch from next".
Decision brief
Systematically migrate a Next.js application (App Router or Pages Router) to pracht — a full-stack Preact framework built on Vite.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/migrate-nextjs"Inspect the Agent Skill "migrate-nextjs" from https://github.com/JoviDeCroock/pracht/blob/21b95a63f0ee9ef30d4de1a2c512c317a10074df/skills/migrate-nextjs/SKILL.md at commit 21b95a63f0ee9ef30d4de1a2c512c317a10074df. 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
Before touching any code, understand what you're migrating:
1. Initialize the pracht project structure:
1. Initialize the pracht project structure:
Pracht shells do NOT render , , or — the framework owns the HTML document.
Next.js (Server Component with data):
Permission review
The documentation asks the agent to read local files, directories, or repositories.
Scan the directory structure:The documentation includes network, browsing, or remote request actions.
const res = await fetch("https://api.example.com/data");The documentation includes network, browsing, or remote request actions.
const res = await fetch("https://api.example.com/data");Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 96/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 93 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Systematically migrate a Next.js application (App Router or Pages Router) to pracht — a full-stack Preact framework built on Vite.
Before touching any code, understand what you're migrating:
next.config.js / next.config.mjs / next.config.ts for custom config.package.json for React/Next versions and dependencies.app/ → App Router (Next 13+)pages/ → Pages Router (legacy)middleware.ts → edge middlewareapp/api/ or pages/api/ → API routes"use client" directives → client componentsasync page/layout components → server components with data fetchinggenerateStaticParams → static generationgenerateMetadata / metadata export → head management"use server") → mutationsAsk the user to confirm the migration scope if the project is large (>20 routes).
If the pracht MCP server is registered (docs/MCP.md), use the generate_route/generate_shell/generate_middleware/generate_api MCP tools for scaffolding and inspect_routes/inspect_api/doctor/verify to check migration progress, instead of Bash. (pracht inspect needs the pracht plugin in the vite config; inspect_build needs a prior pracht build.)
If the source Next.js project uses the pages router (pages/ directory), pracht's pagesDir plugin option provides a near-drop-in migration:
pracht({ pagesDir: "/src/pages" }) in vite.config.tspages/ to src/pages/_app.tsx to pracht shell format (Shell export + children prop)getServerSideProps/getStaticProps to loader exportsexport const RENDER_MODE = "ssg" to static pages, "ssr" for dynamic (default is "ssr"). For time-revalidated pages, export RENDER_MODE = "isg" and a positive integer REVALIDATE in seconds. Webhook policies require ejection.generateRoutesFile to eject to explicit manifestFor pages router projects, you can skip manual manifest wiring entirely (Phase 7 below).
| Next.js | Pracht | Notes |
|---|---|---|
pages/ directory | pagesDir plugin option | Auto-discovers routes from file system |
app/page.tsx | src/routes/*.tsx + route() in manifest | File is a module; wiring is explicit |
app/layout.tsx | src/shells/*.tsx + shells in defineApp | Shells are named, not directory-nested |
app/loading.tsx | Loading export on the shell | Rendered as SSR placeholder for SPA routes until the client router takes over |
app/error.tsx | ErrorBoundary export in route module | Same concept, different wiring |
app/not-found.tsx | notFound: in defineApp (or pages/404.tsx in pagesDir mode) | Not a route — never matches a URL, so it cannot shadow static assets |
middleware.ts | src/middleware/*.ts + middleware in defineApp | Named, applied per route/group |
app/api/*/route.ts | src/api/*.ts with GET/POST exports | Auto-discovered, no manifest entry |
generateStaticParams | getStaticPaths() export | Returns RouteParams[] of param objects |
generateMetadata | head() export | Returns { title, meta } |
| Server Components | loader() export | Data fetching moves to loader; component is always a Preact component |
"use server" actions | API routes + <Form> / fetch | Mutations move to src/api/*; return Response objects |
"use client" (few, in a mostly-server app) | hydration: "islands" + src/islands/ | Only islands ship JS; see the islands note in Phase 4 |
revalidatePath / res.revalidate() | webhookRevalidate() + POST /__pracht/revalidate | On-demand ISG regeneration; combinable with timeRevalidate(seconds) |
useRouter() (next/navigation) | useNavigate() from pracht | Accepts paths or typed route targets after pracht typegen |
useSearchParams() | useSearchParams() from pracht | Returns reactive read-only params; SSG receives the browser query after hydration, while loaders use url.searchParams |
useParams() | useParams() from pracht | Direct equivalent; also available as params in loader args |
next/link <Link> | <Link route="..."> or plain <a> | Prefer typed <Link> for known app routes after pracht typegen; plain anchors still work |
next/link prefetch={false} | <Link prefetch="none"> | Pracht prefetches on hover/focus by default; also "viewport", "render" |
useLinkStatus() / pending UI | useNavigation() | { state, location, formData } — powers progress bars and optimistic UI |
next/image | <Image> from @pracht/image | Responsive srcsets plus Node, Cloudflare, Vercel, or passthrough loaders |
next/head or Metadata API | head() export on route/shell | Per-route and per-shell head merging |
next/script <Script> | <Script> from @pracht/core | Strategies: beforeHydration (≈ beforeInteractive), afterHydration (≈ afterInteractive, default), idle (≈ lazyOnload), visible |
className | class | Preact uses class attribute |
React.useState etc. | import { useState } from "preact/hooks" | Preact hooks API is compatible |
React.useEffect | import { useEffect } from "preact/hooks" | Same API |
import React from "react" | Remove — no import needed | Pracht's Vite plugin handles JSX automatically |
Initialize the pracht project structure:
src/
routes.ts # Route manifest
routes/ # Route modules
shells/ # Layout shells
middleware/ # Server-side middleware
api/ # API routes
Create vite.config.ts:
import { defineConfig } from "vite";
import { pracht } from "@pracht/vite-plugin";
export default defineConfig({
plugins: [pracht()],
});
Update package.json:
react, react-dom → preactnext → @pracht/core (framework runtime), @pracht/cli (provides the pracht bin), @pracht/vite-plugin, and @pracht/adapter-node (or target adapter). There is no package named pracht.next/image, add @pracht/image; add sharp only for the built-in Node optimization endpoint or build-time ?pracht imports (static imports / blur placeholders).dev → pracht dev, build → pracht build, start → node dist/server/server.js (Node.js) or a platform-specific deploy command; add preview → pracht preview to serve the production build locallyRemove Next.js config files: next.config.*, next-env.d.ts, .next/
If tsconfig.json has "jsx": "preserve", change to "jsx": "react-jsx" and add "jsxImportSource": "preact".
For each layout.tsx:
Next.js:
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body className="root">{children}</body>
</html>
);
}
Pracht:
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" };
}
Key differences:
<html>, <head>, or <body> — the framework owns the HTML document.class not className.defineApp({ shells: { main: "./shells/main.tsx" } }).For each page.tsx:
Next.js (Server Component with data):
async function getData() {
const res = await fetch("https://api.example.com/data");
return res.json();
}
export default async function Page() {
const data = await getData();
return <div className="page">{data.title}</div>;
}
export async function generateMetadata() {
const data = await getData();
return { title: data.title };
}
Pracht:
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>;
}
Key transforms:
loader() exportgenerateMetadata → head() exportexport default function Page as the page componentclassName → classasync components — data comes via props from loaderNext.js:
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Pracht:
import { useState } from "preact/hooks";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Key transforms:
"use client" directive — not needed in prachtimport { ... } from "react" → import { ... } from "preact/hooks" or import { ... } from "preact/compat"import { ... } from "react-dom" → import { ... } from "preact/compat"Islands note: if the source app is mostly server components with only a handful of "use client" components, don't 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 to src/islands/ — the rest of the page renders as inert HTML and only the islands ship JavaScript. See docs/ISLANDS.md.
Next.js (app/api/users/route.ts):
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const users = await getUsers();
return NextResponse.json(users);
}
Pracht (src/api/users.ts):
import type { ApiRouteArgs } from "@pracht/core";
export async function GET({ request }: ApiRouteArgs) {
const users = await getUsers();
return Response.json(users);
}
Key transforms:
NextRequest → standard Request (via ApiRouteArgs)NextResponse.json() → Response.json() (Web standard)app/api/users/[id]/route.ts → src/api/users/[id].tsNext.js (middleware.ts):
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const session = request.cookies.get("session");
if (!session) return NextResponse.redirect(new URL("/login", request.url));
return NextResponse.next();
}
export const config = { matcher: ["/dashboard/:path*"] };
Pracht (src/middleware/auth.ts):
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();
};
Then apply it in the manifest:
group({ middleware: ["auth"] }, [
route("/dashboard", () => import("./routes/dashboard.tsx"), { render: "ssr" }),
]);
Key transforms:
config.matcher to manifest group/route assignmentNextResponse.redirect() → return redirect("/path", { request })NextResponse.next() → return next()await next() and observe the response — useful for tracing.Note: For pages router projects using pagesDir, this phase is automatic. Skip to Phase 8.
Instead of hand-writing every entry, prefer pracht generate route --path ... --render ... (with --shell/--middleware/--loader as needed) per page: it creates a wired skeleton and updates src/routes.ts for you — then port the Next.js component/loader bodies into the generated files. Hand-write the manifest only for shapes the generator cannot express.
Build src/routes.ts mapping every migrated page. Module references accept () => import("./path") (enables IDE navigation) or plain "./path" strings — both work:
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("/about", () => import("./routes/about.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",
},
});
Choose render modes based on the Next.js original:
generateStaticParams) → "ssg"cookies(), headers(), per-request data) → "ssr"revalidate option) → "isg" with timeRevalidate(seconds)revalidatePath / res.revalidate()) → add webhookRevalidate() (alone or as [timeRevalidate(seconds), webhookRevalidate()]) and trigger via POST /__pracht/revalidate"spa"next/link → typed <Link> or plain <a>After manifest wiring is in place, run pracht typegen and prefer route-id based links for known app routes:
// Next.js
import Link from "next/link";
<Link href={`/products/${id}`}>Product</Link>
// Pracht
import { Link } from "@pracht/core";
<Link route="product" params={{ id }}>Product</Link>
Plain anchors still work for simple, external, or user-provided URLs because the client router intercepts same-origin <a> clicks:
<a href="/about">About</a>
<Link> also accepts navigation-behavior props: prefetch ("none" | "intent" | "viewport" | "render", default "intent" on hover/focus — the equivalent of next/link's prefetch tuning), preserveScroll (skip the scroll-to-top reset), and viewTransition (wrap the navigation in document.startViewTransition() where supported). Scroll restoration on back/forward works out of the box, like Next.js.
next/image → @pracht/image// Next.js
import Image from "next/image";
<Image src="/photo.jpg" width={500} height={300} alt="Photo" />
// Pracht
import { Image } from "@pracht/image";
<Image src="/photo.jpg" width={500} height={300} alt="Photo" />
Choose the loader for the deployment target:
createImageHandler() from @pracht/image/node, install
sharp, and set its localOrigin to the same trusted value as
nodeAdapter({ canonicalOrigin }).cloudflareLoader; do not bundle the Node
handler because sharp does not run in Workers.vercelLoader and keep Vercel's allowed image sizes
aligned with the Pracht breakpoints.passthroughLoader.Preserve the original width, height, fill, sizes, quality, and
priority intent. See docs/IMAGES.md for the endpoint and loader wiring.
Static imports and blur placeholders migrate too: replace
import photo from "./photo.jpg" with import photo from "./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. Pracht's
blur is CSS-only (no fade animation, no inline event handlers).
For apps that relied on next/image producing files during a static export,
use ?pracht&pracht-static instead. It emits cached responsive WebP variants
and bypasses the runtime loader while retaining plain, hydration-free <img>
markup. When Markdown content contains relative images, prefer
defineMarkdownCollection() from @pracht/markdown; it applies the same
static pipeline to normal  syntax. Keep root-relative
public/ and remote image URLs unchanged, and use an absolute Vite base for
static variants.
useRouter → navigation// Next.js
import { useRouter } from "next/navigation";
const router = useRouter();
router.push("/dashboard");
// Pracht
import { useNavigate } from "@pracht/core";
const navigate = useNavigate();
navigate("/dashboard");
// After `pracht typegen`, prefer route ids for known routes
navigate({ route: "dashboard" });
// Next.js
"use server";
async function createPost(formData: FormData) {
await db.insert({ title: formData.get("title") });
revalidatePath("/posts");
}
// Pracht — API route handler
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") });
// revalidatePath("/posts") equivalent: regenerate the ISG page on demand
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") },
});
}
For the revalidation call to take effect, the /posts route must be render: "isg" and opt in with revalidate: webhookRevalidate() (or [timeRevalidate(seconds), webhookRevalidate()]) in the manifest — import both from @pracht/core — and PRACHT_REVALIDATE_TOKEN must be set in the runtime environment. If /posts is a plain SSR route, skip the revalidation call; the redirect re-renders it fresh anyway.
cookies() / headers() → loader args// Next.js
import { cookies, headers } from "next/headers";
const session = cookies().get("session");
const ua = headers().get("user-agent");
// Pracht — available in loader args
export async function loader({ request }: LoaderArgs) {
const cookies = request.headers.get("cookie");
const ua = request.headers.get("user-agent");
return {
/* ... */
};
}
"use client" and "use server" directives.next/* imports (next/link, next/image, next/navigation, next/headers).className → replace with class.react imports → replace with preact equivalents.next.config.*, next-env.d.ts, .next/ directory.pracht typegen if route ids/paths changed or if you converted links/navigation to typed route ids.pracht dev) and fix any remaining issues.| Next.js package | Pracht equivalent |
|---|---|
next | @pracht/core + @pracht/cli + @pracht/vite-plugin + @pracht/adapter-node (or target adapter) |
next/image | @pracht/image |
react | preact |
react-dom | preact |
next/font/local | defineFont() from @pracht/core — register via head() { return { fonts: [font] } }, use font.className/font.style in components |
next/font/google | Download 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-auth | Direct integration in middleware/loaders |
next/og | @vercel/og or custom solution |
Many React libraries work with Preact via preact/compat. Add aliases in vite.config.ts if needed:
resolve: {
alias: {
"react": "preact/compat",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime",
}
}
Note: The pracht Vite plugin sets these aliases automatically. Only add manual aliases if a dependency doesn't resolve correctly.
preact/compat aliasing and flag them.pracht dev to verify. Fix errors iteratively.class not className, no React import needed, preact/hooks for hooks.$ARGUMENTS
Frequently asked questions
Systematically migrate a Next.js application (App Router or Pages Router) to pracht — a full-stack Preact framework built on Vite.
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.
Static rules flagged read-files, network in the source; the page lists the matching lines and excerpts.
Alternatives
fcakyon/claude-codex-settings
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
yonatangross/orchestkit
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.
yonatangross/orchestkit
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.
theBGuy/GitDesktop
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