diff --git a/scripts/seo-audit.mjs b/scripts/seo-audit.mjs
new file mode 100644
index 0000000..dec18ca
--- /dev/null
+++ b/scripts/seo-audit.mjs
@@ -0,0 +1,246 @@
+#!/usr/bin/env node
+/**
+ * Auditoria repetível de SEO / AEO / GEO, sem dependências (Node >= 20).
+ *
+ * node scripts/seo-audit.mjs --base http://localhost:3000 --out audit-antes.json
+ * node scripts/seo-compare.mjs audit-antes.json audit-depois.json
+ *
+ * Lê o sitemap, baixa cada URL como um robô (HTML sem JavaScript) e confere o que buscadores e sistemas de
+ * resposta/IA realmente enxergam. Não inventa nota: cada verificação é passa/falha com a evidência.
+ * O que depende de conta (Search Console, Bing, CrUX, citações em plataformas de IA) está em docs/seo-geo-operations.md.
+ */
+import { writeFileSync } from "node:fs";
+
+const args = Object.fromEntries(
+ process.argv
+ .slice(2)
+ .map((a, i, all) => (a.startsWith("--") ? [a.slice(2), all[i + 1]] : null))
+ .filter(Boolean),
+);
+const BASE = (args.base ?? "https://evandro.dev.br").replace(/\/$/, "");
+const SITE = "https://evandro.dev.br"; // origem canônica declarada no código
+const UA = "Mozilla/5.0 (compatible; seo-audit/1.0)";
+const AI_BOTS = [
+ ["Googlebot", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"],
+ ["bingbot", "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"],
+ ["OAI-SearchBot", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.0; +https://openai.com/searchbot"],
+ ["GPTBot", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.1; +https://openai.com/gptbot)"],
+ ["ChatGPT-User", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ChatGPT-User/1.0; +https://openai.com/bot)"],
+ ["Claude-SearchBot", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-SearchBot/1.0; +searchbot@anthropic.com)"],
+ ["ClaudeBot", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)"],
+ ["PerplexityBot", "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)"],
+];
+
+const local = (url) => url.replace(SITE, BASE); // permite auditar localhost com URLs canônicas de produção
+const get = async (url, ua = UA, init = {}) => {
+ const t0 = performance.now();
+ const res = await fetch(url, { headers: { "user-agent": ua, "accept-encoding": "gzip, br" }, redirect: "manual", ...init });
+ const body = init.method === "HEAD" ? "" : await res.text();
+ return { res, body, ms: Math.round(performance.now() - t0) };
+};
+
+const attrs = (tag) => {
+ const out = {};
+ for (const m of tag.matchAll(/([a-zA-Z_:][\w:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) out[m[1].toLowerCase()] = m[2] ?? m[3] ?? "";
+ return out;
+};
+const decode = (s) => s.replace(/&/g, "&").replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, "<").replace(/>/g, ">").trim();
+const strip = (html) =>
+ decode(
+ html
+ .replace(/<(script|style|noscript|template|svg)[\s\S]*?<\/\1>/gi, " ")
+ .replace(//g, " ")
+ .replace(/<[^>]+>/g, " ")
+ .replace(/\s+/g, " "),
+ );
+const words = (s) => (s.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) ?? []).length;
+
+function parsePage(html) {
+ const metas = [...html.matchAll(/]*>/gi)].map((m) => attrs(m[0]));
+ const meta = (key, val = "name") => metas.find((m) => m[val]?.toLowerCase() === key)?.content;
+ const links = [...html.matchAll(/]*>/gi)].map((m) => attrs(m[0]));
+ const headings = [...html.matchAll(/]*>([\s\S]*?)<\/h\1>/gi)].map((m) => ({ level: +m[1], text: strip(m[2]) }));
+ const imgs = [...html.matchAll(/
]*>/gi)].map((m) => attrs(m[0]));
+ const anchors = [...html.matchAll(/]*>/gi)].map((m) => attrs(m[0]));
+ const ld = [...html.matchAll(/