I can put AI generation inside a job you can inspect later, with source ranges, rejected candidates, validation output, and a git diff.
Blog Automation Workflow
A GitHub Actions job that picks certification textbook ranges, drafts SEO/GEO blog posts, checks the Markdown, adds covers, and only publishes when git sees a real diff.
Automated content can drift, repeat an old topic, make unsupported claims, publish bad frontmatter, or run twice for the same week.
Weekly slot state, cert rotation, table of contents (TOC) screening, source coverage checks, novelty judge, Markdown retry, automation.json, and commit-on-diff.
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.
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 cert-prep articles without making every post a manual project. The point was not volume for its own sake. It was SEO content that stayed tied to real exam categories, course pages, and textbook-backed topics.
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 unsupported claims, miss the course CTA, ship broken frontmatter, or run twice for the same week.
So the prompt sits in the middle, not at the beginning. Before drafting, the generator resolves a Monday UTC slot, checks state, rotates the certification category, selects a table of contents (TOC) range, extracts enough prose, and screens for duplicate source ranges. After drafting, it validates the Markdown and retries once with concrete 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.
Three trade-offs worth naming.
The short version: choice, reason, cost.
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 cached textbook table of contents (TOC) files, picks usable line ranges, and rejects weak or duplicate candidates before the model sees anything.
The model should write from bounded source material, not invent the content calendar. 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 and trace the post, not just the prompt
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