---
name: scaffold-nextjs
description: "Scaffolds a Next.js turborepo with Blode UI, icons, Ultracite (oxlint/shadcn), workspace hooks, and GitHub/Vercel setup. Use when asked to \"create a Next.js project\", \"bootstrap a turborepo\", or \"start a new web app\". For a page in an existing app use ui-design; for a CLI use scaffold-cli."
compatibility: Requires a shell, Git, Node.js, pnpm, and package registry access.
---
# Scaffold Next.js
Scaffold a Next.js turborepo with full tooling, GitHub, and Vercel deployment.
- **IS:** bootstrapping a brand-new Next.js turborepo end to end: app creation, Blode UI, Ultracite with `ultracite/oxlint/shadcn`, turborepo conversion, GitHub, and Vercel.
- **IS NOT:** scaffolding a TypeScript CLI or npm package (use `scaffold-cli`), designing folder structure or module contracts for an existing app (use `codebase-architecture`), building a page inside an existing app, or choosing visual direction and palettes (use `ui-design`).
The references encode the house stack and dependency order. Verify version-sensitive flags against the installed CLI and bundled documentation; update a proven incompatible template rather than forcing stale flags. Where a Next.js question comes up that the references do not answer, read the bundled docs at `node_modules/next/dist/docs/` in the app (they match the installed version) rather than training data.
## Reference Files
| File | Read When |
|------|-----------|
| `references/app-setup.md` | Phase 2: create-next-app flags, TypeScript 7 upgrade, Instant Navigations, shadcn + Blode registry, icons, Agentation, Ultracite 7.12+ with `ultracite/oxlint/shadcn`, move into apps/web/ |
| `references/turbo-configs.md` | Phase 6: root package.json, turbo.json, root lefthook.yml, .gitignore, knip.json, workspace scripts, next.config.ts, root and app AGENTS.md |
| `references/deploy-and-launch.md` | Phase 7 and 8: GitHub, Vercel, CI workflow, metadataBase, verification, security.txt, favicon, OG image, validation checklist |
## Scaffold Workflow
Copy this checklist to track progress:
```text
Scaffold progress:
- [ ] Phase 1: Gather project info
- [ ] Phase 2: Create Next.js app
- [ ] Phase 2.1: Upgrade to TypeScript 7
- [ ] Phase 2.2: Turn on Instant Navigations
- [ ] Phase 3: Install Blode UI components and icons
- [ ] Phase 4: Install Agentation
- [ ] Phase 5: Install Ultracite
- [ ] Phase 5.1: Enable ultracite/oxlint/shadcn
- [ ] Phase 6: Convert to Turborepo
- [ ] Phase 7: GitHub and Vercel setup
- [ ] Phase 8: Pre-launch checklist
- [ ] Validation: run the checklist in deploy-and-launch.md
```
### Phase 1: Gather project info
Collect from the user (ask only for what is missing):
| Variable | Example | Default | Used in |
|----------|---------|---------|---------|
| `{{name}}` | `acme-web` | none (required) | Root package.json, directory name, README |
| `{{description}}` | `Marketing site for Acme` | none (required) | App package.json, README |
| `{{repo}}` | `acme-corp/acme-web` | none (required) | GitHub remote URL |
| `{{domain}}` | `acme.com` | none (ask if missing) | Vercel custom domain, metadataBase |
| `{{author}}` | `Your Name` | none (required) | package.json author |
| `{{year}}` | `2026` | current year | LICENSE |
### Phase 2: Create Next.js app
Run the create-next-app command from `references/app-setup.md` exactly as written (it pins linter, React Compiler, and package-manager flags). Confirm the app loads on the port reported by the server. Use a free task-owned port when 3000 is occupied.
### Phase 2.1: Upgrade to TypeScript 7
TypeScript 7 section of `references/app-setup.md`: install `typescript@^7` and confirm `npm run build` type-checks through `tsc`. No config accompanies it.
### Phase 2.2: Turn on Instant Navigations
Instant Navigations section of `references/app-setup.md`: set `cacheComponents`, `partialPrefetching`, and `experimental.turbopackRustReactCompiler` in `next.config.ts`. Cheap here and expensive later, so do it before any route exists. Read the authoring rules in that section before Phase 3; they govern how every page is written.
### Phase 3: Install Blode UI components and icons
Blode UI section of `references/app-setup.md`: `shadcn init`, register the `@blode` namespace, set `iconLibrary` in `components.json`, install `blode-icons-react`, then add components.
### Phase 4: Install Agentation
Agentation section of `references/app-setup.md`: install the package, patch `app/layout.tsx` with the dev-only `` guard. Optionally add Google Analytics via `@next/third-parties`.
### Phase 5: Install Ultracite
Ultracite section of `references/app-setup.md`: run `ultracite@latest init` with the exact flags listed, including `--js-plugins @shadcn/lint` (Ultracite ≥ 7.12). Verify with `npx ultracite fix` and `npx ultracite check`. The `lefthook.yml` it writes is temporary; Phase 6 replaces it with a root-level one.
### Phase 5.1: Enable ultracite/oxlint/shadcn
`ultracite/oxlint/shadcn` section of `references/app-setup.md`: confirm init wrote `import shadcn from "ultracite/oxlint/shadcn"` into `extends` alongside core/next/react. If it did not, add that import (and `jsPlugins: shadcn.jsPlugins`). Do not hand-roll `jsPlugins: ["@shadcn/lint"]` or a starter-only `no-restyle` rule. The preset already turns `shadcn/no-restyle` off inside `**/components/ui/**`; add a matching override only when `aliases.ui` is a different path.
### Phase 6: Convert to Turborepo
Move the app into `apps/web/` (commands at the end of `references/app-setup.md`), then from `references/turbo-configs.md`:
1. Generate root `package.json`, `turbo.json`, `lefthook.yml`, `knip.json`, and `.gitignore` from the templates. Delete `apps/web/lefthook.yml`; git only reads the copy next to `.git`.
2. Update `apps/web/package.json` scripts to the turbo-compatible block and remove its `prepare` script (the root one installs the hooks).
3. Verify `apps/web/next.config.ts` still has `reactCompiler: true`, `cacheComponents: true`, and `partialPrefetching: true`.
4. Write the root `AGENTS.md` from the template. Keep the Phase 5.1 design-system lint paragraph in `apps/web/AGENTS.md` outside the Next-managed markers, including `ultracite fix` / `ultracite fix --codex`.
5. Run `npm install` from the root, then `npm run dev` once from the coding agent's shell. When Next 16.3 detects a coding agent in the environment it appends its managed `nextjs-agent-rules` block to `apps/web/AGENTS.md` (some generators also create a CLAUDE.md wrapper). Commit AGENTS.md and remove any duplicate CLAUDE.md wrapper. From a plain terminal nothing is written; that is fine, the block arrives on the agent's first run.
6. Verify `npm run check`, `npm run build`, and `npx lefthook run pre-commit --all-files` pass from the root, then `npm run start -w web` and load the home page from the production build.
### Phase 7: GitHub and Vercel setup
From `references/deploy-and-launch.md`: create the GitHub repo with `gh`, deploy to Vercel, attach `{{domain}}`.
### Phase 8: Pre-launch checklist
From `references/deploy-and-launch.md`: add the CI workflow, set `metadataBase` to `https://{{domain}}`, register the site with Search Console and Bing, add `security.txt`, the favicon package, and the OG image, then run the validation checklist at the end of that file. Done only when every validation item passes; "the site loads" is not sufficient evidence.
## Placeholder Reference
Templates use `{{variable}}` syntax. Before Phase 7, sweep for missed placeholders:
```bash
grep -rn '{{' --include='*.json' --include='*.ts' --include='*.tsx' --include='*.md' --include='*.yml' .
```
A `{{name}}` left in `package.json` fails `npm install` (invalid-name error); a `{{domain}}` left in metadata ships broken OG URLs. Two placeholders in the root `package.json` template are not gathered in Phase 1: `{{ultracite_version}}` is copied from the `ultracite` entry that `ultracite init` wrote into `apps/web/package.json`, and `{{npm_version}}` is the output of `npm --version`.
## Gotchas
- No `src/` directory. The scaffold uses `--no-src-dir`; adding `src/` later breaks the `@/*` alias and every shadcn component path.
- Never set `experimental.useTypeScriptCli`. Since 16.3 the CLI checker is the default, and the flag exists only to switch it back off with `false`; setting it to `true` is noise that reads like a requirement.
- Expect raw `tsc` diagnostics from the CLI checker: no Next.js code frames, and the full `tsconfig.json` project is checked (tests and `.next/dev/types` included), so a type error in a file `next build` used to skip now blocks the build. If you add `node --test` files later, either keep them type-clean or add `**/*.test.ts` to `tsconfig.json` `exclude`.
- A green `next build` does not mean navigation is instant. Instant navigation validation runs in development only (`validationLevel: 'warning'`) and never fails the build, so validate in `next dev` and read the overlay.
- With `cacheComponents: true`, any route segment that exports `dynamic`, `dynamicParams`, `revalidate`, or `fetchCache` fails the build; `runtime`, `maxDuration`, `instant`, and `prefetch` remain valid. That includes route handlers such as a hand-written `robots.txt/route.ts`. Put the data access in a separate `'use cache'` function with `cacheLife`, called from the page or the `GET`; the directive cannot sit on the `GET` export itself.
- `'use cache'` is in-memory per instance on serverless hosts, so on Vercel a cached value computed in one function invocation is not seen by the next. The docs' answer is `'use cache: remote'` for anything that must be shared; use it for the data behind the sitemap and any list page, and keep plain `'use cache'` for values that are cheap to recompute.
- `generateStaticParams` must return at least one param under Cache Components; an empty array raises `empty-generate-static-params`. Unlisted params get the App Shell on first visit and upgrade in the background.
- Cache Components keep the previous route's DOM mounted (React ``), so a background or theme hung off `body` or `html`, including a `body:has(.marker)` rule, leaks onto the next route. Own backgrounds per route, and key any theme switch off `usePathname()` in React rather than a class on `body`. Dropdowns and form state also survive navigation; clean them up in an effect or derive them from the URL.
- Never add `output: "standalone"`. It is for self-hosting, and on Vercel it stops `.next/next-server.js.nft.json` being written, so the build compiles every page and then dies in Vercel's onBuildComplete.
- Never set `runtime = "edge"`; it is deprecated in 16 and Cache Components requires Node.js. For work that must outlive the response (analytics, logging), use `after()` from `next/server` rather than a floating promise, which Node can cut off the moment the response goes out.
- Add no Turbopack cache config. `turbopackFileSystemCacheForDev`, `turbopackFileSystemCacheForBuild`, and memory eviction (`'auto'`) are on by default in 16.3.
- `turbopack.root` is not needed here. Turbopack infers the workspace root from the lockfile; set it only when linked packages live outside the repo.
- `next dev` appends a managed `` block to the `AGENTS.md` next to the `next` package (so `apps/web/`, not the root), and writes `CLAUDE.md` as `@AGENTS.md` only when neither file exists. It runs only when a coding agent is detected in the environment (`next/dist/server/lib/generate-agent-files.js`), so a plain terminal never triggers it. Keep AGENTS.md, remove any generated CLAUDE.md wrapper, and keep project instructions outside the markers.
- `create-next-app --react-compiler` installs `babel-plugin-react-compiler` as a devDependency. With `experimental.turbopackRustReactCompiler` on it is unused; remove it after Phase 2.2 so nobody reads it as a requirement.
- `ultracite init --skip-install` writes `check` and `fix` scripts, sets `"type": "module"`, and adds `oxlint`, `oxfmt`, `lefthook`, and `@shadcn/lint` (the last from `--js-plugins`). It writes no `prepare` script (that happens in the install step it skipped). Pin those tools to the versions the first `npm install` resolves before committing, and let the root `prepare` own hook installation. Confirm `ultracite` is ≥ 7.12 (`npm ls ultracite --depth=0`); older CLIs reject `--js-plugins @shadcn/lint` or skip the preset. JS plugins need Oxlint ≥ 1.80 and Node ≥ 20.19; if the plugin fails to load, bump `oxlint` rather than dropping `shadcn` from `extends`.
- No ESLint or Prettier. Ultracite owns lint and format via Oxlint + Oxfmt; a stray `.eslintrc` makes the editor disagree with the lefthook pre-commit hook. Pass `--js-plugins @shadcn/lint` to `ultracite init` (Ultracite ≥ 7.12). That is how 7.12 registers `ultracite/oxlint/shadcn`. Do not replace that preset with `jsPlugins: ["@shadcn/lint"]` plus a starter-only `shadcn/no-restyle` block.
- Keep Ultracite's `extends` (`core`, `next`, `react`) and `ignorePatterns`, and add `shadcn`. Replacing `oxlint.config.ts` with a README `.oxlintrc.json` example drops the framework presets. Keep `jsPlugins: shadcn.jsPlugins` on the root config: Oxlint already loads the plugin from the preset, but Knip only reads `jsPlugins` off the root and otherwise flags `@shadcn/lint` as unused.
- The preset turns `shadcn/no-restyle`, `no-arbitrary-values`, and `require-static-classes` off inside `**/components/ui/**`. Do not duplicate that override unless `components.json` `aliases.ui` points elsewhere; then add a matching override for that path or the plugin reports definition files for styling themselves.
- Run lint and format through the workspace scripts: root `npm run check` / `npm run fix` (turbo runs them inside `apps/web`), or `npx ultracite check` from `apps/web`. Running `ultracite`, `oxlint`, or `oxfmt` from the repo root finds no `oxlint.config.ts` there and lints with defaults, which disagrees with the hook and skips the shadcn preset. Remaining design-system findings after `ultracite fix` can go to `npx ultracite fix --codex` (or `--claude`) from `apps/web`.
- No manual git hooks. Lefthook owns them; husky or another hook manager double-runs or skips fixes.
- `lefthook.yml` lives at the repo root, next to `.git`. A copy inside `apps/web/` is read only when lefthook is invoked from that directory, which the git hook never does. The root file scopes each job with `root: "apps/web/"` so staged paths are passed relative to the workspace, where `oxlint.config.ts` and `oxfmt.config.ts` live.
- The hook runs `oxfmt` and `oxlint` as two jobs with their own globs, not `ultracite fix`. Ultracite exits non-zero when the staged set contains no lintable JS/TS file, so a CSS-only or Markdown-only commit fails the hook outright; two jobs let lefthook skip whichever has nothing to do. The `oxfmt` glob includes `md` and `mdx` so it inspects what `format:check` inspects.
- No app dependencies in the root `package.json` (root holds only `turbo`, `ultracite`, and `lefthook`); they break workspace isolation and turbo cache keys. `@shadcn/lint` stays in `apps/web` with `oxlint.config.ts`. Pin the same `ultracite` version (≥ 7.12) at the root and in `apps/web` so config resolution cannot drift.
- Never run `npx shadcn@latest add @blode/...` before `npx shadcn@latest registry add @blode=...`; the unregistered namespace makes the add fail.
- Never import from `lucide-react`; `blode-icons-react` is Blode UI's icon library and mixed imports bundle two icon sets. `shadcn init` writes `"iconLibrary": "lucide"` into `components.json`; change it to `blode-icons-react` before adding components, and replace any generated `lucide-react` import paths.
- Never create `apps/web/` by hand. Scaffold at the root first, then move it in Phase 6; hand-building skips create-next-app defaults (Tailwind wiring, alias config).
- `next-env.d.ts` is generated and belongs in `.gitignore` (create-next-app already lists it). Do not commit it or edit it; custom declarations go in a separate `.d.ts` referenced from `tsconfig.json`.
- Next.js loads `.env.local` from the app directory (`apps/web/`), not the turborepo root. `vercel env pull apps/web/.env.local` is the pull command, and only `NEXT_PUBLIC_` variables reach the browser, inlined at build time.
- `node --test` runs a test file directly, where the `@/` alias does not resolve; test files and the modules they import use relative paths, and a test script globs `lib/**/*.test.ts` rather than naming one file, or a new test is never executed while the gate reports green.
- The root `.gitignore` ignores `.claude/` but un-ignores `.claude/knowledge/` (and `apps/web/.claude/`). Knowledge files are the memory these skills mine; experiment output is what the ignore is for.
- Check the Vercel Root Directory before dashboard deploys. On a 404 or wrong app, set Root Directory to `apps/web` in Settings > General.
## Skill Handoffs
| When | Run |
|------|-----|
| After deployment, optimise SEO | `seo` |
| Before launch, audit UI quality | `ui-design` (Audit mode) |
| Before launch, add motion and animation | `ui-animation` |
Maintenance only: `evals/evals.json` contains regression scenarios for changes to this skill; it does not load during a user task.