SEO
Hook
Hooks
What codex-seo runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add agricidaniel/codex-seo --agent claude-codeShips with codex-seo. Installing the plugin gets these hooks.
What fires, and when
PostToolUse
- Matches
Edit|Writepython3 "${CODEX_PLUGIN_ROOT}/hooks/validate-schema.py" "$FILE_PATH"
Where it lives
- hooks/pre-commit-seo-check.shGitHub
Read the script
#!/usr/bin/env bash set -euo pipefail # Pre-commit SEO validation hook for Codex. # # Hook configuration in ~/.codex/settings.json: # { # "hooks": { # "PreToolUse": [ # { # "matcher": "Bash", # "hooks": [ # { # "type": "command", # "command": "~/.codex/skills/seo/hooks/pre-commit-seo-check.sh", # "exitCodes": { "2": "block" } # } # ] # } # ] # } # } # # NOTE: The matcher is "Bash" (tool name only). This script runs on ALL # Bash tool uses. It checks if there are staged files before proceeding. # If there are no staged changes, it exits 0 immediately. ERRORS=0 WARNINGS=0 # Check if there are staged changes โ exit early if not if ! git diff --cached --quiet 2>/dev/null; then : # There are staged changes, proceed with checks else exit 0 # No staged changes, nothing to check fi echo "๐ Running pre-commit SEO checks..." # Check staged HTML-like files STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\.(html|htm|php|jsx|tsx|vue|svelte)$' || true) if [ -z "${STAGED_FILES}" ]; then echo "โ No HTML files staged โ skipping SEO checks" exit 0 fi for file in ${STAGED_FILES}; do if [ ! -f "${file}" ]; then continue fi # Check for placeholder text in schema if grep -qiE '\[(Business Name|City|State|Phone|Address|Your|INSERT|REPLACE)\]' "${file}" 2>/dev/null; then echo "๐ ${file}: Contains placeholder text in schema markup" ERRORS=$((ERRORS + 1)) fi # Check title tag length TITLE=$(grep -oP '(?<=<title>).*?(?=</title>)' "${file}" 2>/dev/null | head -1 || true) if [ -n "${TITLE}" ]; then TITLE_LEN=${#TITLE} if [ "${TITLE_LEN}" -lt 30 ] || [ "${TITLE_LEN}" -gt 60 ]; then echo "โ ๏ธ ${file}: Title tag length ${TITLE_LEN} chars (recommend 30-60)" WARNINGS=$((WARNINGS + 1)) fi fi # Check for images without alt text if grep -qP '<img(?![^>]*alt=)' "${file}" 2>/dev/null; then echo "โ ๏ธ ${file}: Images found without alt text" WARNINGS=$((WARNINGS + 1)) fi # Check for deprecated schema types if grep -qE '"@type"\s*:\s*"(HowTo|SpecialAnnouncement)"' "${file}" 2>/dev/null; then echo "๐ ${file}: Contains deprecated schema type" ERRORS=$((ERRORS + 1)) fi # Check for FID references (should be INP) if grep -qi 'First Input Delay\|"FID"' "${file}" 2>/dev/null; then echo "โ ๏ธ ${file}: References FID โ should use INP (Interaction to Next Paint)" WARNINGS=$((WARNINGS + 1)) fi # Check meta description length META_DESC=$(grep -oP '(?<=<meta name="description" content=").*?(?=")' "${file}" 2>/dev/null | head -1 || true) if [ -n "${META_DESC}" ]; then META_LEN=${#META_DESC} if [ "${META_LEN}" -lt 120 ] || [ "${META_LEN}" -gt 160 ]; then echo "โ ๏ธ ${file}: Meta description length ${META_LEN} chars (recommend 120-160)" WARNINGS=$((WARNINGS + 1)) fi fi done echo "" if [ "${ERRORS}" -gt 0 ]; then echo "๐ ${ERRORS} critical error(s) found โ commit blocked" echo "Fix the errors above and try again." exit 2 elif [ "${WARNINGS}" -gt 0 ]; then echo "โ ๏ธ ${WARNINGS} warning(s) found โ commit allowed" exit 0 else echo "โ All SEO checks passed" exit 0 fi - hooks/validate-schema.pyRunsGitHub
Read the script
#!/usr/bin/env python3 """Post-edit schema validation hook for Codex. Validates JSON-LD schema after file edits. Returns exit code 2 to block if critical validation errors found. Hook configuration in ~/.codex/settings.json: { "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "python3 ~/.codex/skills/seo/hooks/validate-schema.py \"$FILE_PATH\"", "exitCodes": { "2": "block" } } ] } ] } } Note: matcher filters by tool name only (Edit, Write). The script itself checks if the file contains schema markup before validating. """ import json import re import sys import os from typing import List def validate_jsonld(content: str) -> List[str]: """Validate JSON-LD blocks in HTML content.""" errors = [] pattern = r'<script\s+type=["\']application/ld\+json["\']\s*>(.*?)</script>' blocks = re.findall(pattern, content, re.DOTALL | re.IGNORECASE) if not blocks: return [] # No schema found โ not an error for i, block in enumerate(blocks, 1): block = block.strip() try: data = json.loads(block) except json.JSONDecodeError as e: errors.append(f"Block {i}: Invalid JSON โ {e}") continue if isinstance(data, list): for item in data: errors.extend(_validate_schema_object(item, i)) elif isinstance(data, dict): errors.extend(_validate_schema_object(data, i)) return errors def _validate_schema_object(obj: dict, block_num: int) -> List[str]: """Validate a single schema object.""" errors = [] prefix = f"Block {block_num}" # Check @context if "@context" not in obj: errors.append(f"{prefix}: Missing @context") elif obj["@context"] not in ("https://schema.org", "http://schema.org"): errors.append(f"{prefix}: @context should be 'https://schema.org'") # Check @type if "@type" not in obj: errors.append(f"{prefix}: Missing @type") # Check for placeholder text placeholders = [ "[Business Name]", "[City]", "[State]", "[Phone]", "[Address]", "[Your", "[INSERT", "REPLACE", "[URL]", "[Email]", ] text = json.dumps(obj) for p in placeholders: if p.lower() in text.lower(): errors.append(f"{prefix}: Contains placeholder text: {p}") # Check for deprecated types schema_type = obj.get("@type", "") deprecated = { "HowTo": "deprecated September 2023", "SpecialAnnouncement": "deprecated July 31, 2025", "CourseInfo": "retired June 2025", "EstimatedSalary": "retired June 2025", "LearningVideo": "retired June 2025", "ClaimReview": "retired June 2025 โ fact-check rich results discontinued", "VehicleListing": "retired June 2025 โ vehicle listing structured data discontinued", } if schema_type in deprecated: errors.append(f"{prefix}: @type '{schema_type}' is {deprecated[schema_type]}") # Check for restricted types used incorrectly restricted = {"FAQPage": "restricted to government and healthcare sites only (Aug 2023)"} if schema_type in restricted: errors.append(f"{prefix}: @type '{schema_type}' is {restricted[schema_type]} โ verify site qualifies") return errors def main(): if len(sys.argv) < 2: sys.exit(0) filepath = sys.argv[1] if not os.path.isfile(filepath): sys.exit(0) # Only validate HTML-like files valid_extensions = (".html", ".htm", ".jsx", ".tsx", ".vue", ".svelte", ".php", ".ejs") if not filepath.endswith(valid_extensions): sys.exit(0) try: with open(filepath, "r", encoding="utf-8", errors="ignore") as f: content = f.read() except (OSError, IOError): sys.exit(0) errors = validate_jsonld(content) if not errors: sys.exit(0) # Categorize errors critical_keywords = ["placeholder", "deprecated", "retired"] critical = [e for e in errors if any(kw in e.lower() for kw in critical_keywords)] warnings = [e for e in errors if e not in critical] if warnings: print("โ ๏ธ Schema validation warnings:") for w in warnings: print(f" - {w}") if critical: print("๐ Schema validation ERRORS (blocking):") for e in critical: print(f" - {e}") sys.exit(2) # Block the edit sys.exit(1) # Warnings only โ proceed if __name__ == "__main__": main()
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withcodex-seo
Codex-first SEO analysis suite with 1 orchestrator skill, 26 specialist workflows, 24 TOML agent profiles, MCP/API extensions, deterministic headless runners, and premium audit report generation.
Get the whole plugin
Stats
814
Stars
114
Forks
Maintained
Maintenance
Python
Language
1mo ago
Last commit
5mo ago
Created
5h ago
Added
Repo: agricidaniel/codex-seo

