I Built a Pipeline That Writes and Publishes Blog Posts to This Site
This post was written and published by an automated pipeline. The one I'm about to describe.
Here's the setup: I wanted a way to go from a rough note — a few bullet points about what I built or figured out — to a published blog post on gauravhira.dev, without manually writing, formatting, and uploading anything. The pipeline does that end to end using n8n, Claude, and Supabase.
What the Pipeline Does
Five nodes in n8n handle the whole flow:
- Webhook trigger — listens for an incoming POST request
- Claude (via API) — takes the note content and writes a full blog post in my voice
- Supabase insert — saves the generated post to the
blog_poststable - Response node — returns a success confirmation
The portfolio frontend fetches posts from Supabase on load. When a new row lands in that table, the post is live. No manual deployment step, no CMS dashboard to log into.
The Webhook
The entry point is an n8n webhook node sitting at a fixed URL. When I have something worth writing about, I send a POST request to that URL with a JSON body:
{
"topic": "automated blog pipeline",
"notes": "webhook -> claude -> supabase -> portfolio page. n8n. claude configured with voice/tone. want to explain the whole flow."
}
That's the entire input. The notes field is intentionally rough — that's the point. The value of the pipeline is in what happens next.
Claude Configuration
The Claude node is where the actual writing happens. The system prompt is doing most of the work here. It's a detailed prompt that specifies:
- Writing voice: direct, technical, no hedging
- Structure rules: short paragraphs, numbered steps where they help, concrete specifics over vague claims
- What to avoid: hype words, marketing language, rhetorical patterns that sound promotional
- Output format: Markdown, with a title, body, excerpt, and SEO fields as a JSON object
The user message passed to Claude is just the notes content from the webhook body. Claude expands that into a full post while staying inside the constraints defined in the system prompt.
Getting the system prompt right took a few iterations. The first version produced posts that were technically accurate but had a slightly generic consulting-blog tone — sentences like "this solution enables seamless content delivery." That's not how I write. I tightened the prompt with specific negative examples and explicit rules about sentence structure. The output improved noticeably after that.
One thing that's working well: Claude is consistent about including real specifics. When the notes mention a number or a concrete behavior, it keeps that in. When notes are vague, it flags the vagueness rather than inventing detail — which is the correct behavior for technical writing.
Supabase Storage
The blog_posts table has a straightforward schema:
create table blog_posts (
id uuid default gen_random_uuid() primary key,
title text not null,
body text not null,
excerpt text,
seo_title text,
meta_description text,
target_keyword text,
tags text[],
created_at timestamptz default now()
);
The n8n Supabase node maps the fields from Claude's JSON output directly into this table. One thing I had to handle: Claude returns the body as a Markdown string, and I'm storing it as-is. The frontend renders it with a Markdown parser, so there's no conversion step needed at write time.
Row-level security is on. The n8n connection uses a service role key stored as an n8n credential, so it can write to the table. The public-facing portfolio page uses the anon key and can only read.
Frontend Integration
The portfolio page is fetching posts from Supabase directly:
const { data: posts } = await supabase
.from('blog_posts')
.select('*')
.order('created_at', { ascending: false });
That's the entire data layer. Posts appear in reverse chronological order. When the Supabase insert from n8n completes, the next page load returns the new post.
I'm not doing any build step or static generation here — it's a live fetch. For a personal portfolio with low traffic, that's fine. If this were a higher-traffic site, I'd add caching in front of the Supabase query, but that's not a problem worth solving right now.
What Went Wrong
A few things broke during setup that are worth documenting.
Claude returning malformed JSON. The first few runs, Claude occasionally wrapped the JSON output in a markdown code block (```json ... ```), which broke the Supabase insert node because n8n was trying to parse a string with backticks in it. Fixed this by adding an explicit instruction in the system prompt: "Return only raw JSON. No markdown fences, no preamble, no trailing text." That stopped it reliably.
Field name mismatches. Claude was returning seo_title but I'd initially named the column seo_title_tag in Supabase. The insert was silently dropping that field — Supabase didn't throw an error, it just didn't write it. I caught this when I noticed the column was always null. Renamed the column to match.
Webhook authentication. The initial version had the webhook URL completely open. Anyone who found it could trigger Claude API calls and write to my database. I added a static bearer token check as the second node in the workflow — if the Authorization header doesn't match, the workflow stops immediately. Simple, but enough for this use case.
What This Actually Solves
The friction of writing and publishing was the bottleneck. I'd build something, think about writing it up, and then not do it because formatting and publishing felt like a separate task.
Now the gap between "I built something" and "there's a post about it" is the time it takes to jot rough notes and send a curl request. The pipeline handles everything between that and a live URL.
The voice/tone configuration in Claude means the posts don't read like AI output — they read like how I actually write, because the system prompt is built around my actual writing patterns. That was the non-obvious part to get right. Generic "write like a software engineer" prompts produce generic output. Specific behavioral rules with concrete examples produce something that actually sounds like a person.
The Stack
- n8n — workflow automation, self-hosted
- Claude API — writing and content structuring
- Supabase — Postgres-backed storage with REST API
- gauravhira.dev — Next.js frontend fetching from Supabase
Total moving parts: four services, five n8n nodes, one database table. The whole thing took about a day to get working end to end, with most of that time spent on the Claude prompt and debugging the JSON parsing issues.