Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download mblode-agent-skills-skills_scaffold-nextjs-24f4fd8.zip · 23 KB
Part of mblode/agent-skills — 22 skills

Install

skills CLI npx skills add https://github.com/mblode/agent-skills/tree/main/skills/scaffold-nextjs
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install mblode-agent-skills@llmmart
Git 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 (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:

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:

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.

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.

No comments yet.

Reviews (0)

No reviews yet.

Related