| name | publish-seo-batch |
| description | Use when publishing or expanding SEO blog articles on botskills.sh, e.g. "publish today's articles", "write N articles", "expand the blog", "add a blog post", or any request that creates or edits files under packages/web/src/app/blog/posts. |
Publish SEO Batch (botskills.sh)
Pipeline: source topics -> dedup slugs -> write -> gate -> relink -> register ->
build -> commit -> deploy -> prove live -> IndexNow. A batch is not published
until step 9 passes. Adapted from the qaskills.sh pipeline, which is the proven
version of this.
Step 0: State check
git -C /Users/promode/botskills status --short
date +%F
Step 1: Source topics
docs/seo/topics-100.json holds the plan. Anything already written is a file in
packages/web/src/app/blog/posts/. Take the next unwritten topics from the plan,
or WebSearch for net-new demand. Topics mined from social posts go in the
x-signal cluster with a note that their facts are DISPUTED and need
verification.
Step 2: Dedup every slug BEFORE writing
cd /Users/promode/botskills
for s in slug-one slug-two; do
echo "$s: $(grep -rln "$s" packages/web/src/app/blog/posts/ | wc -l) hits"
done
Any hit means pick a different slug.
Step 3: Write
File packages/web/src/app/blog/posts/<slug>.ts, shape per
docs/seo/WRITER-SPEC.md. Depth per docs/seo/LONGFORM-SPEC.md:
3000-4000 body words, 13-15 declarative verb-led H2 sections, 4+ tables,
exactly 4 FAQ questions as the last H2.
Product facts come ONLY from docs/seo/VERIFIED-FACTS-2026-08-25.md. Nothing
from its DO NOT ASSERT list, ever. Grok Bot ships weekly and the popular posts
about it contradict each other, so re-verify against docs.x.ai before asserting
anything new. Every docs.x.ai page has a raw markdown twin at <path>.md.
Cluster addenda for templated title frames live in /tmp during a run and
should be recreated per batch: jobs, integrations, roles, and comparisons each
have anti-template rules, because a templated body gets the whole cluster
filtered out of the index.
Step 4: Gate every file
python3 /tmp/gate.py <slug>...
python3 /tmp/checkh2.py <slug>...
python3 /tmp/slop.py
python3 /tmp/seoaudit.py
No H2 may be reused anywhere in the corpus. Zero em dashes. Zero invented bot
slugs.
Step 5: Kill orphans
python3 /tmp/relink.py
python3 /tmp/relink.py --write
Every article needs 3+ inbound internal links or it sits unindexed. This is the
failure that produced 137 orphan pages on qaskills.
Step 6: Register in BOTH registries
python3 /tmp/register.py
grep -c "'<slug>'" packages/web/src/app/blog/posts/index.ts
Missing the posts map entry means a 404. Missing postList means invisible on
/blog and absent from the sitemap. Never hand-edit sitemap.ts; it derives from
postList.
Step 7: Build
pnpm build
A failure inside a post file is almost always an unescaped backtick or ${.
Step 8: Commit and deploy
Stage explicit paths, never git add -A. Conventional commit, imperative, no
trailers, no footers, no em dash.
npx vercel --prod --yes
vercel --prod uploads the WORKING TREE, so commit first or deploy from a
throwaway worktree at HEAD.
Step 9: Prove it live, then ping IndexNow
for s in <slugs>; do
echo "$s $(curl -s -o /dev/null -w '%{http_code}' https://botskills.sh/blog/$s)"
done
curl -s https://botskills.sh/sitemap.xml | grep -c '<loc>'
node scripts/indexnow.mjs <slug>...
Every slug must be 200 and in the sitemap. IndexNow 200/202 means accepted; 403
SiteVerificationNotCompleted means the key route is unreachable. Google does
not participate in IndexNow and finds posts through the sitemap.
Red flags: stop and redo the step
- Writing a file before the step 2 dedup grep
- Asserting a price, model name, or platform fact not on the SAFE list
- Reporting done without step 9 output in hand
git add -A
- Skipping the build because "it is just content"