nextjs-code-quality-enforcer — Claude Code hook for Next.js
A PostToolUse hook that checks every Next.js file you write: App Router structure, Server and Client Component directives, imports, types. It runs right after an edit.
npx claude-code-templates@latest --hook development-tools/nextjs-code-quality-enforcerWhat nextjs-code-quality-enforcer does
The hook attaches to PostToolUse with the matcher Write|Edit|MultiEdit. Once a .js, .jsx, .ts or .tsx file is written successfully, it reads the path from tool_input.file_path and runs a set of checks. Anything under node_modules is skipped.
What it catches: pages missing export default function, a layout without a children prop, state hooks and event handlers inside a Server Component with no use client directive, and getServerSideProps or getStaticProps left over from the Pages Router. It also nudges you toward next/image instead of <img>, next/link instead of <a> for internal navigation, a metadata export for SEO, and a helper like clsx for class name concatenation. Notes about the React import and React.FC stay advisory.
The script is bash and parses the incoming JSON with jq, so the hook will not run without it. Timeout is 20 seconds. Real violations are printed to stderr and the command exits with code 2, which stops the flow. Keep jq on your PATH before you turn this on in a project you care about.
Install with npx claude-code-templates@latest --hook development-tools/nextjs-code-quality-enforcer; the config lands in .claude/settings.json.
What it helps with
- catch a Next.js page with no default export before you commit
- strip state hooks out of a Server Component
- switch internal links from
<a>tonext/link - find Pages Router data fetching in an App Router project
- flag plain
<img>tags wherenext/imagebelongs
Example prompts for the agent
- I just wrote app/dashboard/page.tsx, run the quality hook on it
- why is the hook complaining about useState in my component?
- check code quality on the files I just changed
How to install nextjs-code-quality-enforcer
1claude-code-templates
npx claude-code-templates@latest --hook development-tools/nextjs-code-quality-enforcer2Manually: put the files in place
.claude/settings.jsonThe hooks block: event (PreToolUse, PostToolUse, Stop…) → command. Personal ones go to ~/.claude/settings.json
Step by step, with commands for each platform: Claude Code hooks: what they are, where to configure them, how to block a risky command
Questions about nextjs-code-quality-enforcer
How do I install nextjs-code-quality-enforcer in Claude?
Run npx claude-code-templates@latest --hook development-tools/nextjs-code-quality-enforcer, then confirm the entry showed up in .claude/settings.json.
What does nextjs-code-quality-enforcer do?
After every write or edit to a JS/TS file it checks the code against Next.js conventions: App Router structure, Server and Client Component directives, imports and types. Violations are printed to stderr.
What does the hook need to run?
The bash script parses its input JSON with jq, so that utility has to be installed and on your PATH. The command timeout is 20 seconds.
Source text
nextjs-code-quality-enforcer.json · MIT
{
"description": "Enforce Next.js best practices, proper file structure, component patterns, and TypeScript usage with automated code reviews and suggestions. Validates Next.js App Router conventions, Server/Client component patterns, proper imports, and TypeScript usage. Provides real-time feedback on code quality and adherence to Next.js best practices.",
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "bash -c 'input=$(cat); FILE_PATH=$(echo \"$input\" | jq -r \".tool_input.file_path // empty\"); SUCCESS=$(echo \"$input\" | jq -r \".tool_response.success // false\"); if [ \"$SUCCESS\" = \"true\" ] && [[ \"$FILE_PATH\" =~ \\.(js|jsx|ts|tsx)$ ]] && [[ ! \"$FILE_PATH\" =~ node_modules ]]; then echo \"🔍 Next.js Code Quality Enforcer: Reviewing $FILE_PATH...\"; ISSUES=0; if [ -f \"$FILE_PATH\" ]; then if [[ \"$FILE_PATH\" =~ app/.* ]]; then echo \"📁 App Router file detected: $FILE_PATH\"; if [[ \"$FILE_PATH\" =~ page\\.(js|jsx|ts|tsx)$ ]] && ! grep -q \"export default function\" \"$FILE_PATH\" 2>/dev/null && ! grep -q \"export default async function\" \"$FILE_PATH\" 2>/dev/null; then echo \"❌ Page component must export default function\" >&2; ((ISSUES++)); fi; if [[ \"$FILE_PATH\" =~ layout\\.(js|jsx|ts|tsx)$ ]] && ! grep -q \"children\" \"$FILE_PATH\" 2>/dev/null; then echo \"❌ Layout component should accept children prop\" >&2; ((ISSUES++)); fi; if [[ \"$FILE_PATH\" =~ page\\.(js|jsx|ts|tsx)$ ]] && ! grep -q \"Metadata\" \"$FILE_PATH\" 2>/dev/null && ! grep -q \"metadata\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ Consider adding metadata export for SEO\"; fi; if grep -q \"use client\" \"$FILE_PATH\" 2>/dev/null; then echo \"🖥️ Client Component detected\"; if ! grep -E \"(useState|useEffect|onClick|onChange|onSubmit)\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ Client component without interactivity - consider Server Component\"; fi; else echo \"🚀 Server Component (default)\"; if grep -E \"(useState|useEffect|onClick|onChange|onSubmit)\" \"$FILE_PATH\" 2>/dev/null; then echo \"❌ Interactive features in Server Component - add \\\"use client\\\" directive\" >&2; ((ISSUES++)); fi; fi; fi; if [[ \"$FILE_PATH\" =~ \\.(jsx|tsx)$ ]]; then if ! grep -q \"import.*React\" \"$FILE_PATH\" 2>/dev/null && grep -q \"<\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ JSX without React import (Next.js 17+ handles this automatically)\"; fi; if ! grep -q \"FC\\|FunctionComponent\" \"$FILE_PATH\" 2>/dev/null && grep -q \"props\" \"$FILE_PATH\" 2>/dev/null && [[ \"$FILE_PATH\" =~ \\.tsx$ ]]; then echo \"💡 Consider using React.FC or explicit prop types for TypeScript\"; fi; fi; if [[ \"$FILE_PATH\" =~ \\.js$ ]] && [ -f \"tsconfig.json\" ]; then echo \"📝 JavaScript file in TypeScript project: $FILE_PATH\"; echo \"💡 Consider migrating to TypeScript for better type safety\"; fi; if grep -q \"next/image\" \"$FILE_PATH\" 2>/dev/null; then echo \"✅ Using next/image for optimized images\"; elif grep -q \"<img\" \"$FILE_PATH\" 2>/dev/null; then echo \"🖼️ Regular <img> tag detected\"; echo \"💡 Consider using next/image for better performance\"; fi; if grep -q \"next/link\" \"$FILE_PATH\" 2>/dev/null; then echo \"✅ Using next/link for navigation\"; elif grep -q \"<a href=\" \"$FILE_PATH\" 2>/dev/null && ! grep -q \"http\" \"$FILE_PATH\" 2>/dev/null; then echo \"🔗 Regular <a> tag for internal links detected\"; echo \"💡 Use next/link for internal navigation\"; fi; if grep -q \"getServerSideProps\\|getStaticProps\" \"$FILE_PATH\" 2>/dev/null; then echo \"⚠️ Pages Router data fetching methods detected\"; echo \"💡 Consider migrating to App Router with Server Components\"; fi; if grep -q \"className=.*{\" \"$FILE_PATH\" 2>/dev/null; then echo \"🎨 Dynamic className detected\"; if ! grep -q \"clsx\\|classnames\\|cn(\" \"$FILE_PATH\" 2>/dev/null; then echo \"💡 Consider using clsx or similar utility for className concatenation\"; fi; fi; if [ $ISSUES -eq 0 ]; then echo \"✅ Code quality check passed for $FILE_PATH\"; else echo \"❌ Found $ISSUES code quality issues in $FILE_PATH\" >&2; exit 2; fi; else echo \"❌ File $FILE_PATH not found\"; fi; else echo \"ℹ️ Code quality check skipped (not a JavaScript/TypeScript file or failed operation)\"; fi'",
"timeout": 20
}
]
}
]
}
}
© davila7 · MIT · davila7/claude-code-templates