<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Stéphane Jambu</title>
    <description>The latest articles on DEV Community by Stéphane Jambu (@stephanejambu).</description>
    <link>https://dev.to/stephanejambu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3953538%2Fcb0f614f-3cb6-47bb-8250-5bc12d1d16d6.jpg</url>
      <title>DEV Community: Stéphane Jambu</title>
      <link>https://dev.to/stephanejambu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stephanejambu"/>
    <language>en</language>
    <item>
      <title>Industrial SEO at 100 Pages/Week: My n8n + Claude Code + RAG Stack</title>
      <dc:creator>Stéphane Jambu</dc:creator>
      <pubDate>Wed, 27 May 2026 04:51:53 +0000</pubDate>
      <link>https://dev.to/stephanejambu/industrial-seo-at-100-pagesweek-my-n8n-claude-code-rag-stack-2k58</link>
      <guid>https://dev.to/stephanejambu/industrial-seo-at-100-pagesweek-my-n8n-claude-code-rag-stack-2k58</guid>
      <description>&lt;p&gt;I run a French SEO agency from Siem Reap, Cambodia. We've shipped 1,300+ semantic content clusters for 650+ brands — typically at 50 to 100 pages per project per week.&lt;/p&gt;

&lt;p&gt;That cadence is impossible with a traditional content team. It's also impossible with raw LLM generation: the output looks fine in isolation and rots when you read three pages in a row.&lt;/p&gt;

&lt;p&gt;What works is a three-layer pipeline that treats content like a production line, not a creative process. Here's the actual stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "AI content" as people usually do it
&lt;/h2&gt;

&lt;p&gt;Most "industrial AI content" implementations look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keyword list → ChatGPT prompt → publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It produces 50 pages in an hour. It also produces 50 pages that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repeat the same five intros across the whole cluster&lt;/li&gt;
&lt;li&gt;Hallucinate stats no one can audit&lt;/li&gt;
&lt;li&gt;Drift away from the actual cluster theme by page 30&lt;/li&gt;
&lt;li&gt;Use no internal linking strategy&lt;/li&gt;
&lt;li&gt;Read like LinkedIn boilerplate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google's May 2024 leak confirmed what experienced SEOs already knew: the algorithm scores the cluster, not just the page. &lt;code&gt;siteFocusScore&lt;/code&gt;, &lt;code&gt;siteAuthority&lt;/code&gt;, and the compressed quality signals don't care that page 23 is well-written if pages 1 through 22 read like the same prompt with different keywords.&lt;/p&gt;

&lt;p&gt;So the question isn't "how do I generate content fast?" It's "how do I generate content fast AND keep it coherent across the whole cluster?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The three layers
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────┐
│  Layer 1 — RAG knowledge base (per-client)         │
│  - Client brief, brand voice, product corpus       │
│  - Existing pages (what's already said)            │
│  - Topic graph (what each page must cover)         │
└────────────────────────────────────────────────────┘
                       ▼
┌────────────────────────────────────────────────────┐
│  Layer 2 — n8n orchestration                       │
│  - Pull next page brief from Google Sheet          │
│  - Inject RAG context + brief                      │
│  - Call LLM (Claude/DeepSeek depending on tier)    │
│  - Save draft to Sheet column                      │
│  - Trigger QA round                                │
└────────────────────────────────────────────────────┘
                       ▼
┌────────────────────────────────────────────────────┐
│  Layer 3 — Claude Code QA loop                     │
│  - Read draft + cluster context                    │
│  - Check coherence, internal links, brand voice    │
│  - Either approve or write structured feedback     │
│  - Loop until pass or human escalation             │
└────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is that Layer 1 is what makes Layer 2's output not boring, and Layer 3 is what catches Layer 2's mistakes before a human ever reads them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: the RAG knowledge base
&lt;/h2&gt;

&lt;p&gt;One per client. Indexed and re-indexed on every brief update. Stored as a local vector DB (we use &lt;code&gt;qdrant&lt;/code&gt; for production, &lt;code&gt;chromadb&lt;/code&gt; for prototyping) with three collections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pseudo-structure (sanitized from production)
&lt;/span&gt;&lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_brief&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positioning.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tone_of_voice.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products/*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;existing_pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site_pages/*.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# cleaned + extracted
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;topic_graph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster_map.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# which page covers which subtopic
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retrieval at generation time pulls top-k from each collection, with weights tuned per cluster. Typical pull for one page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 chunks from &lt;code&gt;client_brief&lt;/code&gt; (voice + positioning)&lt;/li&gt;
&lt;li&gt;5 chunks from &lt;code&gt;existing_pages&lt;/code&gt; (so we don't repeat what's already said)&lt;/li&gt;
&lt;li&gt;1 chunk from &lt;code&gt;topic_graph&lt;/code&gt; (what THIS page must cover that others don't)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is what kills cluster drift. Without it, page 30 will accidentally rewrite page 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: the n8n workflow
&lt;/h2&gt;

&lt;p&gt;n8n is the right tool because the loop has too many side effects to keep in a Python script: Google Sheets read/write, LLM API calls with retry logic, conditional branching on tier, webhook callbacks from Layer 3, Slack notifications when something stalls.&lt;/p&gt;

&lt;p&gt;The core loop, simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Trigger: cron every 10min]
    │
    ▼
[Google Sheets: get next row WHERE status = "to_write"]
    │
    ▼
[HTTP: call RAG service, get context]
    │
    ▼
[Switch by tier]
    ├─ premium → Claude Sonnet
    ├─ standard → DeepSeek Pro
    └─ longtail → DeepSeek Flash
    │
    ▼
[LLM call with composed prompt]
    │
    ▼
[Google Sheets: update row with draft + status = "to_qa"]
    │
    ▼
[Webhook: trigger Layer 3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Switch node by tier is what makes the unit economics work. A premium page costs ~$0.40 in API spend; a long-tail page costs ~$0.02. You can't ship 100 pages/week on premium pricing for every page.&lt;/p&gt;

&lt;p&gt;n8n's other quiet superpower: error workflows. Every node in the production graph has an error handler that writes to a "stuck" sheet with the error message and stack. A human reads that sheet once a day. Anything not in the sheet just worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: the Claude Code QA loop
&lt;/h2&gt;

&lt;p&gt;This is the layer most people don't have, and it's the one that decides whether the cluster is shippable or another low-quality generative blob.&lt;/p&gt;

&lt;p&gt;I use Claude Code (the CLI, not the API directly) because the agentic loop is built in. The QA agent runs against each draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--model&lt;/span&gt; claude-sonnet-4-6 &lt;span class="nt"&gt;--dangerously-skip-permissions&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Read the draft at &lt;/span&gt;&lt;span class="nv"&gt;$DRAFT_PATH&lt;/span&gt;&lt;span class="s2"&gt;. Read the cluster context at &lt;/span&gt;&lt;span class="nv"&gt;$CONTEXT_PATH&lt;/span&gt;&lt;span class="s2"&gt;.
   Run the checks defined in qa-rules.md.
   For each failed check, write structured feedback to &lt;/span&gt;&lt;span class="nv"&gt;$FEEDBACK_PATH&lt;/span&gt;&lt;span class="s2"&gt;.
   If all checks pass, write APPROVED to &lt;/span&gt;&lt;span class="nv"&gt;$STATUS_PATH&lt;/span&gt;&lt;span class="s2"&gt; and exit.
   If 3+ checks fail, write ESCALATE to &lt;/span&gt;&lt;span class="nv"&gt;$STATUS_PATH&lt;/span&gt;&lt;span class="s2"&gt; and exit."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;qa-rules.md&lt;/code&gt; file is the contract. It includes things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Voice: matches the tone defined in client_brief/tone_of_voice.md
&lt;span class="p"&gt;-&lt;/span&gt; Repetition: no intro paragraph that mirrors another page in this cluster
&lt;span class="p"&gt;-&lt;/span&gt; Internal links: 3-5 contextual links to sibling pages in the cluster
&lt;span class="p"&gt;-&lt;/span&gt; Claims: every statistic must trace to a source in client_brief/ or be removed
&lt;span class="p"&gt;-&lt;/span&gt; Hooks: opening sentence must not be a generic platitude
&lt;span class="p"&gt;-&lt;/span&gt; Tail: closing sentence must not be a CTA — that's the layout's job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the agent writes structured feedback, n8n picks it up via webhook and routes the draft back to Layer 2 with the feedback injected into the next prompt. Three rounds, then human review.&lt;/p&gt;

&lt;p&gt;The economics: ~70% of drafts pass on round 1, ~25% on round 2, ~5% need human eyes. That last 5% is where the real attention goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently if starting over
&lt;/h2&gt;

&lt;p&gt;A few things that cost us months to learn:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Don't index the entire client site into the RAG store on day 1.&lt;/strong&gt; Index the brief + 10 hand-picked pages first. When generation starts producing "voice drift," then add more pages. Indexing too early means the retrieval pulls in stale content as "the voice."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Don't put cluster theme detection in the LLM.&lt;/strong&gt; Encode it in the topic graph as structured metadata. The LLM is bad at remembering "this page is about X, not Y" across 50 turns; the topic graph never forgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Add a fingerprint check on intros.&lt;/strong&gt; Cheap: hash the first 50 words of every published page in the cluster. New drafts get compared. If hamming distance is below threshold, regenerate the intro. This single check killed the repetition problem in one afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Don't trust the LLM's self-evaluation in the same call.&lt;/strong&gt; Two-call evaluation (generate, then a fresh model instance evaluates) catches things a single call misses. Same model, fresh context, no anchoring bias.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Keep a kill switch on every workflow.&lt;/strong&gt; A single Sheet cell named &lt;code&gt;pipeline.enabled&lt;/code&gt;. If it's &lt;code&gt;FALSE&lt;/code&gt;, every n8n trigger short-circuits. You will need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-offs
&lt;/h2&gt;

&lt;p&gt;This pipeline is not "AI writing the content." It's a content production line where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The brief is human (a real SEO strategist's work)&lt;/li&gt;
&lt;li&gt;The cluster map is human (someone decided what each page covers)&lt;/li&gt;
&lt;li&gt;The voice corpus is human (a real client brand)&lt;/li&gt;
&lt;li&gt;The QA contract is human (the rules we ship by)&lt;/li&gt;
&lt;li&gt;The LLM does the typing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't skip those four human inputs and expect this to work. The pipeline ships 100 pages/week because the upstream work is done. Without it, you ship 100 pages of slop.&lt;/p&gt;

&lt;p&gt;The other trade-off: this is a system, not a tool. It needs maintenance — RAG re-indexing as the client's catalog evolves, prompt tuning as Claude/DeepSeek versions change, kill switch monitoring. Budget roughly 1 senior engineer day per week per active project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm publishing the topic graph schema, the QA rules template, and a sanitized version of the n8n workflow as a separate repo. Drop a comment if you'd find that useful.&lt;/p&gt;

&lt;p&gt;The same pipeline, with a different QA layer that checks LLM citability instead of cluster coherence, is what we use for GEO (Generative Engine Optimization). That's the next post in this series.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stéphane Jambu — SEO engineer building topical authority at scale. 1,300+ semantic clusters for 650+ brands. Speaking on industrial SEO at &lt;a href="https://stephane-jambu.com" rel="noopener noreferrer"&gt;stephane-jambu.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>ai</category>
      <category>automation</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
