I can put AI generation inside a job you can audit afterwards, down to which pages it read and which titles it threw away.
Blog Automation Workflow
A GitHub Actions job that picks a topic out of the course textbook, drafts the post, checks it, and publishes only when git sees a real change.
Every run passes the gates or publishes nothing.
Trigger
- 01 Cron route
Bearer CRON_SECRET protects the Vercel endpoint.
- 02 Dispatch
GitHub API starts blog-post-auto.yml with a Monday UTC slot.
Generate
- 01 Rotate cert
State picks TSAC-F, CSCS, NASM CPT, or CISSN.
- 02 Screen topic
Table of contents (TOC) ranges, slug tokens, and source overlap reject repeats.
- 03 Draft
OpenRouter writes from source text, style guide, template, and schema.
Publish
- 01 Validate
Frontmatter, links, sections, definitions, and word count must pass.
- 02 Cover
Pexels image and attribution attach when a match is found.
- 03 Commit
Commits content/blog-posts only when git sees a diff.
Automated content wanders off topic, repeats itself, or quietly runs twice for the same week.
Every run leaves a trace file: which week it filled, which textbook pages it drew from, which titles it rejected, and what the repeat check scored.
Blog index and generated post.
A weekly blog generator for Gifted Academics. It picks a certification topic, pulls bounded source material from the matching textbook, drafts a Markdown post through OpenRouter, validates the result, adds a cover image, writes a trace file, and lets GitHub publish the diff.
Why it exists
The business needed regular certification-prep articles without making every post a manual project. The point was never volume for its own sake. It was search traffic that stayed tied to real exam categories and to topics the textbook could actually back up.
I built around that constraint. The system chooses from the course source material first, then asks the model to write inside a tight template. The publish path is a git diff, not a hidden CMS mutation.
What made it hard
Blog automation fails quietly if the only check is "did the model return text?" A generated article can repeat an old topic, make claims the textbook does not support, leave out the link back to the course, or run twice for the same week.
The prompt sits in the middle of the run. Before drafting, the generator works out which Monday it is filling and picks the next certification in the rotation. Then it chooses a range of textbook pages, pulls enough prose out of them, and checks that range has not been used before. After drafting, it validates the Markdown and retries once with specific feedback before writing files.
What the workflow leaves behind
Each generated post gets an automation.json file: slot, cert,
selected slug, source file, textbook line ranges, rejected candidate titles,
model, novelty judgment, workflow run URL, and cover metadata. That file makes
the article easier to inspect after the fact.
The latest generated post on the live blog came from the June 22, 2026 slot. Its trace recorded a CISSN source range, novelty confidence of 0.88, the OpenRouter model used for drafting, and the Pexels photo attribution that appears in the rendered article.
What the gates are for.
GitHub Actions is the publisher
GitHub Actions checks out main, installs the app, runs the generator, commits content/blog-posts when there is a diff, and pushes back to main.
The blog is file-backed content. Publishing through git leaves a run URL, a reviewable diff, and a history you can roll back.
The action needs write permissions and production secrets. A bad generator change can publish directly unless the gates catch it first.
Pick the topic before asking the model
The generator rotates through NSCA TSAC-F, NSCA CSCS, NASM CPT, and CISSN. It reads the cached table of contents for each textbook, picks usable page ranges, and throws out weak or repeated candidates before the model sees anything.
The model writes from bounded source material; the content calendar is resolved before it runs. Candidate selection is deterministic, so a run is explainable after the fact.
The output is only as good as the table-of-contents cache and the line-range heuristics. Messy OCR headings still need rejection rules.
Validate the post, then record why it exists
Each draft has to pass frontmatter, slug, section, word-count, link, and definition checks. The run also writes automation.json with the source ranges, model, novelty judgment, cover metadata, and rejected candidates.
A generated post has to render cleanly, and I need to be able to audit it later. The trace file explains why that post exists and which source material supported it.
Validation adds one more repair loop and can turn a run into a no-op. I would rather skip a week than publish a near-duplicate or a broken post.
The stack.
- GitHub Actions
- Node 22
- OpenRouter
- GPT-5.4 mini
- Pexels API
- Next.js App Router
- Markdown content
- Custom frontmatter parser
- Textbook table-of-contents cache
- Source-range extraction
- LLM novelty judge
- Markdown validation
- automation.json trace
- Vercel
- Claude Code
Want the walkthrough, or the parts that aren’t written up yet?
jakerosow@gmail.com