Estructura del proyecto
cultiva-seo-scout/
├─entrypoints/
│ ├─popup/— Popup al hacer clic en el icono
│ │ ├─index.html
│ │ ├─main.tsx
│ │ └─App.tsx— UI principal con scores
│ ├─content.ts— Analiza DOM de cada página
│ └─background.ts— Service worker / mensajes
├─public/icon/
│ ├─16.png 48.png 128.png
├─wxt.config.ts
├─package.json
└─tsconfig.json
Content Script
entrypoints/content.ts
· Analiza el DOM de cualquier página
// Runs on every page — analyzes on-page SEO in real time export default defineContentScript({ matches: ["<all_urls>"], main() { // Listen for analysis requests from popup browser.runtime.onMessage.addListener((msg) => { if (msg.type === "ANALYZE_PAGE") { return Promise.resolve(analyzePage()); } }); }, }); function analyzePage(): SEOReport { const title = document.title; const metaDesc = document .querySelector('meta[name="description"]') ?.getAttribute("content") ?? ""; const h1s = [...document.querySelectorAll("h1")] .map(h => h.textContent?.trim() ?? ""); const h2s = document.querySelectorAll("h2").length; const h3s = document.querySelectorAll("h3").length; const imgs = [...document.querySelectorAll("img")]; const imgsNoAlt = imgs.filter(i => !i.alt).length; const canonical = document .querySelector('link[rel="canonical"]') ?.getAttribute("href") ?? ""; const bodyText = document.body.innerText ?? ""; const wordCount = bodyText.split(/\s+/).filter(Boolean).length; const keywords = extractTopKeywords(bodyText, 8); const score = calcScore({ title, metaDesc, h1s, imgsNoAlt, wordCount }); return { title, metaDesc, h1s, h2s, h3s, imgTotal: imgs.length, imgsNoAlt, wordCount, canonical, keywords, score, url: location.href, }; }
Background Service Worker
entrypoints/background.ts
· Coordina mensajes y badge
export default defineBackground(() => { // Relay messages from popup → active tab content script browser.runtime.onMessage.addListener( async (msg, _sender, sendResponse) => { if (msg.type === "GET_SEO_REPORT") { const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); if (!tab.id) return; const report = await browser.tabs.sendMessage( tab.id, { type: "ANALYZE_PAGE" } ); // Update badge with SEO score browser.action.setBadgeText({ text: String(report.score), tabId: tab.id, }); browser.action.setBadgeBackgroundColor({ color: report.score >= 70 ? "#4ade80" : report.score >= 40 ? "#fb923c" : "#f87171", tabId: tab.id, }); sendResponse(report); } } ); });
Popup UI (React)
entrypoints/popup/App.tsx
· Interfaz 300px con score + checks + keywords
export default function App() { const [report, setReport] = useState<SEOReport | null>(null); const [loading, setLoading] = useState(true); useEffect(async () => { const data = await browser.runtime.sendMessage({ type: "GET_SEO_REPORT", }); setReport(data); setLoading(false); }, []); const copyReport = () => { if (!report) return; navigator.clipboard.writeText( formatMarkdownReport(report) ); }; if (loading) return <Spinner />; return ( <div className="popup"> <ScoreRing score={report!.score} /> <CheckList report={report!} /> <KeywordCloud keywords={report!.keywords} /> <button onClick={copyReport}> Copiar informe </button> </div> ); }
Comandos de desarrollo y build
Inicializar
npx wxt@latest init
Scaffolding interactivo con React + TypeScript
Dev Chrome (HMR)
npm run dev
Abre Chrome con hot reload automático
Dev Firefox
npm run dev:firefox
Firefox con extensión temporal cargada
Build MV3
npm run build
Chrome Web Store — Manifest V3
Build Firefox
npm run build:firefox
Firefox Add-ons — MV2 compatible
ZIP para tiendas
npm run zip
Empaqueta y firma para Chrome + Firefox
Arquitectura de mensajería
Popup (React)
→
sendMessage GET_SEO_REPORT
→
Background SW
↓
tabs.sendMessage ANALYZE_PAGE
Content Script
←→
DOM Página
Compatibilidad de navegadores
| Navegador | Manifest | Hot Reload | Storage API | Tienda |
|---|---|---|---|---|
| Chrome / Chromium | MV3 | Soportado | Soportado | Chrome Web Store |
| Firefox | MV2 / MV3 | Soportado | Soportado | Firefox Add-ons |
| Edge | MV3 | Soportado | Soportado | Edge Add-ons |
| Safari | MV3 | ⚠ Parcial | Soportado | Mac App Store |
Vista previa del popup
🌿
CultivaSEO Scout
cliente-tienda.com/colecciones/cafeteras
65
SEO On-Page: Mejorable
3 problemas detectados. Meta description demasiado corta y 2 imágenes sin alt text.
Título de página
"Cafeteras de Especialidad — Tienda Online" (48 car.)
Meta description
"Las mejores cafeteras." (22 car. — mínimo 70)
H1 único
"Cafeteras de especialidad para casa y oficina"
Estructura H2 / H3
7 × H2 · 12 × H3 — jerarquía correcta
Imágenes sin alt text
2 de 9 imágenes no tienen atributo alt
Canonical URL
cliente-tienda.com/colecciones/cafeteras
Contenido
1.240 palabras — densidad adecuada
Keywords detectadas
Informe exportado (Markdown)
## SEO Report — cliente-tienda.com **Score:** 65/100 ⚠️ Mejorable **URL:** /colecciones/cafeteras **Fecha:** 2026-06-16 ### Checks ✅ Título: OK (48 car.) ⚠️ Meta desc: muy corta (22 car.) ✅ H1: presente y único ✅ H2/H3: 7/12 — OK ❌ Imgs sin alt: 2/9 ✅ Canonical: OK ✅ Palabras: 1.240 ### Top Keywords cafetera especialidad · café de origen espresso · molinillo café · filtro v60