create-pracht 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/skills/add-auth/SKILL.md +63 -143
  3. package/skills/add-capabilities/SKILL.md +409 -0
  4. package/skills/add-content/SKILL.md +242 -0
  5. package/skills/add-db/SKILL.md +93 -202
  6. package/skills/add-i18n/SKILL.md +178 -217
  7. package/skills/add-images/SKILL.md +203 -0
  8. package/skills/add-observability/SKILL.md +118 -15
  9. package/skills/add-openapi/SKILL.md +209 -0
  10. package/skills/audit-a11y/SKILL.md +8 -9
  11. package/skills/audit-agent-surface/SKILL.md +335 -0
  12. package/skills/audit-auth/SKILL.md +16 -11
  13. package/skills/audit-bundles/SKILL.md +56 -12
  14. package/skills/audit-csrf/SKILL.md +9 -10
  15. package/skills/audit-deps/SKILL.md +8 -8
  16. package/skills/audit-headers/SKILL.md +9 -10
  17. package/skills/audit-islands/SKILL.md +9 -10
  18. package/skills/audit-loaders/SKILL.md +23 -8
  19. package/skills/audit-redirects/SKILL.md +9 -10
  20. package/skills/audit-secrets/SKILL.md +6 -6
  21. package/skills/audit-seo/SKILL.md +8 -8
  22. package/skills/audit-shells/SKILL.md +8 -9
  23. package/skills/configure-isg/SKILL.md +9 -10
  24. package/skills/migrate-nextjs/SKILL.md +200 -415
  25. package/skills/pracht-debug/SKILL.md +165 -120
  26. package/skills/pracht-deploy/SKILL.md +248 -329
  27. package/skills/pracht-scaffold/SKILL.md +123 -146
  28. package/skills/pracht-test-api/SKILL.md +10 -10
  29. package/skills/pre-deploy/SKILL.md +166 -195
  30. package/skills/scaffold-e2e/SKILL.md +11 -12
  31. package/skills/scaffold-tests/SKILL.md +10 -12
  32. package/skills/tune-render-mode/SKILL.md +7 -8
  33. package/skills/typed-routes/SKILL.md +15 -11
  34. package/skills/upgrade-pracht/SKILL.md +12 -10
  35. package/src/index.js +43 -0
@@ -1,13 +1,12 @@
1
1
  ---
2
2
  name: migrate-nextjs
3
- version: 1.2.0
3
+ version: 1.3.0
4
4
  description: |
5
- Migrate a Next.js application to Pracht. Converts App Router pages, layouts,
6
- middleware, API routes, data fetching, and metadata to pracht equivalents.
7
- Handles React→Preact, className→class, server components→loaders, and
8
- manifest wiring.
9
- Use when asked to "migrate from next", "convert next.js app", "port from
10
- next to pracht", "nextjs migration", or "switch from next".
5
+ Migrate a Next.js app to pracht: App or Pages Router pages, layouts, middleware,
6
+ API routes, data fetching, and metadata — plus React→Preact, `className`→`class`,
7
+ server components→loaders, and manifest wiring.
8
+ Use for "migrate from next", "convert next.js app", "port from next to pracht",
9
+ "nextjs migration", "switch from next".
11
10
  allowed-tools:
12
11
  - Bash
13
12
  - Read
@@ -20,136 +19,105 @@ allowed-tools:
20
19
 
21
20
  # Migrate Next.js to Pracht
22
21
 
23
- Systematically migrate a Next.js application (App Router or Pages Router) to pracht — a full-stack Preact framework built on Vite.
24
-
25
- ## Step 0: Assess the source project
26
-
27
- Before touching any code, understand what you're migrating:
28
-
29
- 1. Read `next.config.js` / `next.config.mjs` / `next.config.ts` for custom config.
30
- 2. Read `package.json` for React/Next versions and dependencies.
31
- 3. Scan the directory structure:
32
- - `app/` → App Router (Next 13+)
33
- - `pages/` → Pages Router (legacy)
34
- - `middleware.ts` → edge middleware
35
- - `app/api/` or `pages/api/` → API routes
36
- 4. Identify rendering patterns in use:
37
- - `"use client"` directives → client components
38
- - `async` page/layout components → server components with data fetching
39
- - `generateStaticParams` → static generation
40
- - `generateMetadata` / `metadata` export → head management
41
- - Server Actions (`"use server"`) → mutations
42
- 5. Note third-party integrations (auth, CMS, DB, analytics).
43
-
44
- Ask the user to confirm the migration scope if the project is large (>20 routes).
45
-
46
- 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`.)
47
-
48
- ## Fast Path: Pages Router Projects
49
-
50
- If the source Next.js project uses the **pages router** (`pages/` directory), pracht's `pagesDir` plugin option provides a near-drop-in migration:
51
-
52
- 1. Set `pracht({ pagesDir: "/src/pages" })` in `vite.config.ts`
53
- 2. Copy `pages/` to `src/pages/`
54
- 3. Convert `_app.tsx` to pracht shell format (`Shell` export + `children` prop)
55
- 4. Convert `getServerSideProps`/`getStaticProps` to `loader` exports
56
- 5. Add `export 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.
57
- 6. Run dev server, iterate on errors
58
- 7. Optionally run `generateRoutesFile` to eject to explicit manifest
59
-
60
- For pages router projects, you can **skip manual manifest wiring entirely** (Phase 7 below).
61
-
62
- ## Concept Mapping
22
+ Migrate in phases: setup → shells → routes → client components → API →
23
+ middleware → manifest → patterns → cleanup. Read each Next.js source file
24
+ before converting it; never infer from the filename. Prefer the simplest
25
+ pracht equivalent, and when a Next.js feature has no equivalent, say so and
26
+ propose an alternative instead of inventing one.
27
+
28
+ MCP: when the pracht MCP server is registered (docs/MCP.md), use
29
+ `generate_route`/`generate_shell`/`generate_middleware`/`generate_api` to
30
+ scaffold and `inspect_routes`/`inspect_api`/`doctor`/`verify` to check
31
+ progress, instead of Bash. `pracht inspect` needs the pracht plugin in the
32
+ vite config; `inspect_build` needs a prior `pracht build`.
33
+
34
+ ## Step 0: Assess the source
35
+
36
+ Read `next.config.*` and `package.json` (React/Next versions, deps), then map
37
+ the tree: `app/` (App Router), `pages/` (Pages Router), `middleware.ts`,
38
+ `app/api/` or `pages/api/`. Note which patterns are in use — `"use client"`,
39
+ `async` server components, `generateStaticParams`, `generateMetadata`/`metadata`,
40
+ `"use server"` actions — and the third-party integrations (auth, CMS, DB,
41
+ analytics). Confirm scope with the user if the app has more than ~20 routes.
42
+
43
+ ## Fast path: Pages Router
44
+
45
+ `pagesDir` makes a pages-router source near-drop-in — **Phase 7 is then
46
+ automatic**:
47
+
48
+ 1. `pracht({ pagesDir: "/src/pages" })` in `vite.config.ts`; copy `pages/` to
49
+ `src/pages/`.
50
+ 2. `_app.tsx` → pracht shell shape (`Shell` export taking `children`).
51
+ 3. `getServerSideProps`/`getStaticProps` → `loader` export.
52
+ 4. `export const RENDER_MODE = "ssg"` on static pages (`"ssr"` is the
53
+ default). For time-revalidated pages export `RENDER_MODE = "isg"` plus a
54
+ positive integer `REVALIDATE` in seconds; webhook policies require ejecting
55
+ to a manifest.
56
+ 5. Run the dev server, iterate, and optionally eject later with
57
+ `generateRoutesFile`.
58
+
59
+ ## Concept mapping
63
60
 
64
61
  | Next.js | Pracht | Notes |
65
62
  | ------------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------- |
66
- | `pages/` directory | `pagesDir` plugin option | Auto-discovers routes from file system |
63
+ | `pages/` directory | `pagesDir` plugin option | Auto-discovers routes from the file system |
67
64
  | `app/page.tsx` | `src/routes/*.tsx` + `route()` in manifest | File is a module; wiring is explicit |
68
65
  | `app/layout.tsx` | `src/shells/*.tsx` + `shells` in `defineApp` | Shells are named, not directory-nested |
69
- | `app/loading.tsx` | `Loading` export on the shell | Rendered as SSR placeholder for SPA routes until the client router takes over |
66
+ | `app/loading.tsx` | `Loading` export on the shell | SSR placeholder for SPA routes until the client router takes over |
70
67
  | `app/error.tsx` | `ErrorBoundary` export in route module | Same concept, different wiring |
71
68
  | `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 |
72
69
  | `middleware.ts` | `src/middleware/*.ts` + `middleware` in `defineApp` | Named, applied per route/group |
73
70
  | `app/api/*/route.ts` | `src/api/*.ts` with `GET`/`POST` exports | Auto-discovered, no manifest entry |
74
71
  | `generateStaticParams` | `getStaticPaths()` export | Returns `RouteParams[]` of param objects |
75
72
  | `generateMetadata` | `head()` export | Returns `{ title, meta }` |
76
- | Server Components | `loader()` export | Data fetching moves to loader; component is always a Preact component |
73
+ | Server Components | `loader()` export | Data fetching moves to the loader; the component is a Preact component |
77
74
  | `"use server"` actions | API routes + `<Form>` / `fetch` | Mutations move to `src/api/*`; return `Response` objects |
78
- | `"use client"` (few, in a mostly-server app) | `hydration: "islands"` + `src/islands/` | Only islands ship JS; see the islands note in Phase 4 |
79
- | `revalidatePath` / `res.revalidate()` | `webhookRevalidate()` + `POST /__pracht/revalidate` | On-demand ISG regeneration; combinable with `timeRevalidate(seconds)` |
80
- | `useRouter()` (next/navigation) | `useNavigate()` from pracht | Accepts paths or typed route targets after `pracht typegen` |
81
- | `useSearchParams()` | `useSearchParams()` from pracht | Returns reactive read-only params; SSG receives the browser query after hydration, while loaders use `url.searchParams` |
82
- | `useParams()` | `useParams()` from pracht | Direct equivalent; also available as `params` in loader args |
83
- | `next/link` `<Link>` | `<Link route="...">` or plain `<a>` | Prefer typed `<Link>` for known app routes after `pracht typegen`; plain anchors still work |
84
- | `next/link` `prefetch={false}` | `<Link prefetch="none">` | Pracht prefetches on hover/focus by default; also `"viewport"`, `"render"` |
85
- | `useLinkStatus()` / pending UI | `useNavigation()` | `{ state, location, formData }` — powers progress bars and optimistic UI |
86
- | `next/image` | `<Image>` from `@pracht/image` | Responsive srcsets plus Node, Cloudflare, Vercel, or passthrough loaders |
75
+ | `"use client"` (few, mostly-server app) | `hydration: "islands"` + `src/islands/` | Only islands ship JS; see Phase 4 |
76
+ | `revalidatePath` / `res.revalidate()` | `webhookRevalidate()` + `POST /__pracht/revalidate` | On-demand ISG; combinable with `timeRevalidate(seconds)` |
77
+ | `useRouter()` (next/navigation) | `useNavigate()` | Takes a path, or `{ route: "id" }` after `pracht typegen` |
78
+ | `useSearchParams()` | `useSearchParams()` | Reactive read-only params; SSG gets the browser query after hydration, loaders use `url.searchParams` |
79
+ | `useParams()` | `useParams()` | Direct equivalent; also `params` in loader args |
80
+ | `next/link` `<Link>` | `<Link route="..." params={{…}}>` or plain `<a>` | Prefer typed `<Link>` after `pracht typegen`; the router intercepts same-origin anchors |
81
+ | `next/link` `prefetch={false}` | `<Link prefetch="none">` | Default `"intent"` (hover/focus); also `"viewport"`, `"render"` |
82
+ | `useLinkStatus()` / pending UI | `useNavigation()` | `{ state, location, formData }` — progress bars, optimistic UI |
83
+ | `next/image` | `<Image>` from `@pracht/image` | Responsive srcsets; Node, Cloudflare, Vercel, or passthrough loaders |
87
84
  | `next/head` or Metadata API | `head()` export on route/shell | Per-route and per-shell head merging |
88
- | `next/script` `<Script>` | `<Script>` from `@pracht/core` | Strategies: `beforeHydration` (≈ `beforeInteractive`), `afterHydration` (≈ `afterInteractive`, default), `idle` (≈ `lazyOnload`), `visible` |
89
- | `className` | `class` | Preact uses `class` attribute |
90
- | `React.useState` etc. | `import { useState } from "preact/hooks"` | Preact hooks API is compatible |
91
- | `React.useEffect` | `import { useEffect } from "preact/hooks"` | Same API |
92
- | `import React from "react"` | Remove — no import needed | Pracht's Vite plugin handles JSX automatically |
93
-
94
- ## Migration Procedure
95
-
96
- ### Phase 1: Project setup
97
-
98
- 1. Initialize the pracht project structure:
99
- ```
100
- src/
101
- routes.ts # Route manifest
102
- routes/ # Route modules
103
- shells/ # Layout shells
104
- middleware/ # Server-side middleware
105
- api/ # API routes
106
- ```
107
- 2. Create `vite.config.ts`:
108
-
109
- ```ts
110
- import { defineConfig } from "vite";
111
- import { pracht } from "@pracht/vite-plugin";
112
-
113
- export default defineConfig({
114
- plugins: [pracht()],
115
- });
116
- ```
117
-
118
- 3. Update `package.json`:
119
- - Replace `react`, `react-dom` → `preact`
120
- - Replace `next` → `@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`.
121
- - If the app imports `next/image`, add `@pracht/image`; add `sharp` only for the built-in Node optimization endpoint or build-time `?pracht` imports (static imports / blur placeholders).
122
- - Update scripts: `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 locally
123
- 4. Remove Next.js config files: `next.config.*`, `next-env.d.ts`, `.next/`
124
- 5. If `tsconfig.json` has `"jsx": "preserve"`, change to `"jsx": "react-jsx"` and add `"jsxImportSource": "preact"`.
125
-
126
- ### Phase 2: Convert layouts → shells
127
-
128
- For each `layout.tsx`:
129
-
130
- **Next.js:**
131
-
132
- ```tsx
133
- export default function RootLayout({ children }: { children: React.ReactNode }) {
134
- return (
135
- <html>
136
- <body className="root">{children}</body>
137
- </html>
138
- );
139
- }
140
- ```
141
-
142
- **Pracht:**
85
+ | `next/script` `<Script>` | `<Script>` from `@pracht/core` | `beforeHydration` (≈ `beforeInteractive`), `afterHydration` (≈ `afterInteractive`, default), `idle` (≈ `lazyOnload`), `visible` |
86
+ | `cookies()` / `headers()` | `request.headers` in loader/middleware/API args | No separate API — read the standard `Request` |
87
+ | `className` | `class` | Preact uses the `class` attribute |
88
+ | `react` / `react-dom` imports | `preact/hooks`, `preact/compat` | Same hook APIs |
89
+ | `import React from "react"` | Remove | The Vite plugin handles JSX |
90
+
91
+ Scroll restoration on back/forward works out of the box. `<Link>` also takes
92
+ `preserveScroll` (skip the scroll-to-top reset) and `viewTransition` (wrap the
93
+ navigation in `document.startViewTransition()` where supported).
94
+
95
+ ## Phase 1: Project setup
96
+
97
+ Create `src/routes.ts` (manifest), `src/routes/`, `src/shells/`,
98
+ `src/middleware/`, `src/api/`, and a `vite.config.ts` whose `plugins` array
99
+ contains `pracht()` from `@pracht/vite-plugin`. Then:
100
+
101
+ - Dependencies: drop `react`/`react-dom` for `preact`; drop `next` for
102
+ `@pracht/core` (runtime), `@pracht/cli` (the `pracht` bin),
103
+ `@pracht/vite-plugin`, and a target adapter such as `@pracht/adapter-node`.
104
+ There is no package named `pracht`. Add `@pracht/image` if the app used
105
+ `next/image`, and `sharp` only for the built-in Node optimization endpoint
106
+ or build-time `?pracht` imports.
107
+ - Scripts: `dev` → `pracht dev`, `build` → `pracht build`, `start` →
108
+ `node dist/server/server.js` (Node) or the platform deploy command; add
109
+ `preview` → `pracht preview`.
110
+ - Delete `next.config.*`, `next-env.d.ts`, `.next/`.
111
+ - In `tsconfig.json`, `"jsx": "preserve"` → `"jsx": "react-jsx"` with
112
+ `"jsxImportSource": "preact"`.
113
+
114
+ ## Phase 2: Layouts → shells
143
115
 
144
116
  ```tsx
145
117
  import type { ShellProps } from "@pracht/core";
146
118
 
147
119
  export function Shell({ children }: ShellProps) {
148
- return (
149
- <div class="root">
150
- <main>{children}</main>
151
- </div>
152
- );
120
+ return <div class="root"><main>{children}</main></div>;
153
121
  }
154
122
 
155
123
  export function head() {
@@ -157,36 +125,15 @@ export function head() {
157
125
  }
158
126
  ```
159
127
 
160
- Key differences:
161
-
162
- - Pracht shells do NOT render `<html>`, `<head>`, or `<body>` — the framework owns the HTML document.
163
- - Use `class` not `className`.
164
- - Register in `defineApp({ shells: { main: "./shells/main.tsx" } })`.
165
-
166
- ### Phase 3: Convert pages → route modules
167
-
168
- For each `page.tsx`:
169
-
170
- **Next.js (Server Component with data):**
171
-
172
- ```tsx
173
- async function getData() {
174
- const res = await fetch("https://api.example.com/data");
175
- return res.json();
176
- }
177
-
178
- export default async function Page() {
179
- const data = await getData();
180
- return <div className="page">{data.title}</div>;
181
- }
128
+ Shells must NOT render `<html>`, `<head>`, or `<body>` — the framework owns
129
+ the document, so move anything from `RootLayout`'s document tags into `head()`.
130
+ Register as `defineApp({ shells: { main: "./shells/main.tsx" } })`.
182
131
 
183
- export async function generateMetadata() {
184
- const data = await getData();
185
- return { title: data.title };
186
- }
187
- ```
132
+ ## Phase 3: Pages → route modules
188
133
 
189
- **Pracht:**
134
+ An `async` page that fetches and exports `generateMetadata` becomes three
135
+ exports — the fetch moves to `loader`, the metadata to `head`, the JSX stays
136
+ in the default export:
190
137
 
191
138
  ```tsx
192
139
  import type { LoaderArgs, RouteComponentProps } from "@pracht/core";
@@ -205,96 +152,40 @@ export default function Page({ data }: RouteComponentProps<typeof loader>) {
205
152
  }
206
153
  ```
207
154
 
208
- Key transforms:
209
-
210
- - Server-side data fetching → `loader()` export
211
- - `generateMetadata` → `head()` export
212
- - Keep `export default function Page` as the page component
213
- - `className` → `class`
214
- - No `async` components — data comes via props from loader
215
-
216
- ### Phase 4: Convert client components
217
-
218
- **Next.js:**
219
-
220
- ```tsx
221
- "use client";
222
- import { useState } from "react";
223
-
224
- export default function Counter() {
225
- const [count, setCount] = useState(0);
226
- return <button onClick={() => setCount(count + 1)}>{count}</button>;
227
- }
228
- ```
229
-
230
- **Pracht:**
231
-
232
- ```tsx
233
- import { useState } from "preact/hooks";
234
-
235
- export function Counter() {
236
- const [count, setCount] = useState(0);
237
- return <button onClick={() => setCount(count + 1)}>{count}</button>;
238
- }
239
- ```
240
-
241
- Key transforms:
155
+ Components are never `async` — data arrives as the `data` prop.
242
156
 
243
- - Remove `"use client"` directive — not needed in pracht
244
- - `import { ... } from "react"` → `import { ... } from "preact/hooks"` or `import { ... } from "preact/compat"`
245
- - `import { ... } from "react-dom"` → `import { ... } from "preact/compat"`
157
+ ## Phase 4: Client components
246
158
 
247
- **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`.
159
+ Drop the `"use client"` directive (pracht has no such concept), and repoint
160
+ imports: `react` → `preact/hooks` for hooks, `react`/`react-dom` →
161
+ `preact/compat` for everything else. The component body is otherwise unchanged.
248
162
 
249
- ### Phase 5: Convert API routes
163
+ **Islands:** if the source is mostly server components with a handful of
164
+ `"use client"` leaves, do not silently regress those pages to full-page
165
+ hydration. Set `hydration: "islands"` on the route (or
166
+ `export const HYDRATION = "islands"` in pages mode) and move the interactive
167
+ components into `src/islands/` — the rest renders as inert HTML. See
168
+ `docs/ISLANDS.md`.
250
169
 
251
- **Next.js (`app/api/users/route.ts`):**
252
-
253
- ```ts
254
- import { NextRequest, NextResponse } from "next/server";
255
-
256
- export async function GET(request: NextRequest) {
257
- const users = await getUsers();
258
- return NextResponse.json(users);
259
- }
260
- ```
170
+ ## Phase 5: API routes
261
171
 
262
- **Pracht (`src/api/users.ts`):**
172
+ `app/api/users/route.ts` → `src/api/users.ts`, dynamic segments included
173
+ (`app/api/users/[id]/route.ts` → `src/api/users/[id].ts`). Handlers take
174
+ `ApiRouteArgs` and use web standards throughout:
263
175
 
264
176
  ```ts
265
177
  import type { ApiRouteArgs } from "@pracht/core";
266
178
 
267
179
  export async function GET({ request }: ApiRouteArgs) {
268
- const users = await getUsers();
269
- return Response.json(users);
180
+ return Response.json(await getUsers());
270
181
  }
271
182
  ```
272
183
 
273
- Key transforms:
184
+ `NextRequest` → the standard `Request` from `ApiRouteArgs`;
185
+ `NextResponse.json()` → `Response.json()`. No manifest wiring — API routes are
186
+ auto-discovered.
274
187
 
275
- - `NextRequest` → standard `Request` (via `ApiRouteArgs`)
276
- - `NextResponse.json()` → `Response.json()` (Web standard)
277
- - Dynamic segments: `app/api/users/[id]/route.ts` → `src/api/users/[id].ts`
278
- - No manifest wiring needed — auto-discovered
279
-
280
- ### Phase 6: Convert middleware
281
-
282
- **Next.js (`middleware.ts`):**
283
-
284
- ```ts
285
- import { NextResponse } from "next/server";
286
- import type { NextRequest } from "next/server";
287
-
288
- export function middleware(request: NextRequest) {
289
- const session = request.cookies.get("session");
290
- if (!session) return NextResponse.redirect(new URL("/login", request.url));
291
- return NextResponse.next();
292
- }
293
-
294
- export const config = { matcher: ["/dashboard/:path*"] };
295
- ```
296
-
297
- **Pracht (`src/middleware/auth.ts`):**
188
+ ## Phase 6: Middleware
298
189
 
299
190
  ```ts
300
191
  import { redirect, type MiddlewareFn } from "@pracht/core";
@@ -306,44 +197,30 @@ export const middleware: MiddlewareFn = async ({ request }, next) => {
306
197
  };
307
198
  ```
308
199
 
309
- Then apply it in the manifest:
310
-
311
- ```ts
312
- group({ middleware: ["auth"] }, [
313
- route("/dashboard", () => import("./routes/dashboard.tsx"), { render: "ssr" }),
314
- ]);
315
- ```
316
-
317
- Key transforms:
318
-
319
- - Path matching moves from `config.matcher` to manifest group/route assignment
320
- - `NextResponse.redirect()` → `return redirect("/path", { request })`
321
- - `NextResponse.next()` → `return next()`
322
- - Pracht middleware is **wrap-around** (Hono/Koa/Astro shape), so you can
323
- also `await next()` and observe the response — useful for tracing.
200
+ `NextResponse.redirect()` → `return redirect("/path", { request })`;
201
+ `NextResponse.next()` → `return next()`. Path matching moves out of
202
+ `config.matcher` and into manifest assignment:
203
+ `group({ middleware: ["auth"] }, [route("/dashboard", …)])`. Pracht middleware
204
+ is wrap-around (Hono/Koa/Astro shape), so you can `await next()` and observe
205
+ the response — useful for tracing.
324
206
 
325
- ### Phase 7: Wire the route manifest
207
+ ## Phase 7: Route manifest
326
208
 
327
- **Note:** For pages router projects using `pagesDir`, this phase is automatic. Skip to Phase 8.
328
-
329
- 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.
330
-
331
- Build `src/routes.ts` mapping every migrated page. Module references accept `() => import("./path")` (enables IDE navigation) or plain `"./path"` strings — both work:
209
+ Skip this phase for `pagesDir` projects. Prefer
210
+ `pracht generate route --path ... --render ...` (plus `--shell`/`--middleware`/
211
+ `--loader`) per page — it creates a wired skeleton **and** updates
212
+ `src/routes.ts` — then port the Next.js bodies into the generated files.
213
+ Hand-write manifest entries only for shapes the generator cannot express.
332
214
 
333
215
  ```ts
334
216
  import { defineApp, group, route } from "@pracht/core";
335
217
 
336
218
  export const app = defineApp({
337
- shells: {
338
- main: () => import("./shells/main.tsx"),
339
- },
340
- middleware: {
341
- auth: () => import("./middleware/auth.ts"),
342
- },
219
+ shells: { main: () => import("./shells/main.tsx") },
220
+ middleware: { auth: () => import("./middleware/auth.ts") },
343
221
  routes: [
344
222
  group({ shell: "main" }, [
345
223
  route("/", () => import("./routes/home.tsx"), { render: "ssg" }),
346
- route("/about", () => import("./routes/about.tsx"), { render: "ssg" }),
347
224
  route("/dashboard", () => import("./routes/dashboard.tsx"), {
348
225
  render: "ssr",
349
226
  middleware: ["auth"],
@@ -351,113 +228,63 @@ export const app = defineApp({
351
228
  route("/blog/:slug", () => import("./routes/blog-post.tsx"), { render: "isg" }),
352
229
  ]),
353
230
  ],
354
- notFound: {
355
- component: () => import("./routes/not-found.tsx"),
356
- shell: "main",
357
- },
231
+ notFound: { component: () => import("./routes/not-found.tsx"), shell: "main" },
358
232
  });
359
233
  ```
360
234
 
361
- Choose render modes based on the Next.js original:
362
-
363
- - Static pages (no data fetching, or `generateStaticParams`) → `"ssg"`
364
- - Dynamic pages (`cookies()`, `headers()`, per-request data) → `"ssr"`
365
- - ISR pages (`revalidate` option) → `"isg"` with `timeRevalidate(seconds)`
366
- - On-demand ISR (`revalidatePath` / `res.revalidate()`) → add `webhookRevalidate()` (alone or as `[timeRevalidate(seconds), webhookRevalidate()]`) and trigger via `POST /__pracht/revalidate`
367
- - Client-only pages → `"spa"`
368
-
369
- ### Phase 8: Handle common patterns
370
-
371
- #### `next/link` → typed `<Link>` or plain `<a>`
372
-
373
- After manifest wiring is in place, run `pracht typegen` and prefer route-id based links for known app routes:
374
-
375
- ```tsx
376
- // Next.js
377
- import Link from "next/link";
378
- <Link href={`/products/${id}`}>Product</Link>
379
-
380
- // Pracht
381
- import { Link } from "@pracht/core";
382
- <Link route="product" params={{ id }}>Product</Link>
383
- ```
384
-
385
- Plain anchors still work for simple, external, or user-provided URLs because the client router intercepts same-origin `<a>` clicks:
386
-
387
- ```tsx
388
- <a href="/about">About</a>
389
- ```
390
-
391
- `<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.
392
-
393
- #### `next/image` → `@pracht/image`
394
-
395
- ```tsx
396
- // Next.js
397
- import Image from "next/image";
398
- <Image src="/photo.jpg" width={500} height={300} alt="Photo" />
399
-
400
- // Pracht
401
- import { Image } from "@pracht/image";
402
- <Image src="/photo.jpg" width={500} height={300} alt="Photo" />
403
- ```
404
-
405
- Choose the loader for the deployment target:
235
+ Module references accept `() => import("./path")` (better IDE navigation) or a
236
+ plain `"./path"` string. Pick render modes from the Next.js original:
237
+
238
+ | Next.js original | Render mode |
239
+ | --------------------------------------------- | -------------------------------------------------------- |
240
+ | No data fetching, or `generateStaticParams` | `"ssg"` |
241
+ | `cookies()`, `headers()`, per-request data | `"ssr"` |
242
+ | `revalidate` option | `"isg"` + `timeRevalidate(seconds)` |
243
+ | `revalidatePath` / `res.revalidate()` | `"isg"` + `webhookRevalidate()`, triggered by `POST /__pracht/revalidate` |
244
+ | Client-only | `"spa"` |
245
+
246
+ ## Phase 8: Remaining patterns
247
+
248
+ **Links and navigation.** After the manifest exists, run `pracht typegen` and
249
+ switch known app routes to route ids: `<Link route="product" params={{ id }}>`
250
+ and `navigate({ route: "dashboard" })`. Plain `<a href="/about">` and
251
+ `navigate("/dashboard")` keep working for simple, external, or user-provided
252
+ URLs.
253
+
254
+ **Images.** `<Image>` from `@pracht/image` takes the same
255
+ `width`/`height`/`fill`/`sizes`/`quality` and priority intent — preserve them.
256
+ Pick the loader for the deployment target:
257
+
258
+ | Target | Loader |
259
+ | ------------------ | -------------------------------------------------------------------------------- |
260
+ | Node | `createImageHandler()` from `@pracht/image/node` + `sharp`; set its `localOrigin` to the same trusted value as `nodeAdapter({ canonicalOrigin })` |
261
+ | Cloudflare Workers | `cloudflareLoader` — never bundle the Node handler, `sharp` does not run in Workers |
262
+ | Vercel Edge | `vercelLoader`, with Vercel's allowed image sizes aligned to the pracht breakpoints |
263
+ | Static hosts | `passthroughLoader` |
264
+
265
+ Static imports and blur placeholders migrate too: `import photo from
266
+ "./photo.jpg"` → `"./photo.jpg?pracht"`, add `prachtImage()` (from
267
+ `@pracht/image/vite`) to the Vite plugins, reference the `@pracht/image/client`
268
+ types once in a `.d.ts`, and keep `<Image src={photo} placeholder="blur" />`
269
+ as-is — the import supplies `width`/`height`/`blurDataURL` exactly like Next's
270
+ static imports, though pracht's blur is CSS-only (no fade, no inline handlers).
271
+ Where `next/image` produced files during a static export, use
272
+ `?pracht&pracht-static`: it emits cached responsive WebP variants and bypasses
273
+ the runtime loader while keeping plain hydration-free `<img>` markup. For
274
+ relative images in Markdown, `defineMarkdownCollection()` from
275
+ `@pracht/markdown` applies the same pipeline to `![alt](./photo.jpg)`. Leave
276
+ `public/` and remote URLs unchanged, and use an absolute Vite `base` for static
277
+ variants. See `docs/IMAGES.md`.
278
+
279
+ **Server Actions.** A `"use server"` mutation becomes an API route; the
280
+ `revalidatePath` half becomes an authenticated webhook call:
406
281
 
407
- - Node: mount `createImageHandler()` from `@pracht/image/node`, install
408
- `sharp`, and set its `localOrigin` to the same trusted value as
409
- `nodeAdapter({ canonicalOrigin })`.
410
- - Cloudflare Workers: configure `cloudflareLoader`; do not bundle the Node
411
- handler because `sharp` does not run in Workers.
412
- - Vercel Edge: configure `vercelLoader` and keep Vercel's allowed image sizes
413
- aligned with the Pracht breakpoints.
414
- - Static hosts: configure `passthroughLoader`.
415
-
416
- Preserve the original `width`, `height`, `fill`, `sizes`, `quality`, and
417
- priority intent. See `docs/IMAGES.md` for the endpoint and loader wiring.
418
-
419
- Static imports and blur placeholders migrate too: replace
420
- `import photo from "./photo.jpg"` with `import photo from "./photo.jpg?pracht"`,
421
- add `prachtImage()` (from `@pracht/image/vite`) to the Vite plugins, reference
422
- the `@pracht/image/client` types once in a `.d.ts`, and keep
423
- `<Image src={photo} placeholder="blur" />` as-is — the import supplies
424
- `width`/`height`/`blurDataURL` exactly like Next's static imports. Pracht's
425
- blur is CSS-only (no fade animation, no inline event handlers).
426
-
427
- #### `useRouter` → navigation
428
-
429
- ```tsx
430
- // Next.js
431
- import { useRouter } from "next/navigation";
432
- const router = useRouter();
433
- router.push("/dashboard");
434
-
435
- // Pracht
436
- import { useNavigate } from "@pracht/core";
437
- const navigate = useNavigate();
438
- navigate("/dashboard");
439
-
440
- // After `pracht typegen`, prefer route ids for known routes
441
- navigate({ route: "dashboard" });
442
- ```
443
-
444
- #### Server Actions → API routes
445
-
446
- ```tsx
447
- // Next.js
448
- "use server";
449
- async function createPost(formData: FormData) {
450
- await db.insert({ title: formData.get("title") });
451
- revalidatePath("/posts");
452
- }
453
-
454
- // Pracht — API route handler
282
+ ```ts
455
283
  import { withBase, type ApiRouteArgs } from "@pracht/core";
456
284
 
457
285
  export async function POST({ request }: ApiRouteArgs) {
458
286
  const form = await request.formData();
459
287
  await db.insert({ title: form.get("title") });
460
- // revalidatePath("/posts") equivalent: regenerate the ISG page on demand
461
288
  await fetch(new URL(withBase("/__pracht/revalidate"), request.url), {
462
289
  method: "POST",
463
290
  headers: {
@@ -466,81 +293,39 @@ export async function POST({ request }: ApiRouteArgs) {
466
293
  },
467
294
  body: JSON.stringify({ paths: ["/posts"] }),
468
295
  });
469
- return new Response(null, {
470
- status: 303,
471
- headers: { location: withBase("/posts") },
472
- });
296
+ return new Response(null, { status: 303, headers: { location: withBase("/posts") } });
473
297
  }
474
298
  ```
475
299
 
476
- 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.
300
+ That call only does something if `/posts` is `render: "isg"` and opts in with
301
+ `revalidate: webhookRevalidate()` (or `[timeRevalidate(seconds),
302
+ webhookRevalidate()]`, both imported from `@pracht/core`), and
303
+ `PRACHT_REVALIDATE_TOKEN` is set in the runtime environment. If `/posts` is a
304
+ plain SSR route, drop the call — the redirect re-renders it fresh.
477
305
 
478
- #### `cookies()` / `headers()` → loader args
306
+ ## Phase 9: Clean up and verify
479
307
 
480
- ```tsx
481
- // Next.js
482
- import { cookies, headers } from "next/headers";
483
- const session = cookies().get("session");
484
- const ua = headers().get("user-agent");
485
-
486
- // Pracht — available in loader args
487
- export async function loader({ request }: LoaderArgs) {
488
- const cookies = request.headers.get("cookie");
489
- const ua = request.headers.get("user-agent");
490
- return {
491
- /* ... */
492
- };
493
- }
494
- ```
495
-
496
- ### Phase 9: Clean up
497
-
498
- 1. Remove all `"use client"` and `"use server"` directives.
499
- 2. Remove all `next/*` imports (`next/link`, `next/image`, `next/navigation`, `next/headers`).
500
- 3. Search for remaining `className` → replace with `class`.
501
- 4. Search for remaining `react` imports → replace with `preact` equivalents.
502
- 5. Remove `next.config.*`, `next-env.d.ts`, `.next/` directory.
503
- 6. Run `pracht typegen` if route ids/paths changed or if you converted links/navigation to typed route ids.
504
- 7. Run the dev server (`pracht dev`) and fix any remaining issues.
308
+ Sweep for leftovers: `"use client"`/`"use server"` directives, `next/*`
309
+ imports, `className`, `react` imports, and `next.config.*`/`next-env.d.ts`/
310
+ `.next/`. Run `pracht typegen` if route ids or paths changed, then `pracht dev`
311
+ and fix errors iteratively.
505
312
 
506
- ## Dependency Mapping
313
+ ## Dependency mapping
507
314
 
508
315
  | Next.js package | Pracht equivalent |
509
316
  | --------------- | ---------------------------------------------------------------------------- |
510
- | `next` | `@pracht/core` + `@pracht/cli` + `@pracht/vite-plugin` + `@pracht/adapter-node` (or target adapter) |
511
- | `next/image` | `@pracht/image` |
512
- | `react` | `preact` |
513
- | `react-dom` | `preact` |
514
- | `next/font/local` | `defineFont()` from `@pracht/core` — register via `head() { return { fonts: [font] } }`, use `font.className`/`font.style` in components |
317
+ | `next` | `@pracht/core` + `@pracht/cli` + `@pracht/vite-plugin` + a target adapter |
318
+ | `next/image` | `@pracht/image` |
319
+ | `react`, `react-dom` | `preact` |
320
+ | `next/font/local` | `defineFont()` from `@pracht/core` — register via `head() { return { fonts: [font] } }`, use `font.className`/`font.style` |
515
321
  | `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 |
516
- | `@next/mdx` | `@mdx-js/rollup` (Vite plugin) |
517
- | `next-auth` | Direct integration in middleware/loaders |
518
- | `next/og` | `@vercel/og` or custom solution |
519
-
520
- ## React Library Compatibility
521
-
522
- Many React libraries work with Preact via `preact/compat`. Add aliases in `vite.config.ts` if needed:
523
-
524
- ```ts
525
- resolve: {
526
- alias: {
527
- "react": "preact/compat",
528
- "react-dom": "preact/compat",
529
- "react/jsx-runtime": "preact/jsx-runtime",
530
- }
531
- }
532
- ```
533
-
534
- Note: The pracht Vite plugin sets these aliases automatically. Only add manual aliases if a dependency doesn't resolve correctly.
535
-
536
- ## Rules
537
-
538
- 1. Always read the Next.js source before converting — understand what each file does.
539
- 2. Migrate in phases: setup → shells → routes → API → middleware → manifest → cleanup.
540
- 3. Prefer the simplest pracht equivalent. Don't over-engineer the migration.
541
- 4. Identify React libraries that need `preact/compat` aliasing and flag them.
542
- 5. After migration, run `pracht dev` to verify. Fix errors iteratively.
543
- 6. If a Next.js feature has no pracht equivalent, explain the gap and suggest alternatives.
544
- 7. Use Preact idioms: `class` not `className`, no `React` import needed, `preact/hooks` for hooks.
322
+ | `@next/mdx` | `@mdx-js/rollup` (Vite plugin) |
323
+ | `next-auth` | Direct integration in middleware/loaders |
324
+ | `next/og` | `@vercel/og` or a custom solution |
325
+
326
+ Most React libraries work through `preact/compat`, and the pracht Vite plugin
327
+ already aliases `react`/`react-dom`/`react/jsx-runtime` for you. Add manual
328
+ `resolve.alias` entries only when a dependency still fails to resolve — and
329
+ flag those libraries to the user.
545
330
 
546
331
  $ARGUMENTS