Content Optimizer
[To Build] ·
content-scorerservice + blog editor sidebar · Claude Sonnet 4.6 (AI modes only)
Analyses a blog post draft across four dimensions — SEO, readability, brand voice, and AI search visibility — and surfaces per-issue guided fixes directly in the blog editor. Also exposes in-editor AI actions (Rewrite, Simplify, Expand, Fix Tone) that invoke the blog-writer agent with a targeted mode flag.
Related: Blog Writer · RAG Integration · Credits · Content Toolkit Overview
Overview
| Function | Score a blog post draft on four quality dimensions and surface actionable guided fixes |
| Type | Service — Content Quality |
| Status | To Build |
| Priority | P1 — Table-Stakes |
| Triggered by | Blog post save (auto), or manual “Analyse” button |
| Score modes | Heuristic (SEO + Readability): 0 credits · AI-powered (Brand Voice + AI Search): 0.25 cr |
| Plan | Free+ (heuristic) · Pro+ (AI modes) |
Why This Is Needed
Content is generated by the blog-writer agent but there is currently no quality signal after generation. A DM reviewer approving a post cannot tell whether:
- The primary keyword is used at the right density
- The article is too complex for the target audience
- The tone matches the client’s brand voice documents
- The structure is suitable for citation in ChatGPT or Perplexity results
The Content Optimizer closes this gap by scoring output immediately after generation and surfacing specific, actionable improvements — rather than requiring the reviewer to manually check everything.
The Four Score Dimensions
1. SEO Score (0–100) — Heuristic, free
Evaluates on-page SEO signals using rule-based analysis, no LLM call required.
| Signal | What is checked | Weight |
|---|---|---|
| Primary keyword in title | Exact or partial match | High |
| Primary keyword in H1 | Exact or partial match | High |
| Primary keyword density | 0.5–2.5% of total words | Medium |
| Primary keyword in meta description | Present | High |
| Secondary keyword coverage | At least 60% of secondary keywords appear in body | Medium |
| Heading structure | H2s present, no skipped levels (H1 → H3 without H2) | Medium |
| Internal links | At least 2 internal link targets used | Medium |
| Meta description length | 140–160 characters | Low |
| Word count vs target | Within ±15% of brief’s targetWordCount | Medium |
| Image alt text | At least 1 image with non-empty alt text | Low |
2. Readability Score (0–100) — Heuristic, free
Evaluated using Flesch-Kincaid Reading Ease adapted per language.
| Signal | What is checked |
|---|---|
| Avg sentence length | Target ≤ 20 words |
| Avg paragraph length | Target ≤ 5 sentences |
| Passive voice ratio | Target < 15% of sentences |
| Complex word ratio | Words with ≥ 3 syllables; target < 20% |
| Transition word ratio | Sentences beginning with a transition; target > 30% |
| Flesch-Kincaid grade level | Target: Grade 8–10 for general audiences |
3. Brand Voice Score (0–100) — AI-powered, 0.25 cr
Compares the post’s tone and style against the tenant’s brand voice documents stored in the client_docs RAG dataset.
- Fetches the tenant’s brand voice / tone-of-voice documents via RAG
- Sends a structured excerpt (first 800 words of article + brand voice doc) to Claude Haiku
- Claude returns a score (0–100) and 3–5 specific observations (e.g. “Brand voice requires conversational tone; this section uses formal register”)
- Falls back to a neutral score with a “No brand voice document found” warning if the RAG dataset has no relevant document
4. AI Search Visibility Score (0–100) — AI-powered, 0.25 cr
Scores how well the post is structured to be cited or surfaced in LLM-driven search (ChatGPT, Perplexity, Google AI Overviews).
| Signal | What is checked |
|---|---|
| Direct answer block | Does the intro answer the primary question within the first 150 words? |
| Definition present | Does the post define the primary keyword or core concept explicitly? |
| FAQ section | Is there a structured FAQ block? |
| Comparison table | Does the post include at least one comparison or feature table? |
| Factual claims sourced | Are statistics cited with a source (external link or inline attribution)? |
| Structured headings | Do H2s read as questions or clear topic titles? |
| Completeness | Does the post cover the topic end-to-end without thin sections (<200 words)? |
Composite Score
The overall Content Health score is a weighted average of the four dimension scores.
| Dimension | Weight |
|---|---|
| SEO Score | 35% |
| Readability Score | 25% |
| Brand Voice Score | 20% |
| AI Search Visibility Score | 20% |
The composite score is shown as a single number (0–100) with a colour indicator: red (0–49), amber (50–74), green (75–100).
Output Contract
interface ContentScoreResult {
blogPostId: string;
scoredAt: string; // ISO timestamp
overallScore: number; // 0–100 weighted composite
seo: {
score: number;
fixes: GuidedFix[];
};
readability: {
score: number;
fixes: GuidedFix[];
};
brandVoice: {
score: number;
observations: string[]; // 3–5 Claude-generated observations
skipped: boolean; // true if no brand voice doc found in RAG
};
aiSearch: {
score: number;
fixes: GuidedFix[];
};
}
interface GuidedFix {
category: 'seo' | 'readability' | 'brand_voice' | 'ai_search';
severity: 'critical' | 'warning' | 'suggestion';
title: string; // e.g. "Primary keyword missing from meta description"
description: string; // specific, actionable — e.g. "Add 'email marketing automation' to the meta description (currently 178 chars; trim to 155)"
suggestedAction: string; // label for the action button — e.g. "Fix meta description"
targetSection?: string; // optional — section heading where the fix applies
}How It Works
Blog post saved (auto-save or manual save)
↓
API: POST /tenant/v1/blog/:id/score
↓
content-scorer service runs heuristic pass (SEO + Readability)
↓ (synchronous — returns in < 300ms)
If plan is Pro+ and AI scores are enabled:
↓
Enqueue two lightweight jobs:
- brand-voice-scorer (Claude Haiku — fetches RAG docs + evaluates tone)
- ai-search-scorer (Claude Haiku — evaluates structure signals)
↓ (async — results streamed back to editor via WebSocket)
ContentScoreResult stored on BlogPost.scoreResult (Json column)
↓
Dashboard blog editor sidebar updates with score panel + guided fixes listIn-Editor AI Actions
Four AI actions are available in the blog editor toolbar, each triggering the blog-writer agent in a targeted mode:
| Action | Mode flag | What it does | Credit cost |
|---|---|---|---|
| Rewrite section | rewrite_section | Rewrites the selected paragraph with the same meaning but improved phrasing | 0.1 cr |
| Simplify | simplify_section | Reduces sentence complexity in the selection; targets Grade 8 reading level | 0.1 cr |
| Expand | expand_section | Adds 100–200 words to the selection, drawing from the original brief and research notes | 0.1 cr |
| Fix Tone | fix_tone | Rewrites selection to align with the brand voice score observations | 0.1 cr |
Each action posts the selected text + mode + blogPostId to POST /tenant/v1/blog/:id/ai-action. The API enqueues a targeted blog-writer job with the mode flag set and streams the response back to the editor.
Dashboard UI
Score Panel (blog editor right sidebar)
- Composite score gauge (0–100, colour-coded)
- Four dimension score bars (SEO, Readability, Brand Voice, AI Search)
- Guided fixes list, sorted by severity (critical → warning → suggestion)
- Each fix has an action button that either opens the relevant section in the editor or triggers the corresponding AI action
- “Re-analyse” button for manual refresh after edits
- Last scored timestamp
Score Badge (blog list view)
- Small coloured dot (red / amber / green) next to each blog post title, showing the composite score
- Tooltip shows the breakdown on hover
DB Changes
Add to the BlogPost model in packages/db/prisma/schema.prisma:
scoreResult Json? // ContentScoreResult stored as JSON
scoredAt DateTime?
scoreVersion Int? // incremented on each re-score; for cache invalidationKey Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Heuristic vs LLM scoring | Heuristic for SEO + Readability; LLM only for Brand Voice + AI Search | Keeps 0 credits for the most-used scores; LLM needed only where rule-based analysis is insufficient |
| Score trigger | Auto on save, plus manual button | Auto keeps the panel fresh; manual button lets DM reviewer run a re-score after edits without waiting for another save |
| Model for AI scores | Claude Haiku (not Sonnet) | Short, focused evaluation task; Haiku is 10× cheaper and fast enough |
| Score storage | Stored on BlogPost as JSON | Avoids a separate scoring table; score is always tied to the post version |
| Guided fix specificity | Fixes must name the exact issue and exact remedy, not generic advice | ”Add primary keyword to meta description” is actionable; “Improve SEO” is not |
Implementation Phases
Phase 1 — DB + Heuristic Scorer
- Add
scoreResult,scoredAt,scoreVersioncolumns toBlogPost(migration) - Implement
content-scorer.service.tsinapps/api/src/services/— heuristic SEO + readability pass, returnsContentScoreResultwith empty AI sections - Add
POST /tenant/v1/blog/:id/scoreroute — runs heuristic scorer synchronously, stores result, returnsContentScoreResult - Blog editor sidebar: score panel component showing SEO + Readability scores + guided fixes list
Phase 2 — AI Scores
- Add
brand-voice-scorerandai-search-scorerBullMQ workers - Extend
POST /tenant/v1/blog/:id/scoreto enqueue AI scorer jobs when plan is Pro+ - WebSocket event
blog:score_updatedemitted when AI jobs complete — editor sidebar updates in real time - Credit deduction: 0.25 cr per AI score pair
Phase 3 — In-Editor AI Actions
- Add
POST /tenant/v1/blog/:id/ai-actionroute - Extend
blog-writeragent to handle targeted mode flags (rewrite_section,simplify_section,expand_section,fix_tone) - In-editor toolbar buttons + text selection handler
- Stream AI action response back to editor and replace selected text on accept
Phase 4 — Score Badge in List View
- Include
scoreResult.overallScorein blog list API response - Add colour-coded score badge to blog list page