[Model: gpt-oss-120b | Provider: cerebras]
# Scaling Content Production with AI
**A Technical Guide for Marketing Teams, Content Studios, and Enterprise Communications**
*Prepared for: Content strategists, SEO specialists, product managers, and AI‑ops engineers*
*Version: 1.0 – July 2026*
—
## Table of Contents
| # | Section | Approx. Length |
|—|———|—————-|
| 1 | Executive Overview | 300 w |
| 2 | Foundations: Choosing the Right Model | 400 w |
| 3 | Prompt Engineering for Consistent Quality | 800 w |
| 4 | End‑to‑End Content Production Workflow | 800 w |
| 5 | SEO‑First Prompt Design & Rank‑Ready Output | 600 w |
| 6 | Automated Fact‑Checking & Source Attribution | 500 w |
| 7 | Human‑In‑the‑Loop (HITL) Editing & Quality Gates | 600 w |
| 8 | Content Calendar Automation & Scheduling | 400 w |
| 9 | Governance, Security, and Compliance | 300 w |
| 10 | KPI Dashboard & Continuous Improvement | 300 w |
| **Total** | **≈ 3 800 words** | |
> **Goal:** Provide a reproducible, code‑ready blueprint that lets a team of 5–10 people generate 200+ SEO‑optimized articles per month while maintaining a human‑grade quality bar (≥ 90 % “publish‑ready” on first review).
—
## 1. Executive Overview (≈ 300 words)
In 2024‑2026 the cost of generating a high‑quality, SEO‑friendly article with a large language model (LLM) dropped below **$0.02 per 1 000 tokens** on major cloud providers. This makes AI the cheapest “author” on the planet, but the **bottleneck has shifted** from raw generation to **prompt consistency, data hygiene, and human oversight**.
This guide takes you from a single‑author proof‑of‑concept to a **scalable production line** that:
1. **Standardizes prompts** so the model always respects tone, structure, length, and branding.
2. **Injects SEO signals** (keyword placement, schema markup, internal linking) directly in the prompt.
3. **Runs automated fact‑checking** against trusted knowledge bases (e.g., Wikipedia, internal data lakes, commercial APIs).
4. **Routes every draft through a human‑editing pipeline** that applies style guides, plagiarism checks, and final approval.
5. **Feeds the output into a dynamic content calendar** that auto‑populates publishing platforms (WordPress, Contentful, HubSpot) and tracks performance.
The result is a **repeatable, measurable system** that can be expanded horizontally (more topics, languages, channels) or vertically (deep‑dive guides, multimedia assets) without re‑architecting the core workflow.
—
## 2. Foundations: Choosing the Right Model (≈ 400 words)
| Requirement | Recommended Model (as of July 2026) | Why it fits |
|————-|————————————–|————–|
| **General‑purpose English** | **Claude 3.5 Sonnet** (Anthropic) | Strong instruction following, low hallucination, token‑efficient (≈ 2 × cheaper than GPT‑4 Turbo). |
| **Multilingual (30+ languages)** | **Gemini 1.5 Pro** (Google) | 200 % better coverage on non‑English corpora, built‑in translation primitives. |
| **Fine‑tuning required** | **LLaMA‑3‑8B Instruct** (Meta, open‑source) | Easy to LoRA‑tune on proprietary datasets; can be hosted on on‑prem GPU clusters for data‑privacy. |
| **Very short latency (sub‑500 ms)** | **Mistral‑NeMo 7B** (Mistral AI) | Optimized for inference on NVIDIA H100; good for real‑time content suggestions in UI. |
**Decision Matrix** (example for a US‑based SaaS marketing team):
| Factor | Weight | Claude 3.5 Sonnet | Gemini 1.5 Pro | LLaMA‑3‑8B |
|——–|——–|——————|—————-|———–|
| Cost per 1 k tokens | 30 % | 0.018 | 0.022 | 0.016 |
| Hallucination rate (A/B test) | 25 % | 0.9 % | 1.3 % | 1.1 % |
| SEO‑specific output quality | 20 % | ★★★★ | ★★★ | ★★★★ |
| Fine‑tuning flexibility | 15 % | ★★ | ★ | ★★★★★ |
| Data‑privacy compliance | 10 % | ★★★★★ | ★★★★ | ★★★★★ |
| **Score** | – | **4.2** | **3.6** | **4.5** |
*Result*: For most B2B SaaS teams the **Claude 3.5 Sonnet** is the sweet spot; the **LLaMA‑3‑8B** variant is used when internal data cannot leave the firewall.
### 2.1 Model Access & Cost Management
1. **API Key Vault** – Store keys in a secret manager (AWS Secrets Manager, GCP Secret Manager, or HashiCorp Vault). Rotate every 90 days.
2. **Budget Guardrails** – Deploy a **usage‑monitoring Lambda** that aborts requests when monthly cost > $2 000 (adjustable).
3. **Token‑Level Billing** – Enable **“max_tokens”** per request (e.g., 1 200 for a 800‑word article). This caps both cost and risk of runaway generation.
### 2.2 Prompt‑Caching (Advanced)
If you produce many articles around the same **topic cluster**, cache the “system prompt” (the set of instructions that never changes) using the **OpenAI‑style `prompt_cache_id`** or Anthropic’s **`cached_prompt`** field. This reduces token usage by ~ 15 % and improves latency.
—
## 3. Prompt Engineering for Consistent Quality (≈ 800 words)
### 3.1 Prompt Anatomy
A robust prompt is a **four‑layered template**:
| Layer | Purpose | Example |
|——-|———|———-|
| **System Prompt** (static) | Imposes brand voice, style guide, compliance constraints. | “You are a senior copywriter for **Acme Cloud**, a B2B SaaS company. Write in a professional, conversational tone. Avoid marketing buzzwords like ‘disruptive’, ‘synergy’, ‘game‑changing’. All statements must be verifiable.” |
| **Context Prompt** (dynamic) | Supplies research, keyword list, outline, and any data tables. | “Topic: ‘Zero‑Trust Network Access’. Primary keyword: ‘Zero‑Trust Network Access’. Secondary keywords: ‘ZTNA’, ‘software‑defined perimeter’, ‘remote workforce security’. Include these sub‑headings: …” |
| **Task Prompt** (instruction) | Defines output format, length, SEO requirements. | “Write a 1 200‑word article, using markdown. Include: 1) an H1 title with primary keyword, 2) an opening paragraph of ≤ 150 words, 3) a bullet‑list of 5 key benefits, 4) a 2‑sentence conclusion, 5) a meta description ≤ 155 characters, 6) a table of related FAQs (3 rows). Use the primary keyword in the H1, first paragraph, and once in the conclusion.” |
| **Output Guardrails** (post‑processor) | JSON schema for downstream parsing. | `{ “title”: “…”, “meta_description”: “…”, “sections”: [{ “heading”: “…”, “body”: “…” }], “faq”: [{ “question”: “…”, “answer”: “…” }] }` |
> **Tip:** Keep each layer under **2 000 tokens**. The total request (system + context + task) should stay under **8 000 tokens** to avoid truncation.
### 3.2 Prompt Templates (Reusable Snippets)
Below are **exact prompts** you can copy‑paste into your orchestration engine (e.g., Airflow, Prefect, or a custom Node.js microservice). Replace placeholders in `{{ }}` with runtime values.
#### 3.2.1 System Prompt – Brand Voice
“`text
You are a senior content strategist for {{company_name}}.
Company description: {{company_description}}.
Write in a tone that is:
– Professional yet approachable
– Data‑driven, avoiding vague superlatives
– Inclusive (use gender‑neutral pronouns unless a specific gender is required)
Do NOT:
– Use marketing buzzwords: “disruptive”, “synergy”, “game‑changing”, “cutting‑edge”
– Mention competitor names
– Produce unverified claims
All claims must be backed by a reputable source (cite with a URL). Output must be in valid JSON matching the schema provided later.
“`
#### 3.2.2 Context Prompt – Research Package
“`json
{
“topic”: “{{topic}}”,
“primary_keyword”: “{{primary_keyword}}”,
“secondary_keywords”: [“{{kw2}}”, “{{kw3}}”, “{{kw4}}”],
“outline”: [
{“heading”:”{{h1}}”,”subheadings”:[“{{h1_s1}}”,”{{h1_s2}}”]},
{“heading”:”{{h2}}”,”subheadings”:[“{{h2_s1}}”]},
{“heading”:”{{h3}}”,”subheadings”:[]}
],
“reference_links”: [
“https://en.wikipedia.org/wiki/Zero‑trust_network_access”,
“https://www.nist.gov/publications/zero-trust-architecture”
],
“brand_facts”: {
“product_name”:”Acme Zero‑Trust”,
“release_year”:2024,
“unique_value”:”Identity‑centric security without VPN”
}
}
“`
#### 3.2.3 Task Prompt – SEO & Formatting
“`text
Write a **markdown** article of **≈ 1 200 words** based on the research package.
Requirements:
1. Title (H1) must contain the primary keyword exactly once.
2. Opening paragraph (≤ 150 words) must include the primary keyword and one secondary keyword.
3. Use the provided outline for headings (H2) and sub‑headings (H3). Insert at least one internal link per H2 (use placeholder `{{internal_link}}` that will be replaced later).
4. Include a bullet‑list of **5 benefits** under the heading “Key Benefits”.
5. Insert a **table** (Markdown) titled “Feature Comparison” with three rows and two columns (Feature, Acme, Competitor).
6. End with a conclusion (2 sentences) that restates the primary keyword.
7. Provide a **meta description** (≤ 155 characters) that includes the primary keyword.
8. Provide **3 FAQs** (questions should contain secondary keywords) with concise answers (≤ 75 words each).
Output must be a JSON object matching the following schema:
{
“title”: “string”,
“meta_description”: “string”,
“sections”: [
{“heading”:”string”,”body”:”string”},
…
],
“benefits”: [“string”, …],
“comparison_table”: “string (markdown)”,
“conclusion”: “string”,
“faqs”: [{“question”:”string”,”answer”:”string”}, …]
}
“`
### 3.3 Prompt Tuning Techniques
| Technique | When to Use | Implementation Steps |
|———–|————-|———————-|
| **Few‑Shot Examples** | You need the model to follow a very specific format (e.g., JSON). | Append 2–3 **complete** examples (prompt + output) before the actual request. |
| **Chain‑of‑Thought (CoT)** | Complex reasoning (e.g., “explain why X is better than Y”). | Add “**Think step‑by‑step**:” before the final answer request. |
| **Self‑Consistency** | Reducing variance across multiple generations. | Generate **N=3** completions, parse each JSON, and select the one with highest internal consistency score (e.g., matching schema). |
| **Instruction Prefixes** | Enforcing brand voice across many topics. | Prepend “**[Brand Voice]**” token to the system prompt – many LLM providers expose special tokens for brand alignment. |
| **Dynamic Temperature** | For creative sections (e.g., opening hook) use higher temperature (0.8). For factual sections use low temperature (0.2). | Split the article generation into **two calls**: (1) Hook (2) Body. Or use a **function calling** approach where the model requests a “generate_hook” function. |
### 3.4 Prompt Library Management
1. **Version Control** – Store prompts in a Git repository (`/prompts/`). Tag each change with a semantic version (`v1.2.0‑brand‑voice`).
2. **Testing Harness** – Create an automated test that runs the prompt against a **mock context** and validates the JSON schema using `ajv` (Node) or `jsonschema` (Python).
3. **Roll‑out Strategy** – Deploy new prompt versions via **feature flags** (e.g., `PROMPT_VERSION=2024‑09‑brand`). This lets you A/B test across 10 % of traffic before full rollout.
—
## 4. End‑to‑End Content Production Workflow (≈ 800 words)
Below is a **pipeline diagram** (textual) and a concrete **Airflow DAG** you can copy‑paste. The workflow is modular; each step can be swapped for a different orchestrator (Prefect, Temporal, or custom Kubernetes CronJob).
### 4.1 High‑Level Flow
“`
[Topic Ideation] → [Research Aggregation] → [Prompt Generation] → [LLM Inference] →
[Automated Fact‑Check] → [Internal Link Insertion] → [Human Review] →
[CMS Ingestion] → [Publishing Scheduler] → [Performance Monitoring]
“`
### 4.2 Detailed Step Descriptions
| Step | Input | Process | Output |
|——|——-|———|——–|
| **1. Topic Ideation** | Keyword research CSV (Ahrefs, SEMrush) | Filter by search volume > 1 000, difficulty < 30, brand relevance. | `topic_queue.json` (list of topics). |
| **2. Research Aggregation** | Topic | Run **SerpAPI** or **Google Custom Search** to pull top‑5 URLs; extract snippets via **BeautifulSoup**; store in `research/{{topic_id}}.json`. | JSON with `reference_links` and raw excerpt text. |
| **3. Prompt Generation** | Research JSON + Prompt Templates | Jinja2 renders system, context, task prompts. | `prompt/{{topic_id}}.txt`. |
| **4. LLM Inference** | Prompt txt | Call Claude 3.5 Sonnet (or chosen model) with **max_tokens=1500**, **temperature=0.3**. | Raw model response (`response/{{topic_id}}.json`). |
| **5. Automated Fact‑Check** | Model response + reference_links | Run **FactCheck‑GPT** (open‑source tool) that extracts claims, searches the web via **Bing API**, returns confidence scores. | `facts/{{topic_id}}.json` (each claim with ✅/❌ and source). |
| **6. Internal Link Insertion** | Draft + Site map | Use a **link‑mapper** service that reads the corporate sitemap, selects the most relevant internal page, and injects markdown links. | `draft_with_links/{{topic_id}}.json`. |
| **7. Human Review** | Draft with links | Assign to editor via **Asana** or **Jira** ticket. Editor runs a checklist (style, plagiarism, SEO). | `final/{{topic_id}}.md`. |
| **8. CMS Ingestion** | Final markdown | Use **WordPress REST API** (or Contentful) to create a draft post, attach meta fields, and set `status=private`. | `cms_id`. |
| **9. Publishing Scheduler** | CMS draft + Calendar | Cron job publishes on scheduled date, updates sitemap, sends Slack notification. | Live URL. |
| **10. Performance Monitoring** | Live URL | Pull Google Search Console & Analytics metrics nightly; feed back to KPI dashboard. | Dashboard refresh. |
### 4.3 Example Airflow DAG (Python)
```python
# file: dags/content_production.py
from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.models import Variable
default_args = {
"owner": "content-team",
"depends_on_past": False,
"email_on_failure": True,
"email": ["ops@acme.com"],
"retries": 2,
"retry_delay": timedelta(minutes=5),
}
dag = DAG(
"content_production",
default_args=default_args,
description="End‑to‑end AI‑assisted content pipeline",
schedule_interval="0 2 * * *", # nightly at 02:00 UTC
start_date=datetime(2024, 1, 1),
catchup=False,
tags=["ai", "seo"],
)
def load_topic_queue(**kwargs):
import pandas as pd, json, os
df = pd.read_csv("/opt/data/keyword_report.csv")
# filter logic
topics = (
df.query("search_volume > 1000 and difficulty < 30")
.assign(topic=lambda x: x["keyword"])
.to_dict(orient="records")
)
with open("/tmp/topic_queue.json", "w") as f:
json.dump(topics, f)
def fetch_research(**kwargs):
import json, requests, os
from bs4 import BeautifulSoup
with open("/tmp/topic_queue.json") as f:
topics = json.load(f)
for t in topics:
query = t["topic"]
# SerpAPI call (mocked)
resp = requests.get("https://serpapi.com/search", params={"q": query, "num": 5})
urls = [r["link"] for r in resp.json()["organic_results"]]
snippets = []
for u in urls:
html = requests.get(u).text
soup = BeautifulSoup(html, "html.parser")
[Continued with Model: gpt-oss-120b | Provider: cerebras]
```python
# Grab the first paragraph that contains the keyword (simple heuristic)
paragraph = next(
(p.get_text() for p in soup.find_all("p") if query.lower() in p.get_text().lower()),
""
)
snippets.append({"url": u, "excerpt": paragraph[:500]})
research = {
"topic": query,
"primary_keyword": query,
"secondary_keywords": [], # will be filled later
"reference_links": urls,
"snippets": snippets,
}
out_path = f"/tmp/research/{query.replace(' ', '_')}.json"
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w") as f:
json.dump(research, f, ensure_ascii=False, indent=2)
def render_prompts(**kwargs):
import json, os, jinja2
# Load Jinja templates from the repo
env = jinja2.Environment(
loader=jinja2.FileSystemLoader("/opt/prompts"),
autoescape=False,
trim_blocks=True,
lstrip_blocks=True,
)
sys_tpl = env.get_template("system_prompt.txt")
ctx_tpl = env.get_template("context_prompt.json")
task_tpl = env.get_template("task_prompt.txt")
# Brand‑wide variables (could be stored in Airflow Variables)
brand = {
"company_name": Variable.get("company_name", default_var="Acme Cloud"),
"company_description": Variable.get(
"company_desc",
default_var="We provide cloud‑native security solutions for enterprises."
),
}
for fname in os.listdir("/tmp/research"):
with open(f"/tmp/research/{fname}") as f:
research = json.load(f)
# ----- System prompt (static) -----
system_prompt = sys_tpl.render(**brand)
# ----- Context prompt (dynamic) -----
# Pull secondary keywords from a CSV that maps topics → LSI keywords
lsi_map = json.load(open("/opt/data/lsi_keywords.json"))
secondary = lsi_map.get(research["topic"], [])
context_prompt = ctx_tpl.render(
topic=research["topic"],
primary_keyword=research["primary_keyword"],
kw2=secondary[0] if len(secondary) > 0 else “”,
kw3=secondary[1] if len(secondary) > 1 else “”,
kw4=secondary[2] if len(secondary) > 2 else “”,
outline=[
{“heading”: “What Is ” + research[“topic”], “subheadings”: []},
{“heading”: “Why Zero‑Trust Matters”, “subheadings”: []},
{“heading”: “How Acme Zero‑Trust Works”, “subheadings”: []},
{“heading”: “Implementation Steps”, “subheadings”: []},
{“heading”: “Key Benefits”, “subheadings”: []},
{“heading”: “FAQ”, “subheadings”: []},
],
reference_links=research[“reference_links”],
brand_facts={
“product_name”: “Acme Zero‑Trust”,
“release_year”: 2024,
“unique_value”: “Identity‑centric security without VPN”,
},
)
# —– Task prompt (SEO & formatting) —–
task_prompt = task_tpl.render()
# Combine all three parts into a single payload for the LLM
payload = {
“model”: “claude-3.5-sonnet-20241022”,
“max_tokens”: 1500,
“temperature”: 0.3,
“system”: system_prompt,
“messages”: [
{“role”: “user”, “content”: context_prompt},
{“role”: “assistant”, “content”: task_prompt},
],
}
out_path = f”/tmp/prompts/{research[‘topic’].replace(‘ ‘, ‘_’)}.json”
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, “w”) as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
def call_llm(**kwargs):
import json, os, requests
api_key = Variable.get(“anthropic_api_key”)
headers = {“x-api-key”: api_key, “content-type”: “application/json”}
for fname in os.listdir(“/tmp/prompts”):
with open(f”/tmp/prompts/{fname}”) as f:
payload = json.load(f)
response = requests.post(
“https://api.anthropic.com/v1/messages”,
headers=headers,
json=payload,
)
response.raise_for_status()
data = response.json()
# The model returns a JSON string inside `content[0].text`
raw_output = data[“content”][0][“text”]
# Store raw output for fact‑checking
out_path = f”/tmp/raw_output/{fname.replace(‘.json’, ”)}.txt”
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, “w”) as f:
f.write(raw_output)
def fact_check(**kwargs):
import json, os, subprocess, re
# We will use the open‑source `factcheck-gpt` CLI (installed in the worker image)
for fname in os.listdir(“/tmp/raw_output”):
raw_path = f”/tmp/raw_output/{fname}”
# The CLI expects a plain‑text file and returns a JSON with claim‑level scores
result = subprocess.run(
[“factcheck-gpt”, “–input”, raw_path, “–output”, “-“],
capture_output=True,
text=True,
check=True,
)
claims = json.loads(result.stdout)
# Persist claims for the editor UI
out_path = f”/tmp/factcheck/{fname.replace(‘.txt’, ”)}.json”
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, “w”) as f:
json.dump(claims, f, indent=2)
def insert_internal_links(**kwargs):
import json, os, random
# Load a simple sitemap (CSV: url,topic)
sitemap = {}
with open(“/opt/data/sitemap.csv”) as f:
for line in f:
url, topic = line.strip().split(“,”)
sitemap.setdefault(topic.lower(), []).append(url)
for fname in os.listdir(“/tmp/factcheck”):
base = fname.replace(“.json”, “”)
raw_path = f”/tmp/raw_output/{base}.txt”
with open(raw_path) as f:
content = f.read()
# Very naive link insertion: replace each secondary keyword with a markdown link
# In production you’d use a more sophisticated NER + anchor‑text selector.
# Example: replace “Zero‑Trust Network Access” with “[Zero‑Trust Network Access](/zero-trust)”
# We’ll pick a random internal page from the same topic bucket.
topic = base.replace(“_”, ” “)
candidates = sitemap.get(topic.lower(), [])
if candidates:
anchor = random.choice(candidates)
content = re.sub(
r”\bZero‑Trust Network Access\b”,
f”[Zero‑Trust Network Access]({anchor})”,
content,
flags=re.IGNORECASE,
)
# Store enriched draft
out_path = f”/tmp/draft_with_links/{base}.md”
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, “w”) as f:
f.write(content)
def create_review_task(**kwargs):
import json, os, requests
# Simple Asana integration – you could swap this for Jira, ClickUp, etc.
asana_token = Variable.get(“asana_pat”)
headers = {“Authorization”: f”Bearer {asana_token}”, “Content-Type”: “application/json”}
project_gid = Variable.get(“asana_project_gid”)
for fname in os.listdir(“/tmp/draft_with_links”):
with open(f”/tmp/draft_with_links/{fname}”) as f:
markdown = f.read()
# Build a minimal task description
task_payload = {
“data”: {
“name”: f”Review AI article – {fname.replace(‘.md’, ”).replace(‘_’, ‘ ‘)}”,
“notes”: markdown,
“projects”: [project_gid],
“assignee”: Variable.get(“editor_asana_gid”),
“custom_fields”: {
# Example custom field IDs for “SEO Score” and “Fact‑Check Pass”
“1201234567890”: 0, # SEO Score (numeric)
“1201234567891”: False, # Fact‑Check Pass (boolean)
},
}
}
resp = requests.post(“https://app.asana.com/api/1.0/tasks”, headers=headers, json=task_payload)
resp.raise_for_status()
def publish_to_cms(**kwargs):
import json, os, requests
wp_user = Variable.get(“wp_user”)
wp_pass = Variable.get(“wp_password”)
wp_endpoint = Variable.get(“wp_rest_endpoint”) # e.g., https://acme.com/wp-json/wp/v2/posts
auth = (wp_user, wp_pass)
for fname in os.listdir(“/tmp/draft_with_links”):
with open(f”/tmp/draft_with_links/{fname}”) as f:
markdown = f.read()
# Extract title from first H1 line
title = markdown.split(“\n”)[0].replace(“# “, “”).strip()
# The meta description is part of the JSON output in the original prompt; we’ll pull it from the raw output if present
raw_path = f”/tmp/raw_output/{fname.replace(‘.md’, ”)}.txt”
with open(raw_path) as f_raw:
raw = f_raw.read()
# Regex‑extract meta description JSON field
import re
meta_match = re.search(r’”meta_description”\s*:\s*”([^”]+)”‘, raw)
meta_desc = meta_match.group(1) if meta_match else “”
payload = {
“title”: title,
“content”: markdown,
“status”: “draft”,
“excerpt”: meta_desc,
“format”: “standard”,
}
resp = requests.post(wp_endpoint, auth=auth, json=payload)
resp.raise_for_status()
post_id = resp.json()[“id”]
# Save mapping for the scheduler
with open(“/tmp/published_mapping.json”, “a”) as f:
f.write(json.dumps({“topic”: fname.replace(“.md”, “”), “wp_id”: post_id}) + “\n”)
def schedule_publication(**kwargs):
import json, datetime, requests
# This step runs daily at 06:00 UTC after the Content Calendar has been updated.
# It reads the mapping file and publishes any post whose scheduled date <= today.
today = datetime.date.today().isoformat()
with open("/opt/data/content_calendar.json") as f:
calendar = json.load(f) # [{ "topic": "...", "publish_date": "2026-07-15", "wp_id": 123 }]
wp_endpoint = Variable.get("wp_rest_endpoint")
wp_user = Variable.get("wp_user")
wp_pass = Variable.get("wp_password")
auth = (wp_user, wp_pass)
for entry in calendar:
if entry["publish_date"] <= today and entry.get("published", False) is False:
post_url = f"{wp_endpoint}/{entry['wp_id']}"
resp = requests.post(post_url, auth=auth, json={"status": "publish"})
resp.raise_for_status()
# Mark as published (in the JSON file – in prod you’d use a DB)
entry["published"] = True
# Overwrite calendar with updated flags
with open("/opt/data/content_calendar.json", "w") as f:
json.dump(calendar, f, indent=2)
# ----------------------------------------------------------------------
# DAG wiring
# ----------------------------------------------------------------------
load_topic_queue = PythonOperator(task_id="load_topic_queue", python_callable=load_topic_queue, dag=dag)
fetch_research = PythonOperator(task_id="fetch_research", python_callable=fetch_research, dag=dag)
render_prompts = PythonOperator(task_id="render_prompts", python_callable=render_prompts, dag=dag)
call_llm = PythonOperator(task_id="call_llm", python_callable=call_llm, dag=dag)
fact_check = PythonOperator(task_id="fact_check", python_callable=fact_check, dag=dag)
insert_links = PythonOperator(task_id="insert_links", python_callable=insert_internal_links, dag=dag)
create_review = PythonOperator(task_id="create_review_task", python_callable=create_review_task, dag=dag)
publish_cms = PythonOperator(task_id="publish_to_cms", python_callable=publish_to_cms, dag=dag)
schedule_pub = PythonOperator(task_id="schedule_publication", python_callable=schedule_publication, dag=dag)
# Define linear dependencies
(
load_topic_queue
>> fetch_research
>> render_prompts
>> call_llm
>> fact_check
>> insert_links
>> create_review
>> publish_cms
>> schedule_pub
)
“`
**Explanation of critical choices**
| Step | Why it matters | Recommended tuning |
|——|—————-|——————–|
| **Prompt rendering** | Keeps prompt files version‑controlled; Jinja allows easy variable substitution. | Cache rendered prompts for identical topics to avoid re‑rendering. |
| **LLM temperature = 0.3** | Low temperature reduces hallucinations for factual sections. | For “hook” generation you could spin a second call with `temperature=0.8` and then stitch the two parts together. |
| **Fact‑check CLI** | Off‑loads claim verification to a deterministic process; you can run it in parallel across many workers. | Adjust the confidence threshold (default 0.8) based on your brand risk appetite. |
| **Internal link insertion** | Improves PageRank flow and SEO relevance. | Use a **graph‑based anchor‑text selector** (e.g., Neo4j) for large sites. |
| **Human review task** | Guarantees a final quality gate and captures editorial feedback for future prompt refinements. | Add custom fields for “SEO Score” (0‑100) and “Fact‑Check Pass”. |
| **CMS publishing** | Storing drafts in the CMS allows the editor to preview how the article looks on the site before scheduling. | Enable **auto‑save** every 30 seconds for large drafts to avoid data loss. |
—
## 5. SEO‑First Prompt Design & Rank‑Ready Output (≈ 600 words)
### 5.1 SEO Foundations to Encode in the Prompt
| SEO Element | How to encode (prompt snippet) | Why it matters |
|————-|——————————–|—————-|
| **Primary Keyword Placement** | “Title (H1) must contain **{{primary_keyword}}** exactly once.” | Boosts relevance signal for the target query. |
| **Secondary Keywords** | “Include each secondary keyword at least once in a sub‑heading or bolded phrase.” | Captures long‑tail traffic and LSI relevance. |
| **Meta Description** | “Write a meta description ≤ 155 characters that includes the primary keyword.” | Directly displayed in SERPs; improves CTR. |
| **Schema Markup** | “Provide a JSON‑LD snippet for an `FAQPage` using the FAQ section.” | Helps Google surface rich results. |
| **Internal Linking** | “Insert one internal link per H2 using the placeholder `{{internal_link}}`.” | Distributes link equity and reduces bounce rate. |
| **Word Count** | “Target 1 200 ± 100 words.” | Aligns with average word count of top‑10 results. |
| **Readability** | “Write at a Flesch‑Kincaid grade level of 8 or lower.” | Improves user experience; Google favors easy‑to‑read content. |
| **Image Alt Text** | “Suggest an alt‑text for an illustrative image, containing the primary keyword.” | Accessibility + SEO. |
### 5.2 Full SEO‑Optimized Prompt (Copy‑Paste)
“`text
You are an expert SEO copywriter for {{company_name}}. Write a **markdown** article that satisfies the following constraints:
1. **Title (H1)** – Must contain the primary keyword **{{primary_keyword}}** exactly once.
2. **Opening paragraph** – ≤ 150 words, includes the primary keyword and **one** secondary keyword.
3. **Word count** – Target **≈ 1 200 words** (± 100). Keep sentences short; aim for a Flesch‑Kincaid grade ≤ 8.
4. **Headings** – Follow the supplied outline (H2 and H3). Each H2 must contain **one** internal link to a relevant page on {{company_website}}. Use the placeholder `{{internal_link}}` that will later be replaced with the actual URL.
5. **Keyword distribution** – Primary keyword appears in the title, first paragraph, and conclusion. Each secondary keyword appears **at least once** in a sub‑heading or bolded phrase.
6. **Benefits list** – Under a heading “**Key Benefits**”, provide a bullet list of **exactly 5** benefits. Each bullet must start with a strong verb and reference the primary keyword.
7. **Comparison table** – Insert a markdown table titled “**Feature Comparison**” with three rows (Feature, Acme, Competitor) and two columns. Use the primary keyword in the table caption.
8. **Conclusion** – Two sentences, restating the primary keyword and a call‑to‑action (CTA) that mentions the product name.
9. **Meta description** – ≤ 155 characters, includes the primary keyword and a compelling CTA.
10. **FAQ** – Provide **3** FAQs. Each question must contain a secondary keyword. Answers ≤ 75 words.
11. **JSON‑LD** – Generate a JSON‑LD snippet for an `FAQPage` using the FAQ content.
12. **Image suggestion** – Propose a single illustrative image with an alt‑text that includes the primary keyword.
13. **Plagiarism** – The article must be 100 % original; do not copy sentences from the reference links.
**Output format:** Return a **single JSON object** that matches this schema:
{
“title”: “string”,
“meta_description”: “string”,
“sections”: [
{“heading”:”string”,”body”:”string”},
…
],
“benefits”: [“string”, …],
“comparison_table”: “string (markdown)”,
“conclusion”: “string”,
“faqs”: [{“question”:”string”,”answer”:”string”}, …],
“jsonld_faq”: “string (JSON‑LD)”,
“image”: {“url”:”string (placeholder)”, “alt”:”string”}
}
“`
### 5.3 Embedding Structured Data Directly
When the LLM returns the JSON object, the **`jsonld_faq`** field can be copied verbatim into the `
Leave a Reply