blog-pipeline
Purpose
Move a Markdown draft through quality gates — SEO check, uniqueness scan, link validation — and publish to GitHub Pages and/or dev.to with a single workflow. All gates run locally; only the final publish step touches external APIs.
Runbook
Pre-flight — verify
git,gh,pandoc,curl, andjqare available.GITHUB_TOKENis required for GitHub Pages push.DEVTO_API_KEYis optional; set it only when publishing to dev.to.Lint the draft — run
scripts/lint-draft.sh <post.md>:- Checks frontmatter for
title,description,tags,date - Validates internal links resolve (relative paths)
- Warns on images without
alttext - Enforces line length ≤ 120 chars for diff readability
- Exit 1 on any error; exit 0 with warnings printed to stdout
- Checks frontmatter for
SEO check — run
scripts/seo-check.sh <post.md>:- Title: 50–60 chars recommended
- Meta description: 120–160 chars
- Checks that the focus keyword appears in title, first paragraph, and at least one H2 heading
- Counts reading-time estimate (≈200 wpm)
- Prints a scored report (0–100); score < 60 prints a blocking warning
Uniqueness scan —
scripts/seo-check.shalso extracts 3-gram phrases and checks them against a local bloom-filter of your past posts (~/.blog-pipeline/published.bloom). Flags phrases appearing in > 3 prior posts as potentially over-recycled.Publish — run
scripts/publish.sh <post.md> [--target pages|devto|both]:- GitHub Pages: copies to
_posts/in the configured repo, commits, and pushes - dev.to: POSTs to
https://dev.to/api/articlesusingDEVTO_API_KEY - Default
--targetispagesonly - Dry-run mode:
--dry-runprints the actions without executing them
- GitHub Pages: copies to
Post-publish — appends the article slug to
~/.blog-pipeline/published.bloomfor future uniqueness checks.
Stop conditions
- Abort if
GITHUB_TOKENis unset and target includespages. - Abort if
DEVTO_API_KEYis unset and target includesdevto. - Abort if lint-draft exits 1 — fix frontmatter or broken links first.
- Abort if SEO score < 40 — post needs more work.
- Never force-push the GitHub Pages branch; always
--no-force. - Do not publish if
git statusshows uncommitted changes in the posts directory.
Output format
Lint report (stdout)
✅ Frontmatter OK
✅ Internal links OK (4 checked)
⚠️ Image alt text MISSING on 1 image → line 42
✅ Line length OK
SEO report (stdout)
📝 blog-pipeline SEO check
Title (58 chars) : ✅
Meta description (142) : ✅
Keyword in title : ✅ "HRV"
Keyword in first para : ✅
Keyword in H2 : ✅
Reading time : ~6 min
Uniqueness : ✅ no over-recycled phrases
SEO score : 78 / 100
Publish summary (stdout)
✍️ blog-pipeline publish
Target : GitHub Pages
Post : _posts/2026-04-22-hrv-guide.md
Commit : abc1234 "Add: HRV guide post"
Push : ✅ origin/gh-pages
URL : https://yoursite.com/hrv-guide
Example invocations
scripts/lint-draft.sh my-post.md— validate frontmatter and linksscripts/seo-check.sh my-post.md— score SEO and uniquenessscripts/publish.sh my-post.md --target pages— push to GitHub Pagesscripts/publish.sh my-post.md --target both --dry-run— preview actions- "Check my draft for SEO issues."
- "Publish this post to dev.to."