SEO Content Analyzer Pro
Analyze SEO article drafts with quantitative metrics and expert marketing feedback. Computes word/sentence stats and top 5 frequent words (ignoring stopwords), then presents a markdown table followed by concise, actionable recommendations.
Preview Mode
This is a preview with sample data. The template uses placeholders like
which will be replaced with actual agent data.
About This Template
SEO Content Analyzer Pro is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.
Template Metadata
- Slug
- seo-content-analyzer-pro
- Created By
- ozzo
- Created
- Jul 03, 2026
- Usage Count
- 1
Tags
Code Statistics
- HTML Lines
- 56
- CSS Lines
- 79
- JS Lines
- 90
- Python Lines
- 160
Source Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ agent_name }}</title>
<style>
{{ css_code }}
</style>
</head>
<body>
<header class="app-header">
<div class="brand">
<div class="logo">📈</div>
<div class="titles">
<h1 class="app-title">{{ agent_name }}</h1>
<p class="app-desc">{{ description }}</p>
</div>
</div>
</header>
<main class="layout">
<section class="panel input-panel">
<h2>Article Draft</h2>
<textarea id="draft-input" placeholder="Paste your article draft to analyze SEO performance, readability, and word usage..."></textarea>
<div class="actions">
<button id="analyze-btn" class="primary">Analyze Draft</button>
<span id="status-text" class="status"></span>
</div>
<div class="empty-hint">
<p>Tips for best results:</p>
<ul>
<li>Paste your full SEO article draft (intro, headings, body).</li>
<li>Include headings (H1–H3) and any call-to-action lines.</li>
<li>Provide enough text for accurate sentence/word counts.</li>
<li>Use when refining tone, structure, and on-page SEO.</li>
</ul>
</div>
</section>
<section class="panel output-panel">
<h2>Analysis & Feedback</h2>
<div id="results-container" class="results"></div>
</section>
</main>
<div id="loading-overlay" class="overlay hidden">
<div class="spinner"></div>
<div class="loading-text">Analyzing your SEO draft…</div>
</div>
<script>
{{ js_code }}
</script>
</body>
</html>
:root {
--bg: #0b1020;
--panel: #111735;
--panel-2: #0f1530;
--text: #e9ecff;
--muted: #a7b0d6;
--accent: #6aa1ff;
--accent-2: #7cd6cf;
--danger: #ff6b6b;
--ring: #9cc0ff;
--radius: 12px;
--shadow: 0 8px 24px rgba(0,0,0,0.35);
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0; background: linear-gradient(180deg, #0b1020 0%, #0a0f24 100%);
color: var(--text); font-family: var(--sans); line-height: 1.45;
}
.app-header { padding: 16px 20px; border-bottom: 1px solid rgba(255,255,255,0.06); position: sticky; top: 0; background: rgba(10,15,36,0.8); backdrop-filter: blur(8px); z-index: 2; }
.brand { display: flex; align-items: center; gap: 14px; }
.logo { width: 40px; height: 40px; border-radius: 10px; display:flex; align-items:center; justify-content:center; background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%); box-shadow: var(--shadow); font-size: 22px; }
.titles { display: flex; flex-direction: column; gap: 2px; }
.app-title { margin: 0; font-size: 20px; letter-spacing: 0.2px; }
.app-desc { margin: 0; color: var(--muted); font-size: 13px; }
.layout { display: grid; grid-template-columns: 1.1fr 1.4fr; gap: 16px; padding: 16px; }
.panel { background: var(--panel); border: 1px solid rgba(255,255,255,0.06); border-radius: var(--radius); box-shadow: var(--shadow); padding: 16px; min-height: 300px; }
.panel h2 { margin: 0 0 12px; font-size: 16px; color: #dbe5ff; letter-spacing: 0.2px; }
.input-panel textarea {
width: 100%; min-height: 340px; resize: vertical; padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08);
background: var(--panel-2); color: var(--text); outline: none; box-shadow: inset 0 1px 0 rgba(255,255,255,0.04);
font-family: var(--sans); line-height: 1.5; font-size: 14px;
}
.input-panel textarea:focus { border-color: var(--ring); box-shadow: 0 0 0 3px rgba(156,192,255,0.2); }
.actions { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
button.primary {
background: linear-gradient(135deg, var(--accent) 0%, #4d86ff 100%);
border: none; color: #0a0f24; font-weight: 700; letter-spacing: 0.3px;
padding: 10px 14px; border-radius: 10px; cursor: pointer; transition: transform 0.05s ease, opacity 0.2s; box-shadow: var(--shadow);
}
button.primary:hover { opacity: 0.95; }
button.primary:active { transform: translateY(1px); }
button.primary:disabled { opacity: 0.6; cursor: not-allowed; }
.status { color: var(--muted); font-size: 12px; }
.empty-hint { margin-top: 12px; color: var(--muted); font-size: 13px; }
.empty-hint ul { margin: 8px 0 0 18px; padding: 0; }
.results { background: var(--panel-2); border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; min-height: 360px; padding: 12px; overflow-y: auto; }
.msg { padding: 10px 12px; border-radius: 10px; margin: 8px 0; white-space: pre-wrap; word-break: break-word; }
.msg.user { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.06); }
.msg.assistant { background: rgba(108,161,255,0.08); border: 1px solid rgba(108,161,255,0.18); }
.msg pre { margin: 0; font-family: var(--mono); font-size: 13px; }
.overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; gap: 12px; background: rgba(8,12,28,0.55); z-index: 5; }
.hidden { display: none !important; }
.spinner { width: 28px; height: 28px; border: 3px solid rgba(255,255,255,0.25); border-top-color: var(--accent); border-radius: 50%; animation: spin 1s linear infinite; }
.loading-text { color: var(--text); font-weight: 600; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Responsive */
@media (max-width: 1200px) {
.layout { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.app-header { position: static; }
.input-panel textarea { min-height: 260px; }
}
/* Focus-visible */
button:focus-visible, textarea:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
(function(){
const results = document.getElementById('results-container');
const draftInput = document.getElementById('draft-input');
const analyzeBtn = document.getElementById('analyze-btn');
const statusText = document.getElementById('status-text');
const overlay = document.getElementById('loading-overlay');
function escapeHtml(str){
if(str == null) return '';
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function setProcessing(on){
if(on){ overlay.classList.remove('hidden'); analyzeBtn.disabled = true; }
else { overlay.classList.add('hidden'); analyzeBtn.disabled = false; }
}
function setStatus(msg){ statusText.textContent = msg || ''; }
function ensureLocalModelReady(){
try{
if (window.PROVIDER && typeof window.PROVIDER === 'string' && window.PROVIDER.toLowerCase() === 'local'){
if (!window.agentManager || !window.agentManager.isLoaded){
setStatus('Load a local model using the selector bar above to run analysis.');
return false;
}
}
}catch(e){}
return true;
}
function addMessage(type, content){
const wrap = document.createElement('div');
wrap.className = 'msg ' + (type || 'assistant');
const pre = document.createElement('pre');
pre.textContent = content || '';
wrap.appendChild(pre);
results.appendChild(wrap);
results.scrollTop = results.scrollHeight;
}
window.addMessage = addMessage;
async function runAnalysis(){
setStatus('');
const text = draftInput.value.trim();
if(!text){
setStatus('Please paste an article draft to analyze.');
return;
}
if(!ensureLocalModelReady()) return;
try{
setProcessing(true);
addMessage('user', text.length > 1200 ? (text.slice(0,1200) + "\n\n…(truncated)…") : text);
await window.pyodide.globals.set('user_query', text);
const py = 'await process_user_query(user_query)';
const out = await window.pyodide.runPythonAsync(py);
addMessage('assistant', out);
setStatus('');
} catch(err){
console.error(err);
const friendly = (err && err.message) ? err.message : String(err);
addMessage('assistant', 'Error: ' + friendly + '\n\nCheck your API key or load a local model, then try again.');
setStatus('Encountered an error.');
} finally {
setProcessing(false);
}
}
function init(){
analyzeBtn.addEventListener('click', runAnalysis);
draftInput.addEventListener('keydown', (e)=>{
if((e.metaKey || e.ctrlKey) && e.key === 'Enter') runAnalysis();
});
setStatus('Ready. Paste your SEO draft and click Analyze.');
}
function readyCheck(){
if(window.pyodide && window.pyodideReady){ init(); return; }
}
document.addEventListener('pyodide-ready', init);
if(document.readyState !== 'loading') readyCheck();
else document.addEventListener('DOMContentLoaded', readyCheck);
})();
# SEO Content Analyzer Pro - Python Tools and Orchestration
# Public tool functions are exposed automatically. Keep helpers private.
from typing import Dict, List, Tuple
import re
from collections import Counter
# Max characters of the user draft to include in prompts (protect small local models)
MAX_CHARS: int = [[[MAX_CHARS|6000]]]
# A compact English stopword set for SEO analysis; extend as needed.
STOPWORDS = {
'the','is','and','a','an','to','of','in','for','on','with','as','by','at','be','this','that','it','from','or','are',
'was','were','but','not','your','you','we','our','their','they','have','has','had','can','could','will','would','should',
'may','might','about','into','over','than','then','so','if','when','while','also','more','most','such','which','what',
'who','whom','how','why','where','there','here','these','those','he','she','his','her','its','them','i','me','my','mine',
'yours','theirs','do','does','did','done','up','down','out','only','just','very','any','all','no','nor','too','because',
'been','between','both','each','few','other','some','much','many','per','via','use','used','using','s','t','ll','re',
'd','m','o','ve',' ','','','–','—','…'
}
# ---------------------- TOOL FUNCTIONS (PUBLIC) ----------------------
def compute_text_stats(text: str) -> Dict[str, float]:
"""Compute basic text statistics for an article draft.
Args:
text: The full article draft to analyze.
Returns:
A dict with keys:
- word_count: Total number of words in the draft.
- sentence_count: Total number of sentences (approximation using punctuation).
- avg_words_per_sentence: Average words per sentence (0.0 if no sentences).
"""
if not isinstance(text, str):
raise TypeError("text must be a string")
# Tokenize words: keep alphanumerics and apostrophes (for contractions)
words = re.findall(r"[A-Za-z0-9']+", text)
word_count = len(words)
# Split sentences on ., !, ? while handling multi-punctuation and newlines
raw_sentences = re.split(r"[.!?]+|\n+", text)
sentences = [s.strip() for s in raw_sentences if s and s.strip()]
sentence_count = len(sentences)
avg_words = float(word_count) / sentence_count if sentence_count else 0.0
return {
"word_count": int(word_count),
"sentence_count": int(sentence_count),
"avg_words_per_sentence": round(avg_words, 2),
}
def top_frequent_words(text: str, top_n: int = 5) -> List[Tuple[str, int]]:
"""Extract the top-N most frequent non-stopwords from the draft.
Args:
text: The full article draft to analyze.
top_n: Number of top words to return (default 5).
Returns:
A list of (word, count) tuples sorted by frequency desc, then alphabetically.
Common stopwords like 'the', 'is', and 'and' are ignored.
"""
if not isinstance(text, str):
raise TypeError("text must be a string")
if not isinstance(top_n, int) or top_n <= 0:
raise ValueError("top_n must be a positive integer")
tokens = [w.lower() for w in re.findall(r"[A-Za-z0-9']+", text)]
# Filter stopwords and numeric-only tokens
filtered = [t for t in tokens if t not in STOPWORDS and not t.isdigit() and len(t) > 1]
freq = Counter(filtered)
# Sort by count desc, word asc for stability
ranked = sorted(freq.items(), key=lambda x: (-x[1], x[0]))
return ranked[:top_n]
# ---------------------- HELPERS (PRIVATE) ----------------------
def _truncate_text(text: str, max_chars: int = MAX_CHARS) -> str:
text = text or ""
if len(text) <= max_chars:
return text
return text[:max_chars]
def _render_metrics_table(stats: Dict[str, float], top_words: List[Tuple[str, int]]) -> str:
# Build a neat Markdown table as required.
tw_inline = ", ".join([f"`{w}` ({c})" for w, c in top_words]) if top_words else "—"
lines = [
"| Metric | Value |",
"|---|---|",
f"| Word Count | {stats.get('word_count', 0)} |",
f"| Sentence Count | {stats.get('sentence_count', 0)} |",
f"| Avg Words per Sentence | {stats.get('avg_words_per_sentence', 0.0)} |",
f"| Top 5 Words | {tw_inline} |",
]
return "\n".join(lines)
async def _llm_complete(system_prompt: str, user_prompt: str) -> str:
# Use the platform-provided JS helper
from js import callLLM
try:
result = await callLLM(user_prompt, system_prompt)
except Exception as e:
raise RuntimeError(f"LLM call failed: {e}")
return str(result)
# ---------------------- MAIN ORCHESTRATION ----------------------
async def process_user_query(query: str) -> str:
"""Process the user's article draft and return analysis output.
Args:
query: The article draft text to analyze.
Returns:
A string beginning with a markdown table of metrics, followed by expert recommendations.
"""
draft = (query or "").strip()
if not draft:
return "Please paste an article draft to analyze."
# Compute metrics via tools
stats = compute_text_stats(draft)
top5 = top_frequent_words(draft, top_n=5)
table_md = _render_metrics_table(stats, top5)
# Prepare prompts (trim draft for small models)
trimmed = _truncate_text(draft, MAX_CHARS)
system_prompt = (
"You are a professional SEO content marketing expert. Be concise and practical. "
"Write clear, high-impact recommendations. Avoid fluff. Do not output code blocks."
)
user_prompt = (
"Context: I am optimizing an SEO article draft.\n"
"You are given the draft text and some computed metrics.\n\n"
f"Draft (trimmed to {MAX_CHARS} chars if long):\n" + trimmed + "\n\n"
"Metrics provided separately (already shown to the user).\n"
"Instructions:\n"
"- Do NOT repeat the metrics table.\n"
"- Provide actionable, bullet-point feedback under 'Insights & Recommendations'.\n"
"- Cover: readability & clarity, structure/headings, on-page SEO (keywords without stuffing), suggestions for internal/external links, CTA clarity.\n"
"- If helpful, propose 2 improved H1 title options and 1 meta description (<=160 chars).\n"
"- Keep it concise: 6-10 bullets total.\n"
)
advice = await _llm_complete(system_prompt, user_prompt)
# Always present the table first, then the advice
final_out = table_md + "\n\n" + advice.strip()
return final_out
Name your agent
Based on . You'll get your own private copy to try and customize.