Agent Template

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.

SEO content marketing editing analysis
ozzo Jul 03, 2026 1 use

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.

Topics SEO content marketing editing analysis
Template Preview

Template Metadata

Slug
seo-content-analyzer-pro
Created By
ozzo
Created
Jul 03, 2026
Usage Count
1

Tags

SEO content marketing editing analysis

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, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  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