scaffold-nextjs
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 sca
Install
npx skills add https://github.com/mblode/agent-skills/tree/main/skills/scaffold-nextjs
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install mblode-agent-skills@llmmart
git clone https://github.com/mblode/agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole mblode/agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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 (usecodebase-architecture), building a page inside an existing app, or choosing visual direction and palettes (useui-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:
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 <Agentation /> 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:
- Generate root
package.json,turbo.json,lefthook.yml,knip.json, and.gitignorefrom the templates. Deleteapps/web/lefthook.yml; git only reads the copy next to.git. - Update
apps/web/package.jsonscripts to the turbo-compatible block and remove itspreparescript (the root one installs the hooks). - Verify
apps/web/next.config.tsstill hasreactCompiler: true,cacheComponents: true, andpartialPrefetching: true. - Write the root
AGENTS.mdfrom the template. Keep the Phase 5.1 design-system lint paragraph inapps/web/AGENTS.mdoutside the Next-managed markers, includingultracite fix/ultracite fix --codex. - Run
npm installfrom the root, thennpm run devonce from the coding agent's shell. When Next 16.3 detects a coding agent in the environment it appends its managednextjs-agent-rulesblock toapps/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. - Verify
npm run check,npm run build, andnpx lefthook run pre-commit --all-filespass from the root, thennpm run start -w weband 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:
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; addingsrc/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 withfalse; setting it totrueis noise that reads like a requirement. - Expect raw
tscdiagnostics from the CLI checker: no Next.js code frames, and the fulltsconfig.jsonproject is checked (tests and.next/dev/typesincluded), so a type error in a filenext buildused to skip now blocks the build. If you addnode --testfiles later, either keep them type-clean or add**/*.test.tstotsconfig.jsonexclude. - A green
next builddoes not mean navigation is instant. Instant navigation validation runs in development only (validationLevel: 'warning') and never fails the build, so validate innext devand read the overlay. - With
cacheComponents: true, any route segment that exportsdynamic,dynamicParams,revalidate, orfetchCachefails the build;runtime,maxDuration,instant, andprefetchremain valid. That includes route handlers such as a hand-writtenrobots.txt/route.ts. Put the data access in a separate'use cache'function withcacheLife, called from the page or theGET; the directive cannot sit on theGETexport 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.generateStaticParamsmust return at least one param under Cache Components; an empty array raisesempty-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
<Activity>), so a background or theme hung offbodyorhtml, including abody:has(.marker)rule, leaks onto the next route. Own backgrounds per route, and key any theme switch offusePathname()in React rather than a class onbody. 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.jsonbeing 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), useafter()fromnext/serverrather 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.rootis not needed here. Turbopack infers the workspace root from the lockfile; set it only when linked packages live outside the repo.next devappends a managed<!-- BEGIN:nextjs-agent-rules -->block to theAGENTS.mdnext to thenextpackage (soapps/web/, not the root), and writesCLAUDE.mdas@AGENTS.mdonly 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-compilerinstallsbabel-plugin-react-compileras a devDependency. Withexperimental.turbopackRustReactCompileron it is unused; remove it after Phase 2.2 so nobody reads it as a requirement.ultracite init --skip-installwritescheckandfixscripts, sets"type": "module", and addsoxlint,oxfmt,lefthook, and@shadcn/lint(the last from--js-plugins). It writes nopreparescript (that happens in the install step it skipped). Pin those tools to the versions the firstnpm installresolves before committing, and let the rootprepareown hook installation. Confirmultraciteis ≥ 7.12 (npm ls ultracite --depth=0); older CLIs reject--js-plugins @shadcn/lintor skip the preset. JS plugins need Oxlint ≥ 1.80 and Node ≥ 20.19; if the plugin fails to load, bumpoxlintrather than droppingshadcnfromextends.- No ESLint or Prettier. Ultracite owns lint and format via Oxlint + Oxfmt; a stray
.eslintrcmakes the editor disagree with the lefthook pre-commit hook. Pass--js-plugins @shadcn/linttoultracite init(Ultracite ≥ 7.12). That is how 7.12 registersultracite/oxlint/shadcn. Do not replace that preset withjsPlugins: ["@shadcn/lint"]plus a starter-onlyshadcn/no-restyleblock. - Keep Ultracite's
extends(core,next,react) andignorePatterns, and addshadcn. Replacingoxlint.config.tswith a README.oxlintrc.jsonexample drops the framework presets. KeepjsPlugins: shadcn.jsPluginson the root config: Oxlint already loads the plugin from the preset, but Knip only readsjsPluginsoff the root and otherwise flags@shadcn/lintas unused. - The preset turns
shadcn/no-restyle,no-arbitrary-values, andrequire-static-classesoff inside**/components/ui/**. Do not duplicate that override unlesscomponents.jsonaliases.uipoints 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 insideapps/web), ornpx ultracite checkfromapps/web. Runningultracite,oxlint, oroxfmtfrom the repo root finds nooxlint.config.tsthere and lints with defaults, which disagrees with the hook and skips the shadcn preset. Remaining design-system findings afterultracite fixcan go tonpx ultracite fix --codex(or--claude) fromapps/web. - No manual git hooks. Lefthook owns them; husky or another hook manager double-runs or skips fixes.
lefthook.ymllives at the repo root, next to.git. A copy insideapps/web/is read only when lefthook is invoked from that directory, which the git hook never does. The root file scopes each job withroot: "apps/web/"so staged paths are passed relative to the workspace, whereoxlint.config.tsandoxfmt.config.tslive.- The hook runs
oxfmtandoxlintas two jobs with their own globs, notultracite 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. Theoxfmtglob includesmdandmdxso it inspects whatformat:checkinspects. - No app dependencies in the root
package.json(root holds onlyturbo,ultracite, andlefthook); they break workspace isolation and turbo cache keys.@shadcn/lintstays inapps/webwithoxlint.config.ts. Pin the sameultraciteversion (≥ 7.12) at the root and inapps/webso config resolution cannot drift. - Never run
npx shadcn@latest add @blode/...beforenpx shadcn@latest registry add @blode=...; the unregistered namespace makes the add fail. - Never import from
lucide-react;blode-icons-reactis Blode UI's icon library and mixed imports bundle two icon sets.shadcn initwrites"iconLibrary": "lucide"intocomponents.json; change it toblode-icons-reactbefore adding components, and replace any generatedlucide-reactimport 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.tsis 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.tsreferenced fromtsconfig.json.- Next.js loads
.env.localfrom the app directory (apps/web/), not the turborepo root.vercel env pull apps/web/.env.localis the pull command, and onlyNEXT_PUBLIC_variables reach the browser, inlined at build time. node --testruns a test file directly, where the@/alias does not resolve; test files and the modules they import use relative paths, and a test script globslib/**/*.test.tsrather than naming one file, or a new test is never executed while the gate reports green.- The root
.gitignoreignores.claude/but un-ignores.claude/knowledge/(andapps/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/webin 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.
Files (agent-skills)
-
evals
-
evals.json 2.3 KB
{ "skill_name": "scaffold-nextjs", "evals": [ { "id": 1, "prompt": "Scaffold a Next.js turborepo with Blode UI. Port 3000 belongs to an unrelated local project.", "expected_output": "Use the house stack on a free port with correct workspace ownership.", "files": [], "assertions": [ "Preserves the unrelated server", "Places app dependencies in apps/web", "Keeps the hook configuration at the git root" ] }, { "id": 2, "prompt": "The installed Next.js rejects a flag in the scaffold reference. The current bundled documentation names its replacement.", "expected_output": "Adapt the template using version-matched documentation.", "files": [], "assertions": [ "Does not force the rejected flag", "Records the compatibility correction", "Verifies the resulting build" ] }, { "id": 3, "prompt": "Scaffold a Next.js turborepo with Blode UI and wire the house lint stack.", "expected_output": "Ultracite 7.12+ Oxlint with ultracite/oxlint/shadcn after init, not a hand-rolled starter-only no-restyle snippet.", "files": [], "assertions": [ "Passes --js-plugins @shadcn/lint to ultracite init, or adds import shadcn from ultracite/oxlint/shadcn after init", "Requires ultracite 7.12 or newer", "Keeps ultracite/oxlint core, next, and react extends and adds shadcn", "Does not register jsPlugins as [\"@shadcn/lint\"] with a starter-only no-restyle rule block", "Relies on the preset turning shadcn/no-restyle off for **/components/ui/**, or adds a matching override only when aliases.ui differs", "Documents ultracite fix and ultracite fix --codex for remaining design-system findings" ] } ], "routing": { "should_trigger": [ "Scaffold a Next.js turborepo with Blode UI. Port 3000 belongs to an unrelated local project.", "The installed Next.js rejects a flag in the scaffold reference. The current bundled documentation names its replacement.", "Scaffold a Next.js turborepo with Blode UI and wire the house lint stack." ], "near_miss": [ { "prompt": "Build a pricing page inside the existing Next.js app.", "expected": "ui-design" } ] } }
-
-
references
-
app-setup.md 18.9 KB
# App Setup Commands ## Contents - [Phase 2: Create Next.js app](#phase-2-create-nextjs-app) - [Phase 2.1: Upgrade to TypeScript 7](#phase-21-upgrade-to-typescript-7) - [Phase 2.2: Turn on Instant Navigations](#phase-22-turn-on-instant-navigations) - [Phase 3: Install Blode UI components and icons](#phase-3-install-blode-ui-components-and-icons) - [Phase 4: Install Agentation](#phase-4-install-agentation) - [Phase 4.1: Add Google Analytics (optional)](#phase-41-add-google-analytics-optional) - [Phase 5: Install Ultracite](#phase-5-install-ultracite) - [Phase 5.1: Enable ultracite/oxlint/shadcn](#phase-51-enable-ultraciteoxlintshadcn) - [Phase 6 prep: Move into apps/web/](#phase-6-prep-move-into-appsweb) --- ## Phase 2: Create Next.js app Run non-interactively with all flags: ```bash npx create-next-app@latest {{name}} --typescript --tailwind --no-linter --no-agents-md --react-compiler --app --no-src-dir --import-alias "@/*" --use-npm ``` Sets up: TypeScript, Tailwind CSS v4, no linter (Ultracite installs Oxlint and Oxfmt in Phase 5), React Compiler, App Router, Turbopack (default in Next.js 16+), no src/ directory, `@/*` import alias, npm. `--no-linter` and `--no-agents-md` matter: taking the `--biome` or `--eslint` default means uninstalling it again in Phase 5, and `--agents-md` (on by default) writes an AGENTS.md and CLAUDE.md that Ultracite then overwrites in Phase 5. Next 16.3 adds its own managed block to those files on the first `next dev` regardless, so nothing is lost by skipping the generator here. If prompted interactively, select "No, customize settings" and match the flag values above. After creation, verify: ```bash cd {{name}} npm run dev ``` Confirm the app loads at `http://localhost:3000`. The generated `.gitignore` already lists `.next/`, `.env*`, and `next-env.d.ts`. Leave `next-env.d.ts` ignored: Next.js regenerates it on every `dev`, `build`, and `typegen`, and its contents are an implementation detail. ## Phase 2.1: Upgrade to TypeScript 7 `create-next-app` installs TypeScript 5. Move to TypeScript 7: ```bash npm install -D typescript@^7 ``` That is the whole step. No config goes with it: in 16.3, `next build` runs the project-local `tsc` CLI by default rather than loading TypeScript's JavaScript compiler API, which is what makes TypeScript 7 work at all (7 does not ship that API). `experimental.useTypeScriptCli` exists only to turn the CLI checker back **off** by setting it to `false`, so a fresh scaffold should never mention it. Verify: ```bash npx tsc --version # Version 7.x npm run build # type check runs through tsc, build succeeds ``` Behaviour changes to expect: - Errors are raw `tsc` diagnostics; no Next.js code frames or route-specific rewrites. - The whole `tsconfig.json` project is checked, including test files and `.next/dev/types`. - In VS Code, run "TypeScript: Select TypeScript Version" > "Use Workspace Version" so the editor matches the build. ## Phase 2.2: Turn on Instant Navigations Cache Components and Partial Prefetching make rendering dynamic by default and let every `<Link>` prefetch a shared App Shell. Adopting them in an existing app is a migration; in a new one it is four lines, because there is no legacy caching to unwind and no `<Link prefetch={true}>` to audit. ```typescript import type { NextConfig } from "next"; const nextConfig: NextConfig = { cacheComponents: true, partialPrefetching: true, reactCompiler: true, // Version-skew protection and cache busting: clients on an old deployment // hard-reload instead of loading stale chunks. Vercel sets the variable at // build time; anywhere else it is undefined and the option is inert. deploymentId: process.env.VERCEL_DEPLOYMENT_ID, experimental: { // Runs the React Compiler inside Turbopack as native code instead of // through the Babel plugin. Experimental in 16.3; see the note below. turbopackRustReactCompiler: true, }, }; export default nextConfig; ``` `partialPrefetching` only works with `cacheComponents`, so the two ship together or not at all; `next dev` and `next build` refuse the config otherwise. `turbopackRustReactCompiler` is the one flag here Next.js still marks experimental: the 16.3 docs describe it as released "to gather feedback before it becomes the default". It removes the Babel step from the pipeline, which is where most of the React Compiler's build cost lives, so the scaffold turns it on, but tell the user it is experimental. The exit is one line: drop the flag and `npm install -D babel-plugin-react-compiler`, and `reactCompiler: true` keeps working through Babel. With the Rust compiler on, `babel-plugin-react-compiler` is not needed. `create-next-app --react-compiler` installed it anyway, so remove it now (`npm uninstall babel-plugin-react-compiler`), and add no other Babel transform: any Babel step in the pipeline gives back most of what the Rust path saves. What the flags change, and what to write from day one: - Nothing is cached unless a function says `'use cache'`. Add it at the data access, with `cacheLife` for how long and `cacheTag` for what invalidates it. On Vercel that cache is per function instance; anything that must be shared across instances (the data behind a sitemap or a list page) uses `'use cache: remote'`. - Four route segment configs are gone: `export const dynamic`, `dynamicParams`, `revalidate`, and `fetchCache` are build errors under Cache Components, in pages and route handlers alike. A `'use cache'` helper plus `cacheLife` replaces them; the directive goes on the helper, never on a `GET` export. - `generateStaticParams` must return at least one param, or the build raises `empty-generate-static-params`. Unlisted params get the App Shell on first visit and upgrade in the background. - Never `await params` or `searchParams` at the top of a page. Pass the promise into a `<Suspense>`-wrapped child and await it there, or the shell is tied to one URL. Type the props with the generated `PageProps<'/route'>` helper. - Filters on a list page live in path segments (`/projects/tag/[slug]`), not `searchParams`. Reading search params opts the list out of static rendering and streams it twice; a `has: [{ type: 'query' }]` redirect keeps the old query form working. - Same for `cookies()` and `headers()`: read them inside a boundary so the rest of the page still prerenders. - No `new Date()`, `Date.now()`, `Math.random()` or `crypto.randomUUID()` during render, in server or client components. These are hard build errors. The docs give two fixes: `await connection()` inside a `<Suspense>`-wrapped component for a per-request value, or a `'use cache'` function for one value shared across users (a copyright year, a build stamp). Reading the clock in `next.config.ts` and passing it through `env` also works, but that `env` option is marked legacy; prefer the cached function. - `useSearchParams` always needs a `<Suspense>` boundary, even in a `"use client"` page. - The previous route stays mounted as hidden DOM during navigation (React `<Activity>`), so backgrounds and themes belong to the route, never to `body` or `html`, and any theme switch keys off `usePathname()` rather than a class on `body`. Component state survives back navigation too; reset it in an effect or derive it from the URL. - Keep filesystem-reading modules apart from the constants client components and `proxy.ts` import. A lazy-loaded footer that imports the project list pulls the whole dataset into the browser bundle, and a `lib/site.ts` that imports `next/headers` cannot be imported by the proxy at all. - `generateMetadata` follows the same rules. External data goes behind `'use cache'` inside it; runtime data (`cookies()`, `params`) needs a dynamic marker in the page, or the build raises `blocking-prerender-metadata-runtime`. Verify with `next dev` rather than the build. Instant navigation validation runs in development only and never fails `next build`, so a green build is not evidence. Load each route and confirm the dev overlay reports none. The Navigation Inspector in the Next.js DevTools ("Pause on navigations") freezes the page at its shell so you can see what a visitor gets before data streams in. ## Phase 3: Install Blode UI components and icons Blode UI is a third-party shadcn/ui registry served at `blode.co/ui` (the `ui.blode.co` subdomain 301s there). Use the hosted `@blode` namespace flow. ```bash npx shadcn@latest init npx shadcn@latest registry add @blode=https://blode.co/ui/r/{name}.json npm install blode-icons-react ``` Then open `components.json` and change the icon library before adding any component: ```json { "iconLibrary": "blode-icons-react" } ``` `shadcn init` writes `"iconLibrary": "lucide"`. Left alone, every component the CLI adds imports from `lucide-react`, and the replace step below repeats on each add. Now add components: ```bash npx shadcn@latest add @blode/button ``` Order matters: `registry add` must run before any `add @blode/...` call, or the namespace is unknown and the add fails. Creates: - `components.json`: shadcn config, the Blode registry mapping, and the icon library - `lib/utils.ts`: `cn()` helper, re-exported from the [`cn`](https://github.com/shadcn-ui/cn) package - `components/ui/button.tsx`: button from the Blode registry - CSS variable updates in `app/globals.css` Icons: use `blode-icons-react` for all icon imports. If any generated file still imports `lucide-react`, replace the import paths with `blode-icons-react`. `lucide-react` is not a dependency of this scaffold; if it appears in `package.json`, remove it. Class merging goes through `cn`, which does the conditional joining and the Tailwind conflict resolution in one function. Do not add `clsx` or `tailwind-merge`. `class-variance-authority` is a separate concern and is still what defines variants. ## Phase 4: Install Agentation ```bash npm install agentation ``` Patch `app/layout.tsx`: add `import { Agentation } from "agentation";` at the top, and render the component before `</body>` behind a dev-only guard, `{process.env.NODE_ENV === "development" && <Agentation />}`. Full pattern: ```tsx import { Agentation } from "agentation"; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`} > {children} {process.env.NODE_ENV === "development" && <Agentation />} </body> </html> ); } ``` ## Phase 4.1: Add Google Analytics (optional) ```bash npm install @next/third-parties@latest ``` Add two lines to the Phase 4 layout: the import, and the `<GoogleAnalytics>` element as a sibling of `<body>` (inside `<html>`, after `</body>`), which is where the Next.js third-parties guide places it: ```tsx import { GoogleAnalytics } from "@next/third-parties/google"; // ...inside <html>, after </body>: <GoogleAnalytics gaId="G-XYZ" /> ``` Replace `"G-XYZ"` with your GA4 measurement ID. For any other analytics or error-tracking SDK (PostHog, Sentry), initialise it in `instrumentation-client.ts` at the app root rather than in a client component. The file runs before the app hydrates, needs no exports, and keeps the SDK out of the component tree. Lessons from a production PostHog setup that apply to any SDK: - Guard `init` against `localhost`, `127.0.0.1`, and `*.localhost` (named dev origins from portless) so development sessions do not land in production data. - Point `api_host` at a reverse proxy on your own domain, set from a `NEXT_PUBLIC_` variable, so ad blockers that list the vendor's hosts do not drop the data; set `ui_host` to the vendor's real app so its toolbar and links still work. The proxy origin then belongs in the CSP `script-src` (the SDK lazy-loads chunks), `connect-src`, and `worker-src 'self' blob:`. - Filter `before_send` for browser-extension exceptions (`chrome-extension://`, `runtime.sendMessage`, `Extension context invalidated`) and framework noise (`AbortError`, `Script error.`, `Internal Next.js error`) or the error inbox is unusable within a week. - Server-side captures go straight to the ingestion host (a server request has no blocker to get past) and reuse the browser cookie's `distinct_id` so conversions attach to the same person; send them with `after()`. - A build-time source-map upload wrapper that throws when its credentials are missing must be applied conditionally, or a fresh clone and every Vercel build without the variables fails on `Failed to load next.config.ts`. ## Phase 5: Install Ultracite 1. Run Ultracite init non-interactively (Oxlint + Oxfmt + Lefthook). Scaffolding with `--no-linter` means there is no Biome or ESLint config to remove first; if you inherited one from an older scaffold, delete it and uninstall the dependency before this step, or two linters fight over the same files. ```bash npx ultracite@latest init \ --linter oxlint \ --frameworks next react \ --js-plugins @shadcn/lint \ --integrations lefthook \ --agents universal \ --pm npm \ --skip-install \ --quiet ``` Flag notes: - `--frameworks` takes space-separated values (`next react`), not commas; commas fail validation. - `--js-plugins @shadcn/lint` is how Ultracite 7.12+ registers `ultracite/oxlint/shadcn`. It is opt-in and required here. Older CLIs reject the value or skip the preset; confirm `ultracite` is ≥ 7.12 after install. - `--agents universal` writes `AGENTS.md` with the Ultracite code standards. Without it, `--quiet` skips the agent prompt and no file is written. - `--skip-install` lets you review the generated `package.json` changes before installing. - Omit `--quiet` to confirm the generated file list interactively. Sets up (verified against a real `ultracite@7.12.0 init` run with these flags): - `oxlint.config.ts`: extends `ultracite/oxlint/{core,next,react,shadcn}` and hoists `jsPlugins: shadcn.jsPlugins`. Phase 5.1 only edits this file if that import is missing. - `oxfmt.config.ts`: extends `ultracite/oxfmt` - `lefthook.yml`: a pre-commit hook. This copy is temporary. Phase 6 replaces it with a root-level file scoped to `apps/web/`, because git reads `lefthook.yml` only from the directory that holds `.git`. - `AGENTS.md` with the Ultracite code standards - In `package.json`: `check` and `fix` scripts, `"type": "module"`, and `oxlint`, `oxfmt`, `lefthook` (often `latest`) plus `@shadcn/lint` and a pinned `ultracite`. No `prepare` script: with `--skip-install` the lefthook install step that would write it is skipped, and the root `package.json` in Phase 6 owns it instead. 2. Install, pin, and verify: ```bash npm install npx ultracite fix # oxfmt --write + oxlint --fix npx ultracite check # oxfmt --check + oxlint ``` Both pass with zero errors. Leave the generated `extends` (including `shadcn`) and `ignorePatterns` intact. Replace the `latest` ranges in `devDependencies` with the versions `npm install` resolved (`npm ls ultracite oxlint oxfmt lefthook @shadcn/lint --depth=0`), so the hook and CI run the same binaries. `ultracite` must be ≥ 7.12. Use AGENTS.md directly and remove any generated duplicate CLAUDE.md wrapper; Claude Code supports AGENTS.md through its built-in mod. On the first `next dev` run from a coding agent's shell, Next 16.3 appends its managed `nextjs-agent-rules` block to `AGENTS.md`; content outside the markers is preserved, `CLAUDE.md` is left alone when it exists, and nothing is written from a plain terminal. ## Phase 5.1: Enable ultracite/oxlint/shadcn Ultracite 7.12 ships an opt-in `ultracite/oxlint/shadcn` preset on top of `@shadcn/lint`. It enables all six design-system rules at `error` with the upstream `allow: ["layout"]` policy, and relaxes the component-authoring rules inside `**/components/ui/**`. This phase runs before the turbo move, in `{{name}}/`. After Phase 6 the same files live in `apps/web/`; if you are wiring this into an already-moved tree, `cd apps/web` and edit there, never at the turborepo root. Requires Ultracite ≥ 7.12, Oxlint ≥ 1.80 (JS plugins), and Node ≥ 20.19. Prefer the Phase 5 init flag. Do not fall back to `jsPlugins: ["@shadcn/lint"]` plus a hand-rolled starter-only `no-restyle` snippet. 1. Confirm `oxlint.config.ts` looks like this (framework import order may follow `--frameworks next react`). Keep `core`, `next`, and `react`; add `shadcn`. `jsPlugins: shadcn.jsPlugins` re-declares the plugin on the root config so Knip does not flag `@shadcn/lint` as unused. ```typescript import { defineConfig } from "oxlint"; import core from "ultracite/oxlint/core"; import next from "ultracite/oxlint/next"; import react from "ultracite/oxlint/react"; import shadcn from "ultracite/oxlint/shadcn"; export default defineConfig({ extends: [core, next, react, shadcn], ignorePatterns: core.ignorePatterns, jsPlugins: shadcn.jsPlugins, }); ``` If init ran without the flag, or `ultracite` was older than 7.12, upgrade (`npm install -D ultracite@latest`), install the plugin (`npm install -D @shadcn/lint`), and add the `shadcn` import, `extends` entry, and `jsPlugins` hoist yourself. Re-running `npx ultracite@latest init` with the same flags (including `--js-plugins @shadcn/lint`) also updates an existing config. The preset already turns `shadcn/no-restyle`, `shadcn/no-arbitrary-values`, and `shadcn/require-static-classes` off for `**/components/ui/**` (definitions must restyle). Do not duplicate that override when the default alias is in use. If `components.json` `aliases.ui` points elsewhere, add a matching override for that path and, if needed, `settings.shadcn.ui` on this root config (Oxlint does not merge `settings` from extended configs). 2. Pin `@shadcn/lint` (and `oxlint` if you bumped it) to the resolved versions, the same pin Phase 5 applied to `ultracite`, `oxfmt`, and `lefthook`. 3. Append this to `AGENTS.md` (outside any later Next-managed markers): ```md ## Design-system lint Ultracite extends `ultracite/oxlint/shadcn` (`@shadcn/lint`). After UI changes, run `npx ultracite check`. Findings name the variant, token, or file to use instead. Autofix with `npx ultracite fix`; remaining diagnostics with `npx ultracite fix --codex` (or `--claude`) when that CLI is available. Call sites may add layout classes (`mt-4`, `w-full`); appearance belongs in `components/ui/`. ``` 4. Verify from this directory, not the parent: ```bash npx ultracite check ``` Zero errors. A plugin-load failure usually means Ultracite is older than 7.12, Oxlint is older than 1.80, `@shadcn/lint` is not installed in this package, or `shadcn` is missing from `extends`. Agents see design-system errors through the same Ultracite check that CI runs. `ultracite fix` applies mechanical autofixes; `ultracite fix --codex` (or `--claude`) hands the rest to the local agent CLI, file by file. ## Phase 6 prep: Move into apps/web/ From the parent directory of `{{name}}`: ```bash mkdir -p {{name}}-turbo/apps mv {{name}} {{name}}-turbo/apps/web mv {{name}}-turbo {{name}} ``` The app is now at `{{name}}/apps/web/`. Root config files are generated in `{{name}}/` during Phase 6. `oxlint.config.ts` and the `@shadcn/lint` dependency move with the app; do not reinstall them at the turborepo root. -
deploy-and-launch.md 7.6 KB
# Deploy and Launch ## Contents - [Phase 7: GitHub setup](#phase-7-github-setup) - [Phase 7: Vercel deployment](#phase-7-vercel-deployment) - [Phase 8: Pre-launch checklist](#phase-8-pre-launch-checklist) - [Validation checklist](#validation-checklist) --- ## Phase 7: GitHub setup From the project root (`{{name}}/`): ```bash git init git add -A git commit -m "initial commit" git branch -M main gh repo create {{repo}} --public --source=. --remote=origin --push ``` Creates the repo and pushes in one step via the GitHub CLI (`gh`). If `gh` is unavailable: ```bash git remote add origin https://github.com/{{repo}}.git git push -u origin main ``` `git add -A` is safe here because the tree is fresh and both `.gitignore` files are in place. Confirm `git status` shows no `.next/`, `node_modules/`, or `next-env.d.ts` before committing. ## Phase 7: Vercel deployment Via the Vercel CLI: ```bash npx vercel --yes npx vercel --prod ``` Or via the dashboard: 1. Go to [vercel.com/new](https://vercel.com/new) and add a new project. 2. Import the GitHub repo (`{{repo}}`). 3. Vercel auto-detects the turborepo and Next.js app in `apps/web`. 4. Deploy. Add custom domain `{{domain}}` (dashboard Settings > Domains, or `npx vercel domains add {{domain}}`). On a 404 or wrong app, set the project Root Directory to `apps/web` (dashboard Settings > General > Root Directory) and redeploy; Vercel does not always infer the app location in a fresh turborepo. Optional, once a second workspace exists: add `apps/web/vercel.json` so Vercel skips builds that turbo can prove did not touch the app. ```json { "$schema": "https://openapi.vercel.sh/vercel.json", "ignoreCommand": "npx turbo-ignore" } ``` `turbo-ignore` exits 0 (skip the build) when no file in the workspace or its dependencies changed since the last deploy. Prefix it with a preview guard (`if [ "$VERCEL_ENV" = "preview" ]; then exit 0; fi;`) only if the project deliberately does not build previews. A root `.vercelignore` listing `.turbo`, `node_modules`, `*.log`, and `.git` trims the upload; excluding `.git` means build-time tools cannot read the commit SHA, so pass `VERCEL_GIT_COMMIT_SHA` to anything that wants a release version. Never assume `{{name}}.vercel.app` is yours. The namespace is global and first-come, so a name can already point at an unrelated site; use only the alias Vercel confirms for the project. Verify: `https://{{domain}}` loads the default Next.js page. ### CI Vercel builds are the only gate otherwise, and a failed production build is found after merge. Add `.github/workflows/check.yml` so every pull request runs the same checks as the hook plus a production build: ```yaml name: Check on: pull_request: push: branches: [main] permissions: contents: read jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: cache: npm node-version: "24" - run: npm ci - run: npm run check - run: npm run build ``` Pin `node-version` to what Vercel's project settings use, and cache `apps/web/.next/cache` between runs if build time matters (the Next.js CI caching guide has the per-provider snippets). ## Phase 8: Pre-launch checklist ### Site URL and metadataBase Create `apps/web/lib/site.ts` exporting `siteUrl = "https://{{domain}}"` and `siteName`, then set `metadataBase: new URL(siteUrl)` in the root layout's `metadata` export beside `title: { default, template }` and `description`. Every relative `alternates.canonical` and `openGraph.images` value resolves against it, and a relative value with no `metadataBase` is a build error. `seo` fills in the rest of that object after launch. ### Search Console and Bing verification Add the property for `https://{{domain}}` in Google Search Console and Bing Webmaster Tools before launch, and carry the tokens in `metadata.verification` (`google`, plus `other: { 'msvalidate.01': '...' }` for Bing) rather than a DNS record. Submit `/sitemap.xml` once the first deploy is live. ### security.txt Create `apps/web/public/.well-known/security.txt` with `Contact:`, `Expires:` (no more than a year out), `Preferred-Languages:`, and `Canonical: https://{{domain}}/.well-known/security.txt`. This is the one dotfile path that survives the static pipeline. ### Favicon Generate a favicon package from your source image at [RealFaviconGenerator](https://realfavicongenerator.net/). Its Next.js export drops `favicon.ico`, `icon0.svg`, `icon1.png`, `apple-icon.png`, and `manifest.json` into `apps/web/app/`, and the `web-app-manifest-192x192.png` / `web-app-manifest-512x512.png` files into `apps/web/public/`. Next.js turns the `app/` files into `<link rel="icon">` and `<link rel="apple-touch-icon">` tags through its file conventions; nothing goes in `metadata.icons`. ### OG image Create in `apps/web/app/`: - `opengraph-image.png` (1200x630, under 8 MB or the build fails) - `opengraph-image.alt.txt` (one line of alt text; it becomes `og:image:alt`) Next.js App Router serves the file as both the Open Graph and the Twitter card image via file-based metadata conventions. A separate `twitter-image.png` is redundant: `twitter:image` falls back to the OG image when the file is absent, and a byte-identical duplicate only doubles the payload. Alternatively, generate the card with code (`opengraph-image.tsx` and `ImageResponse` from `next/og`); `seo` covers that pattern and the metadata merge rules that decide whether a page keeps its card. ### Skill handoffs After deployment, run these skills in order: 1. `seo`: metadata, structured data, sitemap, robots, Core Web Vitals 2. `ui-design` Audit mode: accessibility, typography surface checks, interaction quality, craft polish 3. `ui-animation`: motion easing, timing, gestures, and review rules ## Validation checklist After all phases, verify: - [ ] `npm run dev` starts from project root (turbo runs apps/web) and the dev overlay reports no instant-navigation insight on the home route - [ ] `npm run build` succeeds with no errors, and `npm run start -w web` serves the production build (kill anything on port 3000 first; `next start` on a taken port fails silently while the old server keeps answering) - [ ] `npm run check` passes lint, format, and type checks from the root - [ ] `npx lefthook run pre-commit --all-files` passes from the root - [ ] The CI workflow ran green on the first pull request - [ ] `apps/web/AGENTS.md` ends with the Next-managed `nextjs-agent-rules` block (written on the first `next dev` from a coding agent) and is committed; any duplicate `apps/web/CLAUDE.md` wrapper is removed; the Phase 5.1 design-system lint paragraph (including `ultracite fix` / `ultracite fix --codex`) is present outside those markers - [ ] `babel-plugin-react-compiler` is not in `apps/web/package.json`; `ultracite` (≥ 7.12), `oxlint` (≥ 1.80), `oxfmt`, `lefthook`, and `@shadcn/lint` are pinned, not `latest` - [ ] `apps/web/oxlint.config.ts` extends `ultracite/oxlint/{core,next,react,shadcn}` (framework order may vary) and hoists `jsPlugins: shadcn.jsPlugins`. It does not use a hand-rolled `jsPlugins: ["@shadcn/lint"]` plus starter-only `no-restyle` block. `shadcn/no-restyle` stays off for `**/components/ui/**` via the preset (or a matching override when `aliases.ui` is a different path) - [ ] `metadataBase` is set to `https://{{domain}}` and `metadata.verification` carries the Search Console token - [ ] `git status` is clean after `npm run dev` (no regenerated files left uncommitted) - [ ] GitHub repo has initial commit pushed - [ ] Vercel deployment is live at `{{domain}}` - [ ] Favicon appears in browser tab - [ ] OG image renders in social card previews (use https://opengraph.xyz to test) -
turbo-configs.md 9.8 KB
# Turborepo Config Templates ## Contents - [Root package.json](#root-packagejson) - [turbo.json](#turbojson) - [Root lefthook.yml](#root-lefthookyml) - [Root .gitignore](#root-gitignore) - [knip.json](#knipjson) - [apps/web/package.json scripts](#appswebpackagejson-scripts) - [apps/web/next.config.ts](#appswebnextconfigts) - [Root AGENTS.md](#root-agentsmd) --- ## Root package.json Create at `{{name}}/package.json`. Copy the `ultracite` version from `apps/web/package.json` so the two cannot drift, and set `packageManager` to the output of `npm --version` (corepack refuses a mismatch): ```json { "name": "{{name}}", "private": true, "packageManager": "npm@{{npm_version}}", "workspaces": [ "apps/*" ], "scripts": { "build": "turbo build", "dev": "turbo dev", "lint": "turbo lint", "lint:fix": "turbo lint:fix", "format": "turbo format", "format:check": "turbo format:check", "check-types": "turbo check-types", "check": "turbo lint format:check check-types", "fix": "turbo lint:fix format", "prepare": "lefthook install || true" }, "devDependencies": { "lefthook": "^2", "turbo": "^2", "ultracite": "{{ultracite_version}}" } } ``` `check` and `fix` go through turbo so they run inside `apps/web`, where the Oxlint and Oxfmt configs live. `prepare` installs the git hooks from the root `lefthook.yml` on every `npm install`; the `|| true` keeps a CI or Vercel install from failing when there is no `.git` (Vercel builds have none). ## turbo.json Create at `{{name}}/turbo.json`: ```json { "$schema": "https://turbo.build/schema.json", "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "out/**"] }, "dev": { "cache": false, "persistent": true }, "lint": { "dependsOn": ["^build"] }, "lint:fix": { "cache": false }, "format": { "cache": false }, "format:check": { "dependsOn": ["^build"] }, "check-types": { "dependsOn": ["^build"] } } } ``` When the build later needs a secret (an email API key, an analytics token), list it under `build.passThroughEnv` so turbo forwards it without hashing it into the cache key. ## Root lefthook.yml Create at `{{name}}/lefthook.yml` and delete `apps/web/lefthook.yml`. Lefthook loads the config next to `.git`; a file inside a workspace is read only when lefthook is invoked from that directory, which the git hook never does. ```yaml # `root` scopes each job to the workspace: lefthook filters staged files to # that directory and passes them relative to it, which is what oxfmt and oxlint # need because their config files live there rather than here. pre-commit: parallel: true jobs: # oxfmt and oxlint directly, not `ultracite fix`: ultracite exits non-zero # when the staged set contains no lintable JS/TS file, so a docs-only or # CSS-only commit would fail the hook outright. Two jobs with their own # globs let lefthook skip whichever one has nothing to do. - name: oxfmt root: "apps/web/" # Matches what `format:check` inspects (`oxfmt --check .`), markdown # included. Omitting md/mdx lets a doc pass this hook and still fail the # format gate in CI. glob: - "*.js" - "*.jsx" - "*.ts" - "*.tsx" - "*.json" - "*.jsonc" - "*.css" - "*.md" - "*.mdx" run: npx oxfmt --write {staged_files} stage_fixed: true - name: oxlint root: "apps/web/" glob: "*.{js,jsx,ts,tsx}" run: npx oxlint --fix {staged_files} stage_fixed: true ``` Verify from the root after `npm install`: ```bash npx lefthook run pre-commit --all-files ``` ## Root .gitignore Create at `{{name}}/.gitignore`. `apps/web/.gitignore` from create-next-app stays in place and already covers `.next/`, `.env*`, and `next-env.d.ts`; this file covers the root and anything a second workspace adds later. ``` node_modules out dist *.tgz coverage *.lcov logs *.log .env .env.development.local .env.test.local .env.production.local .env.local .eslintcache .cache *.tsbuildinfo .next next-env.d.ts .idea .DS_Store .turbo .vercel # Agent scratch output stays out; knowledge files are the project's memory # and must survive a machine change. .claude/ !/.claude/ /.claude/* !/.claude/knowledge/ !apps/web/.claude/ .cursor/ .vscode/ ``` `.env.local` lives in `apps/web/`, where Next.js reads it, and `apps/web/.gitignore` already ignores `.env*`; `vercel env pull apps/web/.env.local` is the pull command. ## knip.json Create at `{{name}}/knip.json`: ```json { "$schema": "https://unpkg.com/knip@6/schema.json", "ignore": [".vercel/**"] } ``` Run dead-code analysis on demand with `npx knip` from the root (not a devDependency; npx fetches it). Add workspace-specific entry points as needed (e.g. CLI apps or docs sites with custom entry files). ## apps/web/package.json scripts Replace the `scripts` block in `apps/web/package.json`. `ultracite init` left `check` and `fix` there; the root scripts replace them, so drop both. Keep the `"type": "module"` it added: ```json { "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "oxlint .", "lint:fix": "oxlint --fix .", "format": "oxfmt --write .", "format:check": "oxfmt --check .", "check-types": "tsc --noEmit" } } ``` Script names match the tasks in `turbo.json` so turbo can orchestrate them across workspaces. If you add a test runner later, add a matching `test` task to `turbo.json` at the same time; with `node --test`, glob the files (`node --test "lib/**/*.test.ts"`) rather than naming one. Optional: `portless <name> next dev` as the `dev` script gives the app a stable `https://<name>.localhost` origin, so several apps run side by side without port juggling. Two things Vercel's Linux builders can trip on that a Mac never shows: Tailwind's oxide and lightningcss ship platform-specific binaries, and when the lockfile was generated on macOS an `npm ci` on Linux can miss them. Pin the Linux packages in `optionalDependencies` (`@tailwindcss/oxide-linux-x64-gnu`, `lightningcss-linux-x64-gnu`) at the versions the lockfile resolves if that happens. And set an explicit `browserslist` so CSS output does not change when the default query moves. ## apps/web/next.config.ts Verify `apps/web/next.config.ts` still has React Compiler and the Instant Navigations flags after the move: ```typescript import type { NextConfig } from "next"; const nextConfig: NextConfig = { cacheComponents: true, partialPrefetching: true, reactCompiler: true, deploymentId: process.env.VERCEL_DEPLOYMENT_ID, poweredByHeader: false, experimental: { // blode-icons-react is not on Next's built-in optimizePackageImports list // (lucide-react is), so name it or every icon import pulls the barrel. optimizePackageImports: ["blode-icons-react"], turbopackRustReactCompiler: true, }, }; export default nextConfig; ``` `create-next-app` generates this file when React Compiler is selected; verify `reactCompiler: true` is present. The others come from Phase 2.2 and must survive the move into `apps/web/`, since dropping `cacheComponents` silently takes `partialPrefetching` with it. No `turbopack.root` is needed: Turbopack infers the workspace root from the lockfile at `{{name}}/package-lock.json`. If the config ever imports a project module (a `basePath` constant, a site URL), import it by relative path: Next compiles `next.config.ts` without `tsconfig` path resolution, so an `@/` alias resolves against the wrong directory. `experimental.useOffline: true` is worth turning on once the app has forms; it holds a navigation or Server Action through a connectivity drop and retries on reconnect instead of throwing. ## Root AGENTS.md Ultracite's `AGENTS.md` and the Next-managed block both live in `apps/web/`. The root needs its own short file so an agent opening the repo runs commands from the right directory. Create `{{name}}/AGENTS.md`: ````markdown # {{name}} Turborepo. The site lives in `apps/web` (see that app's `AGENTS.md` for code standards and the Next.js docs pointer). Everything here applies to every task, including ones that only touch root config. ## Commands Run these from this directory, not from `apps/web`; they go through Turborepo: ```bash npm run dev # start the site npm run build # build every workspace npm run lint # oxlint, including ultracite/oxlint/shadcn npm run lint:fix # oxlint --fix npm run format # oxfmt --write . (scope to your changes) npm run format:check # oxfmt --check npm run check-types # tsc --noEmit npm run check # lint + format:check + check-types npm run fix # lint:fix + format ``` `npm run check` is the combined quality gate; the pre-commit hook runs the same tools on staged files. `npm run lint` must run inside `apps/web` (turbo does that) so Oxlint loads `oxlint.config.ts` and `ultracite/oxlint/shadcn`. ## Rules - No em dashes in copy, content, docs, or commit messages. Rephrase with a colon, comma, or a separate sentence. - `next.config.ts` keeps `cacheComponents`, `partialPrefetching`, and `reactCompiler` on. Read the Instant Navigations rules in `apps/web/AGENTS.md` before adding a route. - Ultracite's `ultracite/oxlint/shadcn` preset (`@shadcn/lint`) enforces design-system `className` contracts: call sites may add layout classes (`mt-4`, `w-full`); restyle in `components/ui/` instead. After UI work, `npx ultracite fix` then `npx ultracite fix --codex` from `apps/web`. ```` Use AGENTS.md directly without a CLAUDE.md wrapper. After the first `npm run dev` from the coding agent's shell, confirm `apps/web/AGENTS.md` ends with the `<!-- BEGIN:nextjs-agent-rules -->` block and commit it; remove any generated duplicate `apps/web/CLAUDE.md`. The Phase 5.1 design-system lint paragraph stays above those markers.
-
-
SKILL.md 16.9 KB
--- 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 `<Agentation />` 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 `<Activity>`), 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 `<!-- BEGIN:nextjs-agent-rules -->` 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.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.