aaron-marketing
Gives a marketing team an AI staff for planning and running campaigns across SEO, content, paid ads, email, influencer work and product launches. Each area comes with ready-made commands that work on data you supply.
What claude-seo runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Claude Code
> /plugin marketplace add AgriciDaniel/claude-seo> /plugin install claude-seo@agricidaniel-claude-seo
Cursor
Cursor also runs Claude Code hooks from .claude/settings.json, so copy these hooks there to use them in Cursor.
Where it lives
#!/usr/bin/env node
"use strict";
const { spawnSync } = require("child_process");
const path = require("path");
// Same list as valid_extensions in validate-schema.py. The hook fires on every
// Edit and Write, so skip the Python probe for files that cannot hold JSON-LD.
const SCHEMA_FILE_EXTENSIONS = [".html", ".htm", ".jsx", ".tsx", ".vue", ".svelte", ".php", ".ejs"];
// The skip belongs to the schema hook only; any other hook script always runs.
const SCHEMA_HOOK_SCRIPT = "validate-schema.py";
function isSchemaHook(hookScript) {
return path.basename(hookScript) === SCHEMA_HOOK_SCRIPT;
}
function cannotHoldSchema(filePath) {
// An unsubstituted "${tool_input.file_path}" leaves the path to the stdin event.
if (!filePath || filePath.includes("${")) {
return false;
}
const lower = filePath.toLowerCase();
return !SCHEMA_FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
}
function stripWrappingQuotes(value) {
return value.replace(/^["']|["']$/g, "");
}
function pythonCandidates() {
const candidates = [];
if (process.env.CLAUDE_SEO_PYTHON) {
candidates.push({
label: "CLAUDE_SEO_PYTHON",
exe: stripWrappingQuotes(process.env.CLAUDE_SEO_PYTHON),
args: [],
});
}
candidates.push(
{ label: "py -3", exe: "py", args: ["-3"] },
{ label: "python3", exe: "python3", args: [] },
{ label: "python", exe: "python", args: [] },
);
return candidates;
}
function isStoreStubOutput(text) {
return /Microsoft Store|WindowsApps|App execution alias|was not found/i.test(text);
}
function probe(candidate) {
const script = "import sys; print(sys.executable); print(sys.version.split()[0])";
const result = spawnSync(candidate.exe, [...candidate.args, "-c", script], {
encoding: "utf8",
});
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
return result.status === 0 && Boolean((result.stdout || "").trim()) && !isStoreStubOutput(output);
}
function main() {
const [, , hookScript, ...hookArgs] = process.argv;
if (!hookScript || (isSchemaHook(hookScript) && cannotHoldSchema(hookArgs[0]))) {
process.exit(0);
}
for (const candidate of pythonCandidates()) {
if (!probe(candidate)) {
continue;
}
const result = spawnSync(candidate.exe, [...candidate.args, hookScript, ...hookArgs], {
stdio: "inherit",
});
if (result.error) {
continue;
}
process.exit(result.status === null ? 1 : result.status);
}
console.error(
"Claude SEO hook could not find Python. Tried CLAUDE_SEO_PYTHON, py -3, python3, python.",
);
process.exit(1);
}
main();
#!/usr/bin/env python3
"""Post-edit schema validation hook for Claude Code.
Validates JSON-LD schema after file edits. The file is already written when
a PostToolUse hook runs, so nothing here can undo the edit; the hook makes
sure Claude reads what is wrong:
- critical errors (placeholders, deprecated or retired types): exit 2, with
the errors on stderr, which Claude Code feeds back to Claude;
- warnings only: exit 0, with the warnings as hookSpecificOutput
additionalContext JSON on stdout, which Claude reads without an error
notice (exit 1 would show them to the user only).
Hook configuration in ~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node",
"args": [
"${CLAUDE_PLUGIN_ROOT}/hooks/run-python-hook.js",
"${CLAUDE_PLUGIN_ROOT}/hooks/validate-schema.py",
"${tool_input.file_path}"
]
}
]
}
]
}
}
Note: matcher filters by tool name only (Edit, Write). run-python-hook.js
exits before starting Python when the edited file's extension is not in
valid_extensions below, and the script itself checks if the file contains
schema markup before validating.
"""
import json
import os
import re
import sys
from typing import Any, List
BRACKET_PLACEHOLDERS = (
"[Business Name]",
"[City]",
"[State]",
"[Phone]",
"[Address]",
"[Your",
"[INSERT",
"[URL]",
"[Email]",
)
BARE_PLACEHOLDER_RE = re.compile(r"\bREPLACE(?:_[A-Z]+)*\b")
# Match every <script ...>...</script> pair, then filter on the type attribute.
# The previous pattern required ``type`` to be the first and only attribute, so
# blocks carrying a CSP ``nonce``, an ``id`` or ``data-*`` attributes, or an
# unquoted type value were skipped without validation.
#
# The attribute group is a small tokenizer, not a plain ``[^>]*``: it consumes a
# double-quoted value, a single-quoted value, or a run of characters that is
# neither a quote nor ``>``. A ``[^>]*`` scan ends the tag at the first ``>`` it
# sees, quoted or not, so an attribute value containing ``>`` (``data-cond="a>b"``,
# a templated nonce) truncated the tag early and fed the remainder of the
# attributes plus the real body to the JSON parser as garbage. Treating a quoted
# span as atomic keeps an embedded ``>`` from ending the tag prematurely.
# The fallback class excludes both quote characters: if it matched an
# apostrophe, the alternation would be ambiguous and a tag with many
# apostrophes and no closing tag would backtrack exponentially, hanging the
# blocking hook.
_ATTRS_RE = r'(?:"[^"]*"|\'[^\']*\'|[^"\'>])*'
SCRIPT_TAG_RE = re.compile(
r"<script\b(" + _ATTRS_RE + r")>(.*?)</script\s*>", re.DOTALL | re.IGNORECASE
)
LD_JSON_TYPE_RE = re.compile(
r"""(?:^|\s)type\s*=\s*"""
r"""(?:"application/ld\+json"|'application/ld\+json'|application/ld\+json(?=\s|$))""",
re.IGNORECASE,
)
# Server- or client-side template expressions that render JSON-LD at runtime.
# The hook runs on .jsx/.tsx/.vue/.svelte/.php/.ejs sources, where the script
# body is frequently an expression rather than literal JSON. Those blocks cannot
# be validated statically and must not be reported as invalid JSON.
SERVER_TEMPLATE_RE = re.compile(
r"""^(?:
<\?(?:php\b|=) # <?php ... ?> / <?= ... ?>
| <% # EJS / ERB
)""",
re.VERBOSE,
)
COMPONENT_EXPRESSION_RE = re.compile(
r"""^(?:
\{\{ # Vue / Handlebars / Twig
| \{@html\b # Svelte
| \$\{ # JS template literal
| \{\s*[A-Za-z_$][\w$.]* # JSX expression: {schema} / {JSON.stringify(...)}
)""",
re.VERBOSE,
)
COMPONENT_EXTENSIONS = (".jsx", ".tsx", ".vue", ".svelte")
SCHEMA_ORG_CONTEXTS = frozenset(
{"https://schema.org", "http://schema.org", "https://schema.org/", "http://schema.org/"}
)
def _configure_utf8() -> None:
"""Keep hook diagnostics printable on legacy Windows console encodings."""
for stream in (sys.stdout, sys.stderr):
reconfigure = getattr(stream, "reconfigure", None)
if reconfigure:
reconfigure(encoding="utf-8", errors="replace")
def _extract_ld_json_blocks(content: str) -> List[str]:
"""Return the bodies of every ``<script type="application/ld+json">`` block.
Attribute order, extra attributes (``nonce``, ``id``, ``data-*``), tag case
and unquoted type values are all accepted; only the type value is decisive.
"""
blocks = []
for attributes, body in SCRIPT_TAG_RE.findall(content):
if LD_JSON_TYPE_RE.search(attributes):
blocks.append(body)
return blocks
def _is_template_expression(block: str, filepath: str = "") -> bool:
"""True when the script body is rendered at runtime rather than literal JSON.
Server-side markers (PHP, EJS) are never valid JSON and are skipped for
every file type. Component expressions (JSX, Vue, Svelte, template
literals) are only skipped in component sources, so a malformed object
literal in a plain ``.html`` file is still reported.
"""
if SERVER_TEMPLATE_RE.match(block):
return True
if filepath.lower().endswith(COMPONENT_EXTENSIONS):
return bool(COMPONENT_EXPRESSION_RE.match(block))
return False
def _is_schema_org_context(value: Any) -> bool:
"""Accept the schema.org context in its string, list and object forms."""
if isinstance(value, str):
return value in SCHEMA_ORG_CONTEXTS
if isinstance(value, list):
return any(_is_schema_org_context(item) for item in value)
if isinstance(value, dict):
return _is_schema_org_context(value.get("@vocab"))
return False
def validate_jsonld(content: str, filepath: str = "") -> List[str]:
"""Validate JSON-LD blocks in HTML content."""
errors = []
blocks = _exRead the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Universal SEO skill for Claude Code. 26 sub-skills + 19 sub-agents covering technical SEO, E-E-A-T, schema, GEO/AEO, agent readiness (Lighthouse Agentic Browsing, WebMCP, llms.txt), backlinks, local SEO, e-commerce, international SEO, Google APIs, and PDF/Excel reporting. 9 optional extensions for live SEO data.
Gives a marketing team an AI staff for planning and running campaigns across SEO, content, paid ads, email, influencer work and product launches. Each area comes with ready-made commands that work on data you supply.
Runs a consistent marketing strategy process across every brand in a portfolio, producing the same planning files in the same order so agencies and in-house teams can audit depth and catch compliance gaps. Covers SEO audits, ad copy, A/B test planning and AI search visibility.
Runs an autonomous loop for SaaS content marketing and copywriting, with commands to initialize, run, check status, and cancel it. A stop hook keeps the loop going when an agent stops.
Content Repurposing Engine for Claude Code, Turn 1 piece of content into 10+ platform-optimized posts in seconds.
Scrapes tracked competitor accounts on X, Instagram, TikTok and YouTube, scores what is performing well, and turns it into a daily feed of content ideas framed against your own brand.
Professional email management, deliverability optimization, and marketing strategy powered by Claude Code.
The SEO + AI-search (GEO/AEO) optimization toolkit for Claude Code, two-score audit + opt-in fixer. Built for 2026-2027.
A free Claude Code toolkit for SEO, AI answer engine optimization, and generative engine optimization. Its skills cover AI visibility checks, broken link audits, competitor scorecards, content briefs, and content strategy plans.