An Astro site’s content lives in markdown files. That structure — predictable, file-based, schema-driven — is exactly what makes it automatable. A headless publishing agent writes those files, validates them, and commits to GitHub without anyone opening a text editor.
The architecture is straightforward: n8n reads a content schedule, calls Claude to write the draft, runs validation, and pushes to git. The site’s CI/CD pipeline handles the rest.
Quick Answer: A headless publishing agent for Astro has four components: a content schedule (Excel or markdown table), an n8n workflow that reads the schedule and calls Claude API, a validation step that checks the output against your schema, and a GitHub push step that commits the new file. The entire pipeline runs on a cron schedule — no human steps required for drafts.
Architecture overview
Schedule file (Excel/Markdown)
↓
n8n reads next post data
↓
n8n calls Claude API → generates markdown draft
↓
n8n runs validation (description length, required fields, category enum)
↓
n8n commits file to GitHub repository
↓
GitHub Actions triggers deploy pipeline
Each step is a separate n8n module. If any step fails, n8n sends a notification and stops — it doesn’t push bad content.
Step 1 — Reading the content schedule
Your content schedule needs to be machine-readable. The simplest format is a markdown table or a Google Sheet with consistent column names.
For a Google Sheet approach, use n8n’s Google Sheets module to read the next row where Status = “Planned” and Date = tomorrow. Extract: ID, Title, Track, Keyword, Category, CTA Target, Slot.
For a markdown table approach, use n8n’s Code node with a simple parsing function:
// Parse markdown table row
const lines = $input.item.json.content.split('\n');
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = tomorrow.toISOString().split('T')[0];
const posts = lines
.filter(line => line.includes('|') && line.includes(tomorrowStr))
.map(line => {
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
return { date: cells[0], id: cells[1], title: cells[2], track: cells[3], keyword: cells[4] };
});
return posts;
Step 2 — Generating the draft with Claude
Add an HTTP Request module to call the Anthropic API. The system prompt is the key component — it defines the output structure your Astro schema requires.
System prompt structure:
You are a blog writer for MoltyFlywheel.com, an AI tools and affiliate marketing site.
Write a complete blog post in Astro markdown format. The post must include:
1. Valid YAML frontmatter with these exact fields: title, description (≤160 chars),
pubDate, category (one of: guide|review|comparison|case-study|tutorial), tags,
keyword, cluster, ogImage, draft: true, featured: false, slot
2. An answer-first opening paragraph (no preamble)
3. A Quick Answer blockquote (> **Quick Answer:** ...)
4. H2/H3 sections with practical content
5. Trade-offs section
6. FAQ section (4-6 questions)
7. CTA block linking to the post's primary CTA target
Write in English. Minimum 1000 words. No fabricated claims. Be specific and practical.
User message:
Write a blog post for this scheduled content:
ID: {{id}}
Title: {{title}}
Track: {{track}}
Keyword: {{keyword}}
Category: {{category}}
CTA Target: {{cta_target}}
Slot: {{slot}}
pubDate: {{date}}
ogImage: /images/blog/{{slug}}-cover.webp
cluster: cluster-{{cluster_prefix}}-{{topic}}
Claude returns the complete markdown file content including frontmatter.
Step 3 — Validation
After Claude generates the draft, run validation before committing. In n8n, use a Code node:
const content = $input.item.json.content;
const errors = [];
// Extract frontmatter
const fmMatch = content.match(/^---\n([\s\S]+?)\n---/);
if (!fmMatch) { errors.push('Missing frontmatter'); }
const fm = fmMatch ? fmMatch[1] : '';
// Check required fields
const required = ['title', 'description', 'pubDate', 'category', 'tags',
'keyword', 'cluster', 'ogImage', 'draft', 'slot'];
required.forEach(field => {
if (!fm.includes(`${field}:`)) errors.push(`Missing field: ${field}`);
});
// Check description length
const descMatch = fm.match(/description: "(.+?)"/);
if (descMatch && descMatch[1].length > 160) {
errors.push(`Description too long: ${descMatch[1].length} chars (max 160)`);
}
// Check category enum
const catMatch = fm.match(/category: (.+)/);
const validCats = ['guide', 'review', 'comparison', 'case-study', 'tutorial'];
if (catMatch && !validCats.includes(catMatch[1].trim())) {
errors.push(`Invalid category: ${catMatch[1].trim()}`);
}
// Check word count
const wordCount = content.split(/\s+/).length;
if (wordCount < 1000) errors.push(`Too short: ${wordCount} words (min 1000)`);
return { errors, valid: errors.length === 0, content };
If valid: false, route to a notification module and stop. If valid: true, proceed to commit.
For validation errors, have n8n send a Telegram message with the error list. You fix it manually or trigger a retry.
Step 4 — Commit to GitHub
Use n8n’s GitHub module or an HTTP Request to the GitHub Contents API:
PUT https://api.github.com/repos/{owner}/{repo}/contents/src/content/blog/{slug}.md
{
"message": "Add {date} {id} draft — {title}",
"content": "{base64_encoded_content}",
"branch": "main"
}
Encode the file content as base64 before sending. n8n’s Code node handles this:
const content = $input.item.json.content;
const encoded = Buffer.from(content).toString('base64');
return { encoded };
After the commit, GitHub Actions triggers automatically if you’ve set up the deploy workflow. The draft (with draft: true) lands in the repository and waits for the auto-publish cron to flip it.
Handling failures gracefully
Claude generates invalid frontmatter: The validation step catches this before commit. n8n routes to a notification module. You investigate and either retry or write manually.
GitHub API rate limits: n8n’s GitHub module handles rate limit errors with exponential backoff by default. For a once-daily commit, you’re unlikely to hit rate limits.
Schedule reads empty: If there’s no post scheduled for tomorrow, the n8n workflow should exit cleanly with a “no post scheduled” notification — not with an error. Add a check after the schedule-read step: if no rows returned, send notification and stop.
Content is below 1000 words: Validation catches this. Add a retry step that sends Claude a follow-up prompt: “The previous post was only {N} words. Add more detail to the FAQ section and expand the main content sections to reach 1000+ words.”
For the publishing and deploy side of this pipeline, see the AI-powered publishing workflow guide. The n8n program page covers affiliate context.
Frequently Asked Questions
Can this work with other static site generators? Yes — the architecture is the same for any file-based CMS (Next.js MDX, 11ty, Hugo). Change the frontmatter schema and file path in the generation prompt and the GitHub commit path.
How do I handle images in the automated pipeline?
The automated agent writes the draft with an ogImage path in the frontmatter. The actual image file is generated separately — either manually via a design tool or via an image generation API call added to the n8n workflow.
What’s the cost per post for Claude API calls? Using Claude Haiku (the cost-efficient option): approximately $0.002–0.005 per 1000-word post. At daily cadence, that’s under $2/month in generation costs.
Does this work if I have 2 posts per day? Yes — loop the n8n workflow twice (once per post), passing the slot field (morning/evening) to distinguish them. The commit step runs twice, once per file.
What monitoring do I need? At minimum: a Telegram notification at the end of each successful run, and a separate notification if the run fails at any step. Review the Telegram messages daily to confirm the pipeline ran.
Build automation that ships
If you’re running or planning an automated content pipeline on n8n, the n8n program page covers the affiliate program details, and the programs section covers other automation platforms worth comparing.