All skills
mblode avatar

/scaffold-nextjs

@eb5e208
by Matthew Blodemblode/agent-skills136 stars
12

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.

Use this Skill: https://skilld.dev/gh/mblode/agent-skills/scaffold-nextjs

This session only. Nothing lands on disk.

referencesapp-setup.md

≈5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

App Setup Commands

Contents


Phase 2: Create Next.js app

Run non-interactively with all flags:

pnpm create next-app@latest {{name}} --typescript --tailwind --no-linter --no-agents-md --react-compiler --app --no-src-dir --import-alias "@/*" --use-pnpm

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, pnpm. It may also write a pnpm-workspace.yaml listing which dependency build scripts may run; Phase 6 moves those settings to the turborepo root.

--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:

cd {{name}}
pnpm 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:

pnpm add -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:

pnpm exec tsc --version   # Version 7.x
pnpm 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.

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 pnpm add -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 (pnpm remove 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.

pnpm dlx shadcn@latest init
pnpm dlx shadcn@latest registry add @blode=https://blode.co/ui/r/{name}.json
pnpm add blode-icons-react

Then open components.json and change the icon library before adding any component:

{
  "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:

pnpm dlx 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 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

pnpm add 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:

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)

pnpm add @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:

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.
pnpm dlx ultracite@latest init \
  --linter oxlint \
  --frameworks next react \
  --js-plugins @shadcn/lint \
  --integrations lefthook \
  --agents universal \
  --pm pnpm \
  --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.
  1. Install, pin, and verify:
pnpm install
pnpm exec ultracite fix     # oxfmt --write + oxlint --fix
pnpm exec 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 pnpm install resolved (pnpm 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.
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 (pnpm add -D ultracite@latest), install the plugin (pnpm add -D @shadcn/lint), and add the shadcn import, extends entry, and jsPlugins hoist yourself. Re-running pnpm dlx 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).

  1. Pin @shadcn/lint (and oxlint if you bumped it) to the resolved versions, the same pin Phase 5 applied to ultracite, oxfmt, and lefthook.

  2. Append this to AGENTS.md (outside any later Next-managed markers):

## Design-system lint

Ultracite extends `ultracite/oxlint/shadcn` (`@shadcn/lint`). After UI
changes, run `pnpm exec ultracite check`. Findings name the variant, token,
or file to use instead. Autofix with `pnpm exec ultracite fix`; remaining
diagnostics with `pnpm exec 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/`.
  1. Verify from this directory, not the parent:
pnpm exec 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}}:

mkdir -p {{name}}-turbo/apps
mv {{name}} {{name}}-turbo/apps/web
mv {{name}}-turbo {{name}}
rm -rf {{name}}/apps/web/node_modules {{name}}/apps/web/pnpm-lock.yaml

The app is now at {{name}}/apps/web/. Root config files are generated in {{name}}/ during Phase 6. The app's own node_modules and lockfile go because the root pnpm install replaces them with one workspace lockfile; a stale apps/web/pnpm-lock.yaml would make Next.js warn about multiple lockfiles and can pick the wrong workspace root. Leave apps/web/pnpm-workspace.yaml (if present) until Phase 6 copies its settings to the root. oxlint.config.ts and the @shadcn/lint dependency move with the app; do not reinstall them at the turborepo root.

Source: SKILL.md on GitHub

1 warningtoday5 checks · Risk SAFE
  • Gen Agent Trust Hubtoday

    A project scaffolding skill for Next.js and Blode UI. It performs standard development tasks like installing dependencies and deploying to Vercel. A minor security risk exists due to the interpolation of user-provided project names and repository paths into shell commands without explicit sanitization.

  • Sockettoday

    No alerts

  • Snyktoday

    Risk: LOW · No issues

  • Runlayer6mo

    2/4 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at eb5e208. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 25 minutes ago.

Activeupdated 14 hours ago
compatibility
Requires a shell, Git, Node.js, pnpm, and package registry access.

README badge

README badge for mblode/agent-skills/scaffold-nextjs