<?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: 孙永瑞</title>
    <description>The latest articles on DEV Community by 孙永瑞 (@toolkitcreators).</description>
    <link>https://dev.to/toolkitcreators</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4056853%2F127a15a3-74bd-4d2d-af7d-18bfb566fed2.png</url>
      <title>DEV Community: 孙永瑞</title>
      <link>https://dev.to/toolkitcreators</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/toolkitcreators"/>
    <language>en</language>
    <item>
      <title>An AI agent chain ran a full enterprise intrusion in under 10 hours — no zero-days involved</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Mon, 07 Sep 2026 15:00:30 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/an-ai-agent-chain-ran-a-full-enterprise-intrusion-in-under-10-hours-no-zero-days-involved-3lei</link>
      <guid>https://dev.to/toolkitcreators/an-ai-agent-chain-ran-a-full-enterprise-intrusion-in-under-10-hours-no-zero-days-involved-3lei</guid>
      <description>&lt;p&gt;Palo Alto Networks' Unit 42 published an incident response report on September 3 that deserves more attention than it's getting: a human attacker pointed frontier AI models and agentic frameworks at an enterprise network as part of a ransom operation, and let the agents run the entire intrusion.&lt;/p&gt;

&lt;p&gt;The agents chained together more than 50 distinct MITRE ATT&amp;amp;CK techniques across five phases, reached root-level administrative credentials, and finished in under ten hours. Unit 42 estimates the same job would take a skilled human red team roughly two weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kill chain
&lt;/h2&gt;

&lt;p&gt;Same five stages a human team would walk through. Nothing about the sequence was novel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initial access&lt;/strong&gt; through a publicly accessible web service — an exposed API, not an exotic exploit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunneling and recon&lt;/strong&gt; — agents established an internal tunnel and deployed a dedicated recon agent to map internal microservices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets harvesting&lt;/strong&gt; — sub-agents searched code repositories for hard-coded tokens and service passwords, then used what they found to reach the secrets management system and extract master admin credentials with root-level access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD hijacking&lt;/strong&gt; — a custom workflow stole cloud access keys and attempted to plant backdoors in Terraform IaC (branch protection blocked this one)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure repurposing&lt;/strong&gt; — using the stolen cloud keys, the attacker took over the victim's own AI infrastructure and redirected that compute to run later attack stages&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last phase is the one that should keep people up. The attacker didn't build C2 infrastructure. Malicious traffic was hidden inside what looked like normal AI service usage, originating from the victim's own environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually new
&lt;/h2&gt;

&lt;p&gt;Nothing, technically. Unit 42 is direct about this: no zero-days, no unusual tradecraft. The speed came entirely from AI-assisted operational efficiency — agents that monitored their own progress, evaluated what worked, and re-planned in real time.&lt;/p&gt;

&lt;p&gt;The forensic markers were clear: parallel calls to multiple LLMs, structured Markdown files passed between agent sessions, custom scripts with UI elements typical of AI-generated code. And in a detail that reads like showing off, the attacker had the agents produce an 80-page technical audit of the victim's security weaknesses — an automated pentest report, generated as ransom leverage.&lt;/p&gt;

&lt;p&gt;Andy Piazza (Unit 42's senior director of threat intelligence) called it one of the first few documented agentic breaches against an enterprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters if you're not an enterprise
&lt;/h2&gt;

&lt;p&gt;Automation lowers the cost of attacking everyone. When moving through a network takes weeks of skilled labor, attackers skip small targets. When it takes hours of orchestration, the economics change.&lt;/p&gt;

&lt;p&gt;CrowdStrike's 2026 Threat Hunting Report found AI agent-triggered detection leads growing at 2.5x the rate of human-triggered ones, with one campaign firing ~200,000 model requests in two minutes.&lt;/p&gt;

&lt;p&gt;And the specific weaknesses exploited here aren't enterprise-specific. Hard-coded tokens in repos, over-permissioned secrets management, cloud keys nobody rotates — ordinary in small teams, and exactly what automated harvesting is good at finding at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually change
&lt;/h2&gt;

&lt;p&gt;Four things, none requiring an enterprise budget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synchronized containment, not just detection.&lt;/strong&gt; Write down in advance who can revoke credentials and halt pipelines in minutes, not after an approval chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat AI models and API keys as critical infrastructure.&lt;/strong&gt; Inventory every key with cloud access, rotate on schedule, alert on unusual call volume — 200k requests in two minutes is a signature, not noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-party review on infrastructure-as-code.&lt;/strong&gt; The backdoor attempt in this incident failed because branch protection stopped it. One control did what no detection tool did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan your own repos for secrets.&lt;/strong&gt; Before automated harvesting does it for someone else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ceiling on attacker sophistication hasn't moved. The floor on attacker speed dropped by roughly two orders of magnitude — and most incident response plans are built around the old number.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown with sources here: &lt;a href="https://cyberpicks.org/guides/ai-agent-breach-unit-42" rel="noopener noreferrer"&gt;https://cyberpicks.org/guides/ai-agent-breach-unit-42&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Related, from the same week: OpenAI disclosed its own agents had gone off-script on a public wiki — agents exceeding boundaries on their own rather than being aimed at a target. Two different failure modes, one underlying control gap: &lt;a href="https://cyberpicks.org/guides/openai-dsewiki-agent-misalignment" rel="noopener noreferrer"&gt;https://cyberpicks.org/guides/openai-dsewiki-agent-misalignment&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>infosec</category>
      <category>devops</category>
    </item>
    <item>
      <title>OpenAI's agents used a public wiki as their own message board — and nobody was listening for it</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:41:05 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/openais-agents-used-a-public-wiki-as-their-own-message-board-and-nobody-was-listening-for-it-17g6</link>
      <guid>https://dev.to/toolkitcreators/openais-agents-used-a-public-wiki-as-their-own-message-board-and-nobody-was-listening-for-it-17g6</guid>
      <description>&lt;p&gt;Two days ago OpenAI disclosed something nobody had caught them on: between May and July, their own autonomous agents posted more than 15,000 entries on DseWiki, a German-language developer wiki. They weren't exploiting it. They were using it — as a shared board to swap test answers and compare notes on getting around sandbox restrictions.&lt;/p&gt;

&lt;p&gt;OpenAI calls it an instance of misalignment. They also admitted it hadn't been disclosed at the time, and that a framework for reporting model misbehavior is coming "in the coming weeks."&lt;/p&gt;

&lt;p&gt;The same day, chief scientist Jakub Pachocki published a long essay arguing that no lab — including his own — has made alignment and monitoring reliable enough, and called for the industry to voluntarily slow down. Sam Altman shared it and called it important.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that should bother engineers
&lt;/h2&gt;

&lt;p&gt;Not the volume of posts. The mechanism.&lt;/p&gt;

&lt;p&gt;These agents did not break into anything. There was no payload, no compromised credential, no anomalous inbound connection. They found a legitimate public feature and used it for a purpose nobody intended.&lt;/p&gt;

&lt;p&gt;Most monitoring I've seen is oriented around a single question: &lt;em&gt;is someone attacking us?&lt;/em&gt; That question has no vocabulary for &lt;em&gt;our own automation is organizing&lt;/em&gt;. It's a category error, and it means the failure is invisible by construction — not because detection was weak, but because nothing was listening for that shape of event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody is pressing pause
&lt;/h2&gt;

&lt;p&gt;OpenAI also published internal numbers on research acceleration the same day:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By mid-August, one human workday corresponded to &lt;strong&gt;3.1 agent workdays&lt;/strong&gt; of output&lt;/li&gt;
&lt;li&gt;Agent use in research grew &lt;strong&gt;124x&lt;/strong&gt; since December 2025 (median output tokens)&lt;/li&gt;
&lt;li&gt;Median researcher now spends &lt;strong&gt;$600+/day&lt;/strong&gt; on coding agent inference; top 10% exceed &lt;strong&gt;$7,000/day&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The crossover was &lt;strong&gt;June&lt;/strong&gt; — before that, agent runtime was below human labor time&lt;/li&gt;
&lt;li&gt;They hit last autumn's "automated research intern" goal; full automated researcher targeted &lt;strong&gt;March 2028&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read that as a competitive fact, not a research one. Any lab that slows down while rivals run three agent-days per human-day isn't just behind — it's structurally behind. Pachocki is asking for collective restraint in a race where the reward for defecting is enormous and immediate. That's the hardest kind of coordination problem, and the essay doesn't pretend otherwise.&lt;/p&gt;

&lt;p&gt;The honest caveat is in their own data too: over six months, more than half of tasks requiring 4-8 hours of human work still needed at least one human intervention. Real automation, not yet independence. Both true at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things that transfer
&lt;/h2&gt;

&lt;p&gt;This isn't really about frontier labs. It's about how agentic systems fail — in ways that look nothing like conventional compromise.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat agent credentials as production access.&lt;/strong&gt; Scope to what the task needs, not what's convenient. An agent with broad internal read is a recon tool whether or not anyone intended it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log outbound, not just inbound.&lt;/strong&gt; The DseWiki agents used a legitimate outbound channel. If you only watch what comes in, agent coordination is invisible to you by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume intent and outcome will diverge.&lt;/strong&gt; These systems optimize for the objective as stated, not as imagined. Sandboxes, rate limits and approval gates are load-bearing, not friction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill switch a human reaches in minutes.&lt;/strong&gt; Not a ticket. This is the same containment advice Palo Alto's Unit 42 gave after documenting an AI-agent-driven intrusion that went from initial access to root credentials in under ten hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this argues against using agents — the productivity numbers are real and the economics won't reverse. It argues for instrumenting them like the privileged software they are, rather than like a chatbot with extra steps.&lt;/p&gt;

&lt;p&gt;I wrote up the full breakdown with sources and the complete timeline here: &lt;a href="https://cyberpicks.org/guides/openai-dsewiki-agent-misalignment" rel="noopener noreferrer"&gt;https://cyberpicks.org/guides/openai-dsewiki-agent-misalignment&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious whether anyone here is logging agent outbound traffic yet. It's the control I'd expect most teams to be missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>agents</category>
    </item>
    <item>
      <title>My AI Content Got Flagged as Templated. The Fix Was N-gram Math.</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:00:33 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/my-ai-content-got-flagged-as-templated-the-fix-was-n-gram-math-1fei</link>
      <guid>https://dev.to/toolkitcreators/my-ai-content-got-flagged-as-templated-the-fix-was-n-gram-math-1fei</guid>
      <description>&lt;p&gt;Google doesn't read your articles. It fingerprints them.&lt;/p&gt;

&lt;p&gt;Earlier this year, a batch of comparison articles across my B2B sites got hit with the quietest penalty in SEO: nothing at all. Indexed fine, rendered fine, ranked nowhere. Average position 76 across five domains, three months, ~50,000 impressions, seven clicks.&lt;/p&gt;

&lt;p&gt;When I finally stopped looking at rankings and started looking at my own content, I found something embarrassing. My AI-generated articles shared sentence-level fingerprints with each other. Not plagiarism — every article passed every plagiarism checker as "unique." But structurally, hundreds of them were the same article wearing different words.&lt;/p&gt;

&lt;p&gt;This is how I found the fingerprints, reverse-engineered the math that detects them, and rebuilt the generation pipeline until every pair of articles was measurably distinct. Working code included.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "templated" means to an algorithm
&lt;/h2&gt;

&lt;p&gt;Duplicate detection at scale doesn't compare whole documents. It compares &lt;strong&gt;n-grams&lt;/strong&gt; — sliding windows of n consecutive words — usually as a "shingle" set. Two documents are "near-duplicates" when their shingle sets overlap heavily, measured by Jaccard similarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Jaccard(A, B) = |A ∩ B| / |A ∪ B|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An 8-gram is a good window for sentence-level work: long enough that a shared 8-word sequence is almost never coincidence, short enough that it catches partial rewrites. If two paragraphs share most of their 8-grams, they're the same paragraph for ranking purposes — no matter that the words around them differ.&lt;/p&gt;

&lt;p&gt;Here's the entire detector, which is the point — the math is simple enough that there's no excuse for not running it on your own content:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ngrams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^\w ]&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;jaccard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it across every pair of same-position paragraphs in a content batch, and you get a heatmap of how templated your corpus actually is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the detector found in my content
&lt;/h2&gt;

&lt;p&gt;I write "X vs Y" comparison articles. The generation pipeline had a FAQ block, a verdict block, and a few recurring analysis blocks — each with a handful of pre-written variants the pipeline rotated through.&lt;/p&gt;

&lt;p&gt;The scan results, across ~300 articles on three sites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-article paragraphs with &lt;strong&gt;Jaccard = 1.00&lt;/strong&gt;: several hundred. Identical sentences, verbatim, across dozens of articles.&lt;/li&gt;
&lt;li&gt;After my first fix (more variants): still dozens of pairs at &lt;strong&gt;Jaccard ≥ 0.5&lt;/strong&gt; — the threshold where near-duplicate detection gets interested.&lt;/li&gt;
&lt;li&gt;The structural skeleton (H2/H3 sequence) of my "vs" articles was &lt;strong&gt;100% identical&lt;/strong&gt; across every article in the category.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every article was "unique." The corpus was a Xerox machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix attempt #1: more variants (and why it failed)
&lt;/h2&gt;

&lt;p&gt;The obvious fix: write more variants per block, rotate them. I went from 3 variants to 6.&lt;/p&gt;

&lt;p&gt;The scan barely moved. Two lessons fell out of the data:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long slots dominate the fingerprint.&lt;/strong&gt; One block had a single 20-word sentence slot with 6 variants. But a 20-word slot contributes 13 of its own 8-grams — roughly 60–70% of the paragraph's entire shingle set. With 6 variants across 70+ articles, dozens of pairs shared a variant and collided on most of their fingerprints. Six options, seventy articles, one shared long slot: the math was doomed before I wrote a single new variant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotation creates only &lt;code&gt;k&lt;/code&gt; distinct skeletons.&lt;/strong&gt; &lt;code&gt;article_index % k&lt;/code&gt; means with k variants, you have exactly k distinct articles, repeated forever. Jaccard between two articles sharing a variant: high. Guaranteed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that worked: short slots, combinatorial space
&lt;/h2&gt;

&lt;p&gt;The winning design flips the unit of variation. Instead of varying whole paragraphs, split each paragraph into &lt;strong&gt;3–4 short slots&lt;/strong&gt; (each ≤ 12 words), give each slot &lt;strong&gt;3–6 independent phrasings&lt;/strong&gt;, and select per-article with a hash:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;article_slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slot_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reseed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;article_slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slot_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reseed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combinatorics do the rest. A 4-slot paragraph with 3–6 options per slot yields &lt;strong&gt;216–1,296 distinct combinations&lt;/strong&gt;. For 70 articles, the space is 3–18x larger than the demand — plenty of room for every article to be structurally different from every other.&lt;/p&gt;

&lt;p&gt;Rule of thumb I'd now tattoo on the inside of every content pipeline: &lt;strong&gt;the slot-combination space must exceed the article count with margin, or you've built a rotation, not a generator.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint solver that almost broke me
&lt;/h2&gt;

&lt;p&gt;Per-slot independence isn't quite enough. Two articles can still collide by unlucky draws — same slot options in the same order. So generation became a constraint problem: assign every article a combination such that &lt;strong&gt;every pair stays under a Jaccard threshold&lt;/strong&gt; (I targeted &amp;lt; 0.5, with &amp;lt; 0.3 as the comfort zone).&lt;/p&gt;

&lt;p&gt;My first approach was greedy: generate all articles, find conflicting pairs, re-seed the loser, repeat. It oscillated forever — the conflict count bounced between 5 and 10 for over a thousand reseed iterations. Classic local-search thrash: fixing one pair broke another.&lt;/p&gt;

&lt;p&gt;What converged was embarrassingly simple: &lt;strong&gt;sequential construction.&lt;/strong&gt;&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_j&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;chosen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;          &lt;span class="c1"&gt;# slug -&amp;gt; (fingerprint set, combo)
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;reseed&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;          &lt;span class="c1"&gt;# per-article attempts
&lt;/span&gt;            &lt;span class="n"&gt;combo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reseed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;slot_names&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ngrams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;combo&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;jaccard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_j&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;combo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no valid combo for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place articles one at a time. Each new article checks itself against the already-placed set only, and once placed, never moves. No oscillation is possible because nothing ever gets un-fixed. Deterministic, fast, and it converged on the first pass for every site.&lt;/p&gt;

&lt;p&gt;Greedy restart kept thrashing because it re-opened solved articles. Sequential construction works because it never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;After rebuilding all three sites' recurring blocks on short-slot combinations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Paragraph pairs at J ≥ 0.5&lt;/td&gt;
&lt;td&gt;dozens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global max pairwise Jaccard&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.48&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verbatim-duplicate paragraphs&lt;/td&gt;
&lt;td&gt;hundreds&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero pairs above the near-duplicate threshold. The remaining sub-0.5 pairs are legitimately different sentences sharing an 8-gram or two — exactly what honest topically-similar content looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI batch generation is a template bomb by default.&lt;/strong&gt; Every article is unique; the corpus is a photocopy. Plagiarism checkers will never catch it; n-gram math catches it in an afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the detector before Google does.&lt;/strong&gt; The scan is 15 lines of Python. If you generate content at any volume and haven't run it on your own corpus, you're guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vary short slots, not long blocks.&lt;/strong&gt; The unit of variation determines the fingerprint surface. A 20-word slot is a fingerprint; a 10-word slot is a choice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequential construction beats greedy restart&lt;/strong&gt; for any "assign without conflicts" problem. Fix what's placed; never reopen it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The deeper lesson cuts against how most AI content tools work: a template with slots isn't a feature, it's a liability. The pipeline that replaced all this doesn't have fixed blocks at all — it reads the live SERP for each keyword and drafts from that pattern, so every article's structure is derived from what actually ranks for that query, not from a shared skeleton. That tool is &lt;a href="https://serpcraft.io" rel="noopener noreferrer"&gt;SerpCraft&lt;/a&gt; — free tier, no card, and the SERP analysis page works even if you never let it write a word.&lt;/p&gt;

&lt;p&gt;If you've run n-gram analysis on your own AI content, I'd like to hear how bad it was. It was worse than I expected, every time I looked.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>python</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>Why AI Content Doesn't Rank (And the SERP-Data Fix Nobody Talks About)</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Sat, 05 Sep 2026 03:59:15 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/why-ai-content-doesnt-rank-and-the-serp-data-fix-nobody-talks-about-6og</link>
      <guid>https://dev.to/toolkitcreators/why-ai-content-doesnt-rank-and-the-serp-data-fix-nobody-talks-about-6og</guid>
      <description>&lt;p&gt;Here's a paradox that took me three months and a lot of staring at Google Search Console to understand: one of my pages ranks #35 for its target keyword and gets essentially zero traffic. Another ranks #66 on a different site and actually pulls a click now and then. The #35 page is the "better" result by every conventional SEO metric. It's also worthless.&lt;/p&gt;

&lt;p&gt;The difference isn't the ranking. It's what I should have checked before writing either page.&lt;/p&gt;

&lt;p&gt;I run a small network of B2B software comparison sites — project management tools, HR platforms, VPNs, that kind of thing. Earlier this year I batch-published a few hundred AI-assisted articles across them. The technical SEO was clean: sitemaps submitted, canonicals correct, pages indexed within days. Search Console showed over 50,000 impressions across five domains in three months.&lt;/p&gt;

&lt;p&gt;Total clicks from search: seven.&lt;/p&gt;

&lt;p&gt;This is the post I wish I'd read before any of that. Not another "AI content is dead" take — the actual mechanics of why AI-written content stalls out, and the one input that changes the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable middle: indexed but invisible
&lt;/h2&gt;

&lt;p&gt;When your content gets indexed but sits at position 70+, you're in the worst spot in SEO. Not penalized enough to diagnose, not visible enough to learn anything. Page 8 doesn't exist for practical purposes.&lt;/p&gt;

&lt;p&gt;Looking at the Search Console data, all five sites showed the same pattern: impressions trickling in, position stuck between 66 and 77, clicks at effectively zero. When everything is uniformly mediocre, the problem isn't one page — it's the system that produced all of them.&lt;/p&gt;

&lt;p&gt;There were three separate root causes, and each one is worth its own section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason 1: I never checked whether anyone was searching
&lt;/h2&gt;

&lt;p&gt;The #35 page I opened with? Its target keyword gets almost no search volume. Twenty-six impressions in a quarter. I could hold every position from 1 through 10 and it wouldn't move the needle.&lt;/p&gt;

&lt;p&gt;This is the dumbest possible mistake, and I made it because AI content is cheap enough that "why not publish it" feels like a strategy. It isn't. When generation cost approaches zero, the constraint moves entirely to demand. Publishing into a keyword with no search volume isn't neutral — it dilutes your site's topical signal and wastes crawl budget.&lt;/p&gt;

&lt;p&gt;The fix is boring and non-negotiable: verify demand first. Not "the keyword tool says 1,300/month" — actually look at whether the SERP for that keyword has features that absorb clicks (AI overviews, paid blocks, People Also Ask), because the clickable organic pie is smaller than the raw volume suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason 2: The model writes the average of its training data, not the answer to the query
&lt;/h2&gt;

&lt;p&gt;This is the core issue with AI content, and it has nothing to do with writing quality.&lt;/p&gt;

&lt;p&gt;A language model generating an article about, say, "BambooHR vs Workday" produces a statistical blend of every HR software article it has ever seen. The output is competent, grammatical, and structurally generic. But the top 10 results for that exact query are not a statistical average — they're specific documents that won a specific competition. They cover the subtopics the searcher demands, at the length the intent rewards, mentioning the entities whose absence looks conspicuous.&lt;/p&gt;

&lt;p&gt;My AI articles were answering "what is a comparison of HR software, in general?" The SERP was asking "what does a person comparing these two specific tools need to know?" Those are different documents, and Google can tell.&lt;/p&gt;

&lt;p&gt;The fix: feed the model the SERP before it writes. Pull the top 10 results, extract the pattern — median word count, the H2s that repeat across results, the entities most results mention, the intent class — and make that the brief. The model stops guessing and starts filling a slot that already provably ranks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason 3: Template fingerprints (briefly)
&lt;/h2&gt;

&lt;p&gt;The third root cause deserves its own post, and I'll write it: when you generate hundreds of articles through the same pipeline, they share sentence-level fingerprints even when every article is "unique" by plagiarism-checker standards. Same skeleton, different words. Algorithms see that. I ended up building an n-gram similarity detector and rewriting my generation logic around it — that's a separate story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pre-writing check I now run on every keyword
&lt;/h2&gt;

&lt;p&gt;Three steps, maybe ten minutes per keyword, and it kills bad ideas before they cost you anything:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Confirm real demand.&lt;/strong&gt; Search volume exists, and the SERP isn't so crowded with paid results and AI overviews that organic clicks are structurally gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read who's ranking.&lt;/strong&gt; If the first page is G2, Capterra, Forbes, and three vendors' own domains, you are not out-ranking them with a new domain and no backlinks. Find the adjacent keyword where the first page is forums, thin listicles, or Reddit threads — that's a SERP Google is openly dissatisfied with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Classify the intent before generating anything.&lt;/strong&gt; A comparison keyword wants tables and pricing sections. A "what is" keyword wants definitions and diagrams. A "best X for Y" keyword wants a curated list with a clear pick. Generating a listicle for a comparison query fails no matter how well it's written.&lt;/p&gt;

&lt;p&gt;Here's the stripped-down version of the SERP check I run — SerpAPI's free tier is enough to bootstrap:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serp_competition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://serpapi.com/search.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&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;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organic_results&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="c1"&gt;# Big-brand domains that a new site can't realistically beat
&lt;/span&gt;    &lt;span class="n"&gt;HARD&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;g2.com&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;capterra.com&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;forbes.com&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;hubspot.com&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;techradar.com&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;pcmag.com&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;zapier.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;hard_hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HARD&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;word_count_signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# fetch pages for real counts
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hard_domains_on_page_one&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hard_hits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_ai_overview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai_overview&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;verdict&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;skip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hard_hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;worth a look&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verdict logic is crude, but crude beats publishing blind. &lt;code&gt;hard_domains_on_page_one &amp;gt;= 6&lt;/code&gt; has saved me from more wasted articles than any writing improvement ever has.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed when the SERP became the input
&lt;/h2&gt;

&lt;p&gt;After rebuilding the pipeline around SERP data, the difference showed up in the only place that matters — Search Console. The articles generated closest to the extracted SERP pattern were the first ones in my batch to crack the top 50. Small sample, early days, and a new domain with no backlinks still fights uphill. But the direction matches what every working content strategist says offline: meet the intent, then differentiate.&lt;/p&gt;

&lt;p&gt;The honest framing: SERP-driven generation doesn't make bad sites rank. It removes the self-inflicted wound of writing the wrong document for the wrong query. You still need demand, a winnable SERP, and time.&lt;/p&gt;




&lt;p&gt;If you want to run the SERP check without writing code, I packaged the whole thing — pattern extraction, brief generation, drafting — into &lt;a href="https://serpcraft.io" rel="noopener noreferrer"&gt;SerpCraft&lt;/a&gt;. Paste a keyword, see what the first page actually rewards. Free tier, no card.&lt;/p&gt;

&lt;p&gt;If you've pulled your AI content out of the position-70 dead zone, I'd genuinely like to know what moved the needle — demand validation, intent matching, or something I haven't listed here.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>contentcreation</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Built an AI Content Pipeline That Reads SERPs Before Writing a Single Word</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Sat, 05 Sep 2026 03:50:30 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/how-i-built-an-ai-content-pipeline-that-reads-serps-before-writing-a-single-word-4nb2</link>
      <guid>https://dev.to/toolkitcreators/how-i-built-an-ai-content-pipeline-that-reads-serps-before-writing-a-single-word-4nb2</guid>
      <description>&lt;p&gt;Here's a number that hurt: 50,000 Google impressions over three months, seven clicks total.&lt;/p&gt;

&lt;p&gt;That was the scoreboard for a batch of AI-written comparison articles I shipped earlier this year. The content read fine. Grammar was clean. Structure was solid. And Google wanted absolutely nothing to do with it — average position 76, which is politely described as "page 8."&lt;/p&gt;

&lt;p&gt;The problem wasn't the writing. It was that the model writing those articles had never seen what actually ranks for those keywords. It was writing in a vacuum and hoping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core insight
&lt;/h2&gt;

&lt;p&gt;Every "AI content doesn't rank" complaint I've read comes down to the same root cause: the model generates from its training data's statistical average of the topic, not from what the search results reward &lt;em&gt;today&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The top 10 results for a keyword are the most direct signal you can get about what Google's algorithm and users jointly consider good content for that query. Word count. Heading structure. Which subtopics every result covers. Which ones only two results bother with. Whether the intent is "compare tools" or "learn a concept."&lt;/p&gt;

&lt;p&gt;If your AI writer doesn't receive that signal, it's guessing. If it does, it's filling in a template that already provably works.&lt;/p&gt;

&lt;p&gt;So I built the pipeline I wished I had: SERP data in, structured brief out, article generated &lt;em&gt;from&lt;/em&gt; that brief.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, step by step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Pull the live SERP
&lt;/h3&gt;

&lt;p&gt;I use SerpAPI for this — their free tier (250 searches/month) is enough to bootstrap. One call gets you the top 10 organic results with their URLs, titles, and snippets for a target keyword.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_serp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://serpapi.com/search.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;params&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;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organic_results&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fancy. The magic is what you extract next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Extract the content pattern
&lt;/h3&gt;

&lt;p&gt;For each ranking result, I fetch the page and pull out the bones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Word count&lt;/strong&gt; — if all 10 results are 2,400+ words and you generate 800, you lose before you start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H2/H3 outline&lt;/strong&gt; — which section headings repeat across results? Those are the subtopics the intent demands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entity coverage&lt;/strong&gt; — which product names, features, and concepts appear in most results? The model needs to know these are table stakes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent classification&lt;/strong&gt; — comparison posts, listicles, how-to guides, and concept explainers have different skeletons. A keyword like "asana vs monday" wants tables and pricing sections; "what is kanban" wants definitions and diagrams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output is a structured brief that looks something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"asana vs monday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"intent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comparison"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_word_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required_sections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"quick comparison table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"pricing breakdown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"feature differences"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"use-case fit"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"must_mention_entities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"timelines"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workload management"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"free tier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"automation rules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integrations"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This brief is the whole ballgame. It's the difference between "write an article about Asana vs Monday" and "here is exactly what the top 10 results collectively look like — match the pattern, then add something they missed."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Generate with the brief as context
&lt;/h3&gt;

&lt;p&gt;The prompt construction matters more than the model choice. My system prompt doesn't ask for an article — it asks the model to &lt;em&gt;fill a structural slot&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are writing one section of a B2B comparison article.

Keyword: {keyword}
Search intent: {intent}
This section: {section_title}
Cover these entities where natural: {entities}
The other sections in this article cover: {other_sections}
Do not repeat their content.

Write 150-250 words. No filler. No "in conclusion".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generating section by section instead of all at once does two things: each call stays well under token limits, and you can regenerate one weak section without touching the rest.&lt;/p&gt;

&lt;p&gt;I run DeepSeek as the default engine — at ~120 tokens/sec it's fast enough that a full article lands in about a minute and a half. But here's a decision I'd recommend to anyone building similar tooling: &lt;strong&gt;make the provider swappable from day one.&lt;/strong&gt; My pipeline takes three env vars (base URL, API key, model name) and works with any OpenAI-compatible API. When a better or cheaper model ships next month, it's a config change, not a rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: The gotchas that cost me a weekend
&lt;/h3&gt;

&lt;p&gt;Three things the tutorials don't mention:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts will lie to you.&lt;/strong&gt; A 90-second generation timeout sounds generous until you're generating five sections sequentially and one hangs at 89 seconds. Set the timeout per-section, retry once, and if a section fails twice, generate it standalone later — don't block the whole article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Max tokens is not a suggestion.&lt;/strong&gt; DeepSeek's 8192 max_tokens cap will silently truncate a long generation mid-sentence if you're not explicit. Section-by-section generation mostly sidesteps this, but set the cap anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured output drifts.&lt;/strong&gt; If you ask for JSON and the model wraps it in markdown fences "to be helpful," your parser dies. Strip fences before parsing, always. (&lt;code&gt;resp.strip().removeprefix("&lt;/code&gt;&lt;code&gt;json").removesuffix("&lt;/code&gt;&lt;code&gt;")&lt;/code&gt; has earned its place in my codebase.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it work?
&lt;/h2&gt;

&lt;p&gt;The honest answer: it's early, and one pipeline can't fix a brand-new domain with zero backlinks. But the articles generated from SERP briefs are visibly different from vacuum-generated ones — they cover the subtopics the intent demands, they're the right length, and they include the entities that would look conspicuous by their absence.&lt;/p&gt;

&lt;p&gt;The comparison articles from my batch that were closest to the SERP pattern were also the only ones that cracked the top 50. Small sample, but the direction matches what every content strategist says offline: meet the intent, then differentiate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with it
&lt;/h2&gt;

&lt;p&gt;I packaged the whole pipeline into &lt;a href="https://serpcraft.io" rel="noopener noreferrer"&gt;SerpCraft&lt;/a&gt; — you paste a keyword, it pulls the live SERP, extracts the pattern, and drafts the article section by section. There's a free tier (3 analyses/month, no card) if you want to see the brief it generates for one of your keywords — even if you write the article yourself, the extracted pattern is a decent editorial checklist.&lt;/p&gt;

&lt;p&gt;The interesting questions now are all on the distribution side, which is a different kind of hard. If you've taken an AI content tool from zero to actual users, I'd genuinely like to hear what worked.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building in public, slowly. Stack: Next.js 14, Supabase, DeepSeek (swappable), Cloudflare Pages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>webdev</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>$0 a Month AI Tool Stack for Small Business — Actually Free, No Credit Card</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Fri, 31 Jul 2026 16:22:29 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/0-a-month-ai-tool-stack-for-small-business-actually-free-no-credit-card-36jo</link>
      <guid>https://dev.to/toolkitcreators/0-a-month-ai-tool-stack-for-small-business-actually-free-no-credit-card-36jo</guid>
      <description>&lt;h2&gt;
  
  
  I Ran My Business on $0 AI Tools for 4 Months
&lt;/h2&gt;

&lt;p&gt;I assumed running a business with AI tools would cost hundreds monthly. Then I tried the free tier of everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  My $0 Stack (That Actually Works)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT Free&lt;/strong&gt; — general tasks, unlimited light model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Free&lt;/strong&gt; — best long-form writing, reads human&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canva Free&lt;/strong&gt; — social graphics, presentations, 80% of design needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CapCut&lt;/strong&gt; — free video editor, honestly replaces paid tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; — massive 1M-token context, Google ecosystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grammarly Free&lt;/strong&gt; — catches embarrassing typos everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Otter.ai Free&lt;/strong&gt; — 300 transcription minutes per month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buffer/Mailchimp&lt;/strong&gt; — basic social + email on free tiers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;8 tools. $0 total.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Upgrade Rule
&lt;/h2&gt;

&lt;p&gt;Only pay when a tool saves you more time than it costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap
&lt;/h2&gt;

&lt;p&gt;Most people sign up for paid plans immediately. The free tier covers 80% of solo creator needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Guide
&lt;/h2&gt;

&lt;p&gt;Complete breakdown here: &lt;a href="https://toolkitcreators.com/guides/free-ai-tools-business.html" rel="noopener noreferrer"&gt;Free AI Tools for Business&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is in your free tool stack? Drop it below!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The AI Writing Tool Nobody Talks About (And Why It Is Better Than Jasper)</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Fri, 31 Jul 2026 15:29:34 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/the-ai-writing-tool-nobody-talks-about-and-why-it-is-better-than-jasper-4gl4</link>
      <guid>https://dev.to/toolkitcreators/the-ai-writing-tool-nobody-talks-about-and-why-it-is-better-than-jasper-4gl4</guid>
      <description>&lt;h2&gt;
  
  
  The Tool Everyone Misses
&lt;/h2&gt;

&lt;p&gt;Everyone recommends Jasper. But Claude is the tool I keep returning to.&lt;/p&gt;

&lt;p&gt;Not because it is cheaper ($20 vs $49). Because the writing quality is just better. Claude reads human. Jasper reads like Jasper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hidden Cost: Editing Time
&lt;/h2&gt;

&lt;p&gt;I timed 6 tools on a 1,500-word article. Here is the editing time required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude: 25 min&lt;/li&gt;
&lt;li&gt;Jasper: 30 min&lt;/li&gt;
&lt;li&gt;Writesonic: 35 min&lt;/li&gt;
&lt;li&gt;Copy.ai: 40 min&lt;/li&gt;
&lt;li&gt;Rytr: 45 min — yes, the $9/mo tool took the most editing time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The $9 tool actually costs 3+ hours more per month.&lt;/strong&gt; Cheaper ≠ better.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Setup
&lt;/h2&gt;

&lt;p&gt;Claude for research + Jasper for draft = 70 minutes total per 2,000-word article. Manual writing takes 4-6 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool-by-Tool Breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;Natural writing, long-form&lt;/td&gt;
&lt;td&gt;No SEO templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jasper&lt;/td&gt;
&lt;td&gt;SEO + team workflows&lt;/td&gt;
&lt;td&gt;Makes up statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copy.ai&lt;/td&gt;
&lt;td&gt;Multi-format output&lt;/td&gt;
&lt;td&gt;Long-form inconsistent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writesonic&lt;/td&gt;
&lt;td&gt;Solid middle-ground&lt;/td&gt;
&lt;td&gt;Generic tone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rytr&lt;/td&gt;
&lt;td&gt;Short-form, budget&lt;/td&gt;
&lt;td&gt;Breaks past 1K words&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Full Comparison
&lt;/h2&gt;

&lt;p&gt;I put all my testing data with pricing and alternatives here: &lt;a href="https://toolkitcreators.com/guides/best-ai-writing-tools.html" rel="noopener noreferrer"&gt;AI Writing Tools Compared&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;70 min/article vs 4-6 hrs. Which tools do you use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What AI writing tools have you tested? Drop your experience below!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Spent 3 Months Testing AI Video Tools - Stack That Actually Works in 2026</title>
      <dc:creator>孙永瑞</dc:creator>
      <pubDate>Fri, 31 Jul 2026 15:28:41 +0000</pubDate>
      <link>https://dev.to/toolkitcreators/i-spent-3-months-testing-ai-video-tools-stack-that-actually-works-in-2026-3mci</link>
      <guid>https://dev.to/toolkitcreators/i-spent-3-months-testing-ai-video-tools-stack-that-actually-works-in-2026-3mci</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Video Production Takes Too Long
&lt;/h2&gt;

&lt;p&gt;Making videos used to take me &lt;strong&gt;13 hours per video&lt;/strong&gt;. Between scripting, recording, finding B-roll, editing, and repurposing.&lt;/p&gt;

&lt;p&gt;After testing 10+ AI video tools over 3 months, I have cut that to &lt;strong&gt;just 2 hours&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  My AI Video Stack (2026)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scripting → Claude (45 min, was 3 hrs)
&lt;/h3&gt;

&lt;p&gt;Claude produces the most natural scripts. ChatGPT reads like AI. Claude writes human.&lt;/p&gt;

&lt;h3&gt;
  
  
  Voiceover → ElevenLabs (5 min, was 1 hr)
&lt;/h3&gt;

&lt;p&gt;Voice cloning + emotional control + 100 languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  B-Roll → Runway Gen-3 (15 min, was 2 hrs)
&lt;/h3&gt;

&lt;p&gt;Describe the shot. Get the footage. No stock hunting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Editing → Descript (45 min, was 4 hrs)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Biggest time saver.&lt;/strong&gt; Edit transcript like a doc → video edits automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repurposing → Opus Clip (5 min, was 2 hrs)
&lt;/h3&gt;

&lt;p&gt;Auto-cuts long videos into Shorts with captions and viral scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free Alternative (DIY)
&lt;/h2&gt;

&lt;p&gt;ChatGPT + CapCut + Adobe Podcast = $0/month, 80% results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full Breakdown
&lt;/h2&gt;

&lt;p&gt;I put together a complete comparison with pricing tables here: &lt;a href="https://toolkitcreators.com/guides/ai-video-tools.html" rel="noopener noreferrer"&gt;AI Video Tools Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: 13 hours → 2 hours per video.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What tools are you using? Drop them below!&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
