HookStack
Back to catalogue
ValidationPostToolUse· Write|EditPostToolUseAfter tool execution · non-blocking· non-blocking

Next.js image guard

Stop a raw <img> from wrecking your LCP score

Flags raw <img> tags in components (src/**/*.tsx) and routes you to next/image (<Image>) — automatic LCP optimization, zero layout shift (CLS), lazy-loading and modern formats (AVIF/WebP) for free. Non-blocking: a raw <img> is a silent Core Web Vitals regression that quietly costs you ranking.

What does the Next.js image guard hook do?

Next.js image guard is a Claude Code PostToolUse hook matching Write|Edit. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Stop a raw <img> from wrecking your LCP score.

As a PostToolUse hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Protect your Core Web Vitals when adding imagery
  • Enforce next/image over raw <img> in every review

Tags

#seo#performance#core-web-vitals#nextjs#images#lcp

settings.json fragment

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/seo-next-image-guard.mjs"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/seo-next-image-guard.mjs

#!/usr/bin/env node
// Interdit les balises <img> brutes dans les composants (PostToolUse Write|Edit).
// Cible : src/**/*.tsx. Next.js fournit next/image (<Image>) qui optimise le LCP,
// évite le CLS et sert des formats modernes — un <img> brut est une régression
// perf/SEO. Non bloquant : signale chaque occurrence avec la correction.
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';

// <img …> mais pas <Image …> (next/image) ni un nom commençant par Img…
const RAW_IMG_RE = /<img(?=[\s/>])/;

export function run(input, { readFile = readFileSync } = {}) {
  const filePath = input.tool_input?.file_path ?? '';
  if (!/\/src\/.*\.tsx$/.test(filePath)) return null;

  let content;
  try {
    content = readFile(filePath, 'utf8');
  } catch {
    return null;
  }

  if (!RAW_IMG_RE.test(content)) return null;

  return {
    message:
      `[seo-next-image] ${filePath} uses a raw <img> tag.\n` +
      `  → Use next/image (<Image>) instead: it optimizes LCP, prevents layout ` +
      `shift (CLS) and serves modern formats.\n`,
  };
}

/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const input = JSON.parse(readFileSync(0, 'utf8'));
  const result = run(input);
  if (result?.message) process.stderr.write(result.message);
}

Learn more

Related hooks