Disclaimer: the below workflow was run on a hobby site and was purely for the purpose of experimentation and learning for my own benefit, with careful sanity and safety checks built into the workflow with the niche of the hobby site in mind. As a writer myself, I believe very strongly in the value of original human-written word and doing things properly for clients. This was a sandbox excercise, not a template for cutting corners on things that really matter.
I run a hobby website in the wild swimming niche with a UK focus. It gets big traffic spikes during warm months, enjoying a few thousand visits per day across a number of pages that perform well for wild swimming areas in [location] type searches when the UK has another one of its inevitable perma-heatwaves.
My wife and I are keen wild swimmers, and the genesis of the site was to try and curate a space where people could explore wild swimming locations in the UK (and beyond) as well as learn about elements such as safety, cold water swimming, and wild swimming gear.
I started monetising it a year or so ago by deploying AdSense, and it pulls in a couple of hundred of pounds a month during warm weather periods, so more than pays for itself. I have dabbled in the affiliate game but like most hobby sites, it has one fatal flaw: it needs constant fresh content and I don’t have time to maintain it.
So I spent an afternoon jumping on the AI content bandwagon and started exploring solutions to keep it running and building out fresh content for me to ship.
There was no spec document and no architecture diagram. Just a conversational back-and-forth that, over the course of a single session, turned into an automated content pipeline that can publish pages and even insert affiliate links into my highest-traffic content.
This is the story of how that went, including the parts that went wrong along the way.
Context and Opening Prompts
It started relatively simply. I described the site: a niche hobby site with useful, structured content that was built from the ground up with a proper purpose. It was already performing well in organic search. I explained the actual problem, insofar I had no time to add new entries or write blog posts regularly, and asked what AI solutions might help with this.
The opening Claude ask was deliberately vague:
“Build me an army worthy of Mordor.”
Ok, it might have been slightly more specific and less exciting:
“I don’t have time to add to the content and would like to explore AI solutions to help with this.”
Claude’s first move wasn’t to start coding, it was to actually go and look at my live site to understand the structure before proposing anything. I then fed it site architecture documents from Screaming Frog, performance data from GA4 and Google Search Console, and some top level ranking data.
From there, it asked two clarifying questions:
- What did I want to automate first, new entries, blog posts, or both?
- How technically comfortable was I?
I said “both, keep it simple” and “very comfortable”, and that set the tone for everything that followed.
The Desired Solution
The shape of the solution that emerged was:
- A small Node.js codebase, version-controlled on GitHub
- Automation via GitHub Actions (free, no server to maintain)
- Claude’s API doing the actual content generation, with web search enabled so it could ground facts (addresses, access details, product links) in reality rather than hallucinating them
- New content posted straight into WordPress via its REST API
- A CSV-based “queue” system, a simple spreadsheet-like file listing what needs writing next, which the pipeline works through automatically and refills when it runs low
The entire thing ended up running on infrastructure that’s either free (GitHub Actions) or pay-as-you-go credits via the Claude API.
The Desired Requirements
Prefacing the above, I had some pretty stringent requirements about the solutions I wanted to arrive at. A couple of these, namely the cost control element (I ended up burning through Claude API credits pretty quickly early on) only became fully visible in hindsight, though I suppose this is what “vibe coding” is all about:
- Content must be factually grounded: critical for anything involving real-world directions, access info, or safety-relevant details. Solved with mandatory web search grounding baked into every content-generation prompt.
- New entries must nest correctly into existing site structure: not just get created as flat pages, but as properly parented pages matching my site’s real information architecture. This required mapping every category/region to its actual WordPress page ID, which was the most fiddly part of this process.
- A human safety net for higher-risk content: owing to factually accurate information being a crucial part of why my site does well and why I believe it is a trusted resource, I had to take steps to ensure that any new blog posts or location pages were kept in draft and were not pushed live as part of this workflow so I could review them before go-live.
- Affiliate monetisation, done properly: real, verified product links tagged with my actual affiliate ID, not invented URLs or made-up prices.
- A way to update existing high-traffic content: not just create new content. This became its own separate automation given existing page types were the highest-leverage place to add monetisation potential.
- Cost control: once real API credit started getting consumed at a noticeable rate, I needed the ability to run different parts of the pipeline independently, rather than one big all-or-nothing job.
Building the Codebase: Across a Terminal and GitHub’s Web Editor
The actual build was fast. A working skeleton in the first hour: helper modules for calling the Claude API (with web search) and the WordPress REST API, a couple of generator scripts, some CSV seed data, and a GitHub Actions workflow file to run it on an on-demand basis (it was initially built on a scheduled basis, but I didn’t want this).
Here’s a flavour of the kind of code that came out of it (recreated here, not a literal copy of the real repo):
// scripts/lib/claude.mjs — calling the Claude API with web search enabled
export async function generateWithSearch({ system, prompt, maxTokens = 3000 }) {
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: 'claude-sonnet-4-6',
max_tokens: maxTokens,
system,
messages: [{ role: 'user', content: prompt }],
tools: [{ type: 'web_search_20250305', name: 'web_search' }],
}),
});
// ...parse and return the response
}
# .github/workflows/content-pipeline.yml — manually-triggered automation
on:
workflow_dispatch: {} # run on demand from the GitHub Actions tab
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm install
- run: npm run location
- run: npm run blogpost
The genuinely interesting part wasn’t the code itself, it was where I was editing it. Because I was working across two environments (a local terminal for git commands, and GitHub’s own web-based file editor for quick edits), and because I’m not a developer, this split caused most of the pain in the session. More on that below.
Troubleshooting and Tweaks: The Actual Bulk of the Session
If I’m honest, this is where most of the time in the session went. Not designing the system, but fighting my own tooling and Claude’s conflicting output. There were multiple instances of workflows failing to run in Github, leading to burned API credits and consistent code fiddling.
Git basics, from zero. I’d used Git a little before, but concepts like resolving a merge conflict, understanding git pull --rebase, or knowing what to do when a stray Vim editor window pops up demanding a commit message? None of that was familiar. Screenshots of a black terminal window with git push rejected messages became a recurring feature of the conversation:
! [rejected] main -> main (fetch first)
error: failed to push some refs to '...'
hint: Updates were rejected because the remote contains work that
hint: you do not have locally...
The fix, most of the time, was the same three commands:
git pull
git push
…except when it wasn’t, and a genuine merge conflict needed resolving by explicitly telling Git which version to keep:
git checkout --ours <filename>
git add <filename>
git commit -m "Resolve merge conflict"
git push
The GitHub web editor silently truncating pasted files. More than once, a longer file pasted into GitHub’s in-browser editor would get cut off mid-file, producing errors like:
SyntaxError: Unexpected end of input
The fix that actually stuck was abandoning the web editor for anything beyond a few lines, and instead editing files locally in Notepad, then pushing via the terminal. Slower, but far more reliable.
Hidden file extensions creating phantom files. Twice, a new file was created via “New Text Document” in Windows and silently kept its .txt extension underneath a renamed-looking filename, so a script I thought existed (suggest.mjs) was actually sitting there as suggest.mjs.txt and doing nothing. Turning on “show file extensions” in File Explorer settings would have prevented this from the start.
Drift between what I described and what was actually committed. This was the single biggest time sink. Across a long session with lots of incremental edits, it became genuinely unclear which changes had actually landed in the real repo versus which had only been described in chat but never successfully applied. The fix, eventually, was brute-force verification: screenshotting actual folder listings on GitHub and checking file-by-file, rather than assuming anything had worked. Painful, but it’s the only thing that actually restored confidence.
Notes on Pitfalls
A few things I’d flag hard to anyone doing this themselves:
Burning API credits faster than expected. A single top-up lasted through initial testing, then evaporated within one real run once I widened the batch size. Cost scales directly with how much content you’re asking for and how many web searches each generation does. Increasing batch size or frequency without thinking about it can burn through credits fast.
Auto-publishing is a real, not theoretical, risk. As mentioned, my content needs to contain real factual claims that affect someone’s safety or plans (directions, access info, safety conditions), it’s a genuinely different risk profile, and manual review is still key.
AI-suggested product links need spot-checking, not blind trust. Even with web search grounding and explicit instructions never to invent a product URL, it’s still worth manually clicking through a sample of generated affiliate links after each run, both to confirm they resolve to real products and that tracking parameters are actually present. A quick and easy check with a Screaming Frog audit of external links on the pages in question.
Outcome
What actually exists now, running on an on-demand basis:
- New location content pages, generated with real web-search-verified details, correctly nested into the site’s existing structure, publishing drafts in WordPress.
- New blog posts, as above.
- A separate, manually-triggered process for generating product/gear roundup content, deliberately left as drafts for review.
- A third, separate, manually-triggered process that reads a spreadsheet of my actual top-performing pages (pulled from real Search Console data) and inserts a small, contextually relevant “what you’ll need” box with tagged affiliate links directly into each one, processing a batch at a time, skipping anything already done, safe to re-run without duplicating.
Splitting these into separate, independently-triggerable jobs turned out to be the single best structural decision in the whole build. It meant I could control when Claude API credits got used, rather than every scheduled run silently doing (and costing for) everything at once.
Was It Worth It?
I actually asked Claude this at the end:
“So, this wasn’t really worth the effort, was it?”
And it gave me a pretty blunt answer, which I kinda knew in my bones anyway: “no, not really, not on its own.”
The build itself came together fast and can run by itself with almost no ongoing effort. But a large fraction of total time went into fighting Git, file editors, and my own environment, not into the actual content strategy, which is where the real value sits.
If I were starting again, I’d spend the first twenty minutes just getting comfortable with the basic terminal/Git loop before touching any real code. Most of the pain in this whole project had nothing to do with AI, and everything to do with the unglamorous mechanics of getting text from my head onto a server correctly.
The “vibe” part worked. The coding part needed more attention to the boring bits than I expected.
Oh, and I earned a few pounds on an Amazon affiliate commision a couple of days after I launched this. Not quite enough to cover various costs of wasted Claude API credits (though only around USD $30) as part of this process, but a small step in the right direction.