Skip to Content
Content ToolkitContent Optimizer

Content Optimizer

[To Build] · content-scorer service + 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

FunctionScore a blog post draft on four quality dimensions and surface actionable guided fixes
TypeService — Content Quality
StatusTo Build
PriorityP1 — Table-Stakes
Triggered byBlog post save (auto), or manual “Analyse” button
Score modesHeuristic (SEO + Readability): 0 credits · AI-powered (Brand Voice + AI Search): 0.25 cr
PlanFree+ (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.

SignalWhat is checkedWeight
Primary keyword in titleExact or partial matchHigh
Primary keyword in H1Exact or partial matchHigh
Primary keyword density0.5–2.5% of total wordsMedium
Primary keyword in meta descriptionPresentHigh
Secondary keyword coverageAt least 60% of secondary keywords appear in bodyMedium
Heading structureH2s present, no skipped levels (H1 → H3 without H2)Medium
Internal linksAt least 2 internal link targets usedMedium
Meta description length140–160 charactersLow
Word count vs targetWithin ±15% of brief’s targetWordCountMedium
Image alt textAt least 1 image with non-empty alt textLow

2. Readability Score (0–100) — Heuristic, free

Evaluated using Flesch-Kincaid Reading Ease adapted per language.

SignalWhat is checked
Avg sentence lengthTarget ≤ 20 words
Avg paragraph lengthTarget ≤ 5 sentences
Passive voice ratioTarget < 15% of sentences
Complex word ratioWords with ≥ 3 syllables; target < 20%
Transition word ratioSentences beginning with a transition; target > 30%
Flesch-Kincaid grade levelTarget: 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).

SignalWhat is checked
Direct answer blockDoes the intro answer the primary question within the first 150 words?
Definition presentDoes the post define the primary keyword or core concept explicitly?
FAQ sectionIs there a structured FAQ block?
Comparison tableDoes the post include at least one comparison or feature table?
Factual claims sourcedAre statistics cited with a source (external link or inline attribution)?
Structured headingsDo H2s read as questions or clear topic titles?
CompletenessDoes 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.

DimensionWeight
SEO Score35%
Readability Score25%
Brand Voice Score20%
AI Search Visibility Score20%

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 list

In-Editor AI Actions

Four AI actions are available in the blog editor toolbar, each triggering the blog-writer agent in a targeted mode:

ActionMode flagWhat it doesCredit cost
Rewrite sectionrewrite_sectionRewrites the selected paragraph with the same meaning but improved phrasing0.1 cr
Simplifysimplify_sectionReduces sentence complexity in the selection; targets Grade 8 reading level0.1 cr
Expandexpand_sectionAdds 100–200 words to the selection, drawing from the original brief and research notes0.1 cr
Fix Tonefix_toneRewrites selection to align with the brand voice score observations0.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 invalidation

Key Design Decisions

DecisionChoiceRationale
Heuristic vs LLM scoringHeuristic for SEO + Readability; LLM only for Brand Voice + AI SearchKeeps 0 credits for the most-used scores; LLM needed only where rule-based analysis is insufficient
Score triggerAuto on save, plus manual buttonAuto keeps the panel fresh; manual button lets DM reviewer run a re-score after edits without waiting for another save
Model for AI scoresClaude Haiku (not Sonnet)Short, focused evaluation task; Haiku is 10× cheaper and fast enough
Score storageStored on BlogPost as JSONAvoids a separate scoring table; score is always tied to the post version
Guided fix specificityFixes 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

  1. Add scoreResult, scoredAt, scoreVersion columns to BlogPost (migration)
  2. Implement content-scorer.service.ts in apps/api/src/services/ — heuristic SEO + readability pass, returns ContentScoreResult with empty AI sections
  3. Add POST /tenant/v1/blog/:id/score route — runs heuristic scorer synchronously, stores result, returns ContentScoreResult
  4. Blog editor sidebar: score panel component showing SEO + Readability scores + guided fixes list

Phase 2 — AI Scores

  1. Add brand-voice-scorer and ai-search-scorer BullMQ workers
  2. Extend POST /tenant/v1/blog/:id/score to enqueue AI scorer jobs when plan is Pro+
  3. WebSocket event blog:score_updated emitted when AI jobs complete — editor sidebar updates in real time
  4. Credit deduction: 0.25 cr per AI score pair

Phase 3 — In-Editor AI Actions

  1. Add POST /tenant/v1/blog/:id/ai-action route
  2. Extend blog-writer agent to handle targeted mode flags (rewrite_section, simplify_section, expand_section, fix_tone)
  3. In-editor toolbar buttons + text selection handler
  4. Stream AI action response back to editor and replace selected text on accept

Phase 4 — Score Badge in List View

  1. Include scoreResult.overallScore in blog list API response
  2. Add colour-coded score badge to blog list page

© 2026 Leadmetrics — Internal use only