Releases: Benjamin-Bloch/pages-seo
Release list
1.1.0 — Claude Fable 5, faster blog images, security hardening
Added
- Claude Fable 5 support. The Anthropic provider now defaults to
claude-fable-5, Anthropic's newest model. Override withANTHROPIC_TEXT_MODELas before. Bundled price snapshot updated ($10/M in, $50/M out). scripts/optimize-hero-images.sh— recompresses existing AI hero PNGs in R2 to WebP (~97% smaller; a 10-post /blog page drops from ~14 MB of images to under 600 KB) and repointsblog_postsautomatically.- Proper 404s. A branded
404.htmlreplaces the SPA fallback that returned the homepage with HTTP 200 for unknown URLs (soft-404 fix). - Marketing site redesign. Editorial press-sheet look: serif display headlines, highlighter-yellow accents, provider ticker, folio numerals, full-bleed nav, dark mode.
Changed
- Hero images store their real format. The image step sniffs the provider's bytes (JPEG/WebP/PNG) and writes the matching extension + content-type instead of assuming PNG.
- All public pages self-host fonts from
/_fonts/— no more Google Fonts round-trip on blog, posts, programmatic pages, or docs. - Blog index LCP: first card loads eagerly with
fetchpriority=high+ preload; page 1 shipsog:imageandtwitter:card=summary_large_image.
Security
- Constant-time admin-token comparison (bearer / X-Admin-Token).
- Baseline security headers:
X-Content-Type-Options: nosniff+Referrer-Policyon all pages;X-Frame-Options: DENY+Cache-Control: no-storeon the admin SPA and sign-in page.
Upgrading
Re-run the installer with your existing slug (idempotent), or use the admin Updates tab. To shrink existing hero images, run bash scripts/optimize-hero-images.sh once after updating.
pages-seo v1.0.6 — Kill 1-click button, polish /install
Removed
- The "Deploy to Cloudflare" 1-click button. Cloudflare's auto-
generated CI token for new Pages projects doesn't include
Pages:Edit, so the very first build fails with
Authentication error [code: 10000]. Until Cloudflare ships a
fix, the button created more confusion than convenience —
every install hit the wall, half the users abandoned. The
browser installer at/installnow becomes the no-terminal
path, and it actually works end-to-end. Removed from the
marketing hero CTA row, the install-page section, the
/installchooser, and the README. The
err-deploy-button-auth-10000entry in/docs#errors
documents the trace for anyone who already had a half-broken
project.
Added
- First-run zero-secret install. The deploy flow no longer
requires any user-supplied secrets.ADMIN_TOKENand
INDEXNOW_KEYauto-generate on first/api/setupcall;
SITE_NAME/SITE_URLauto-resolve from the request host or
the operator's input in the first-run setup card. The full
list of "optional" Pages secrets in.env.exampleis now
commented out by default so Cloudflare's deploy UI shows zero
required fields.package.jsonships acloudflare.bindings
block with descriptions so any forker who DOES uncomment a key
gets context.
Changed
/installstep 3 redesigned for token paste. Three numbered
cards (open token page → paste token → name site), each visually
locked/active/done as the user progresses. Big animated CTA on
the create-token step, real-time validation on the token field
(regex-match + status line), auto-paste detection on
tab-refocus, auto-focus on the site-name input once the token
validates. The Install button stays disabled until all three
steps are done — no more "submit then learn what's wrong".
Fixed
- IndexNow never worked on browser installs. Both the ping
client and the site-verification file route readenv.INDEXNOW_KEY
with no D1 fallback, so installs that never set the secret
silently never pinged Bing/Yandex/Seznam. Added
functions/_lib/indexnow_key.jsmirroring theadmin_token.js
resolution pattern; both call sites now use it. - Site name fell back to "pages-seo" forever on browser installs.
feed.xml,blog/index,page_render,widget.js, and the
admin generate endpoints readsettings.site_name, but
/api/setupwas writingsite_name_db. Added a derived alias
inloadSettings()that resolvessite_namefrom
(env-or-site_name_db). One fix unblocked 5 call sites. getHost()in IndexNow also only checkedenv.SITE_URL. Now
resolves throughgetSiteIdentity()(env-or-D1).- Workers Builds deploy command broke on
wrangler: command not found.
deploy.shcalled barewrangler; Workers Builds CI doesn't
install wrangler globally. Added wrangler as adevDependency,
rewrote thedeployscript to usenpx wrangler pages deploy,
added acloudflare-deploy.shwrapper that turns auth-10000
errors into actionable build-log messages instead of a generic
stack trace. The old script lives atdeploy:cli.
pages-seo v1.0.5 — AI dedup, internal links, quality scoring, RSS feed
Added
- AI duplicate detection (
functions/_lib/dedup.js). Every
candidate topic is embedded via Workers AI's BGE base model and
cosine-compared against the 50 most recent published posts. The
cron repicks on near-duplicates (similarity ≥ 0.80) up to 5 times,
then falls back to the least-similar option. Calibrated against
real posts: rewrites of an existing topic score ~0.83 (blocked);
family-related but distinct topics score ~0.77 (allowed). - Pre-publish quality scoring (
functions/_lib/quality.js).
Deterministic 0–100 check on word count, headings, lists,
internal links, title/meta length. Posts scoring 'bad' (< 45)
go to a newstatus='review'state instead of going live.
Public site treats 'review' as 404; admin still sees them. The
operator canforce_publish: trueto override. - Internal-link injection (
functions/_lib/internal_links.js).
After generation, scan the body for phrases matching titles and
keywords of other published posts, and inject up to 3 markdown
links — skipping code blocks and existing links. Big SEO + reader-
retention win; every new post becomes a link upgrade for older ones. - Slug cleanup + 301 redirects. The AI occasionally prepended
"Blog" to its own title/slug field, producing URLs like
/blog/blogoptimize-.... Text generation now strips this via a
title-level check (the AI's tell is a capital letter immediately
after "Blog"), then re-slugifies. A new admin endpoint
POST /api/admin/blog/rename-slugmigrates broken legacy slugs
with automatic 301 redirects (via a newblog_post_redirects
table created on first use). - Category-aware topic rotation.
functions/_lib/topics.jsnow
tags each topic with acategory(on-page, technical, content,
links, off-page, analytics, platform, ai). The cron picker
prefers categories not used in the last 7 days, preventing the
"three Cloudflare Pages posts in five days" clustering pattern. - Public RSS feed at
/feed.xml. RSS 2.0 with the 30 most
recent posts, advertised via<link rel="alternate">in every
blog page's<head>and aFeed:line inrobots.txt. No
config required — uses the request's own host. - Richer
/api/version. External tools and other installs now
getrecent_commits[](last 20 on main),release_notes(body
of the latest GH release), andcommits_since_tag(count of
main commits ahead of the latest tag). Backward-compatible: all
v1 fields preserved. - Beefier
/api/health. Addsposts.count,
posts.last_published,posts.hours_since_last,
posts.cron_likely_alive, andjobs.in_flight_stuckso an
uptime monitor can detect a silently-dead cron without you having
to look at the admin UI. - Cron Worker
/diag(pages-seo-cron). Unauthenticated
config probe that reports whether each secret is configured,
whether the BLOG_URL/PROG_URL endpoints answer, the current UK
offset, and the next scheduled run times. Doesn't leak any
secret values.
Fixed
- Cron's
ADMIN_TOKENwas stale. The cron Worker had a
differentADMIN_TOKENfrom the Pages project, so every cron
hour returned{"error":"unauthorized"}. Rotated to a fresh
64-hex token on both sides — diagnosed via the new/diag. - Installer (
run.py/run.js/run.sh) D1 parsing. Wrangler
changed itsd1 createoutput from a TOML snippet to JSON;
installers now accept both, with a bare-UUID fallback for any
future format change. - Installer source archive missing
wrangler.toml. Releases
shipwrangler.template.tomlso the demo's real D1/R2 IDs
aren't carried into installs. All three installers now copy
the template into place when the working file is absent. - Installer password input was fully hidden. Both
run.pyand
run.shnow read from/dev/tty(so curl-piped invocations
don't swallow script bytes as input) and show the last typed
character in plain before masking on the next keystroke —
clearer than blind input without exposing the password. - Markdown rendering — full GFM support. Tables, fenced code
blocks, blockquotes, horizontal rules, nested lists, underscore
bold/italic, strikethrough, images, and bare-URL autolinks all
now render. Replaced the NUL-byte sentinel in the inline-code
protection (which made the source a binary file) with a split-
based approach. Bare-URL autolinks now correctly strip trailing
sentence punctuation. - Cover SVGs serve the cream/sage background for posts without
a stored hero image. Falls back to the live/cover/<slug>.svg
template via a?v=<template_updated_at>cache-buster, so
template edits invalidate cached covers immediately. /installpage restyled to match the marketing palette
(was inheriting admin dashboard's dark/cyan theme). Black primary
button + cream background. The "Stuck? Use AI" pill no longer
overlaps the step list. Re-instated the Browser-vs-Terminal
chooser as the first step.- README/CLI docs replaced
npx pages-seo-installwith the
real working install one-liners. The CLI was never published to
npm.
Changed
- Migration:
blog_postsgainsembedding,embedding_model,
embedding_atcolumns (all nullable; backfilled via the new
POST /api/admin/blog/embed-backfillendpoint). - Migration:
blog_post_redirectstable created lazily by the
rename-slug endpoint. No separate migration needed. /api/versioncache headers now include
stale-while-revalidate=86400so a GitHub blip never breaks
external consumers.
pages-seo v1.0.4
pages-seo v1.0.4
Auto-indexing on every publish — Bing/Yandex/Seznam (IndexNow) AND Google (Search Console). Until now only IndexNow fired; Google relied on natural crawl. This release closes that gap.
What's new
Google Search Console auto-indexing
- Sitemap re-submit on every publish — ToS-clean, supported for any content type.
- Indexing API per-URL ping behind an opt-in toggle (faster crawl pickup but technically against Google's ToS for non-job-posting content; off by default).
- Configure via /admin → Settings → Search engines. Paste a Google Cloud service-account JSON; the rest is auto-detected.
- Credentials live in the AES-GCM vault (keyed off your
ADMIN_TOKEN) — never written to the D1 settings table directly. - Test connection button runs a live sitemap submission so you can verify the service account has Owner permission before the next cron run.
New endpoints
GET /api/admin/google-search-console— describe current configPOST /api/admin/google-search-console— save JSON + optionsDELETE /api/admin/google-search-console— clear allPOST /api/admin/google-search-console/test— live test
How to enable
- /admin → Settings → Search engines — expand the "How to get a service-account JSON" panel and follow the 6 steps (~3 min).
- Paste the JSON, click Save Google credentials, then Test connection.
- Done. Every blog publish + programmatic page now auto-pings Google.
Upgrade
In-app: /admin → System → Updates → Apply update.
CLI / fresh installs:
```bash
curl -fsSL https://seo.benjaminb.xyz/install/run.sh | bash
```
pages-seo v1.0.3
pages-seo v1.0.3
Hotfix for v1.0.2.
Fixed
- Infinite-refresh bug on
/admin → Distribution → SEO. The new widget-snippet UI in v1.0.2 bound click + input listeners every time the SEO tab was activated; after a few tab switches each keystroke triggered N stackedbuild()calls each fetching a cache-bustedwidget.js. The browser surfaced this as constant network activity / "page is refreshing constantly". - The fix wraps listener-binding in a
_widgetWiredonce-guard (matching thebindOnce.donepattern used by Status / Links / Calendar / Updates) and drops the cache-buster on the preview script.
Upgrade
In-app: /admin → System → Updates → Apply update.
CLI / fresh installs:
```bash
curl -fsSL https://seo.benjaminb.xyz/install/run.sh | bash
```
pages-seo v1.0.2
pages-seo v1.0.2
Content quality + admin UX. Headline ask was "can we have it generate better, longer blogs?" — answered.
Highlights
Longer, better blog posts
- Workers AI default upgraded to
@cf/qwen/qwen3-30b-a3b-fp8. Qwen3 is an MoE model: ~3B activated params per token, similar latency to a 7B dense model, but writes noticeably better long-form prose than Llama 3.3. - Default article length 2500–4000 words (was 900–1300). Long-form ranks better for long-tail queries. Editable in
/admin → Settings. max_tokensraised to 8192 across all providers — was clipping articles mid-section.- Prompt enforces length: H2 count + FAQ depth scales with target length; the model is told to count its own words before returning JSON.
Admin theme + widget UX
- Light/dark mode toggle in the topbar. Choice persisted; applied before first paint (no FOWT).
- Widget copy-paste card at
/admin → Distribution → SEOupgraded from a static<pre>into:- JavaScript / iframe / link flavour tabs
- Inline Copy button with ⌘C fallback
- Customise panel: container id, heading, post count, light/dark/auto theme
- Live preview that re-renders on every change
Upgrade
In-app: /admin → System → Updates → Apply update.
CLI / fresh installs:
curl -fsSL https://seo.benjaminb.xyz/install/run.sh | bashThe one-liner now defaults to PAGES_SEO_REF=v1.0.2.
Full changelog
See CHANGELOG.md.
pages-seo v1.0.1
pages-seo v1.0.1
Patch release on top of v1.0.0. Fixes the in-app Update button + a handful of edge-case rough patches.
Fixed
/api/admin/update/applywas 400-ing with "A 'manifest' field was expected in the request body". The /pages/projects/{slug}/deployments endpoint actually expects a multipart/form-data POST with abranchfield for Git-linked projects (the manifest path is for Direct Upload only). The apply flow now sends the right shape.install_method='maintainer'installs couldn't use Update.can_applywas hard-gated on'browser'only. Both methods produce Git-linked Pages projects, so both now share the redeploy hook.- Admin Updates tab now degrades gracefully when GitHub 502s. Cloudflare edge IPs share the 60 req/hr unauth pool with GitHub; transient errors show up as a yellow "try again in 30s" warning instead of a red broken-system error.
- Removed the dead OAuth-client-credentials Basic-auth path from
/api/version,/api/changes,/api/admin/update. GitHub deprecated that pattern in 2021 — onlyenv.GITHUB_TOKENis honoured now. Installs without one fall back to Cloudflare's shared unauth pool.
Upgrade
In-app: /admin → System → Updates → Apply update (now works!).
CLI / fresh installs:
curl -fsSL https://seo.benjaminb.xyz/install/run.sh | bashThe one-liner now defaults to PAGES_SEO_REF=v1.0.1.
Full changelog
See CHANGELOG.md.
pages-seo v1.0.0
pages-seo v1.0.0
First stable release. Self-hosted programmatic SEO toolkit running on Cloudflare Pages — daily AI blog, programmatic landing pages, sitemap, IndexNow, embedded widget, admin dashboard. Free tier of Cloudflare covers most usage.
Install
Browser (recommended, ~3 min):
https://seo.benjaminb.xyz/install
Terminal (one-liner, ~2 min):
curl -fsSL https://seo.benjaminb.xyz/install/run.sh | bash
# or run.py / run.jsNot technical? Hand the install to an LLM — copy the prompt from https://seo.benjaminb.xyz/ai-setup and paste into ChatGPT/Claude/Gemini. The AI walks you through every step.
What's new in 1.0.0
One-click + one-line install
- Browser installer at
/installprovisions D1 + R2 + Pages + schema + admin user in ~3 minutes. - Terminal installer (
curl … | bash) is idempotent — re-running upgrades in place. - Both default to the latest stable tag (
PAGES_SEO_REF=v1.0.0); setPAGES_SEO_REF=mainfor bleeding edge.
AI bootstrap + diagnose-then-fix
/ai-setupgenerates a self-contained LLM prompt with your site's URLs baked in.- Admin → System → "✦ Get an AI to fix it" scans your live install (homepage, /api/health, /api/setup, /api/admin/whoami, /sitemap.xml, /robots.txt, /cover/.svg, /blog) in parallel and emits a prompt that names the specific failures.
Update + Repair in the admin
- System → Status runs 12 health checks across D1/R2/Workers AI/secrets/GitHub source/deploy state, with per-check one-click fixes.
- System → Updates compares your installed SHA against upstream main, shows the commit log, offers a one-click rebuild for browser installs.
- Public
/repairpage works even if /admin won't load — paste a Cloudflare API token, auto-detects your project, diagnose + auto-fix.
Daily AI blog + programmatic pages
- 9 LLM providers (Workers AI default; OpenAI, Anthropic, Gemini, Groq, DeepSeek, Mistral, Together, Cerebras as fallbacks).
- Cover-image renderer that produces hero images live from a saved template + the post title, with R2-backed background images.
- IndexNow pings on every publish; Article + FAQ + Breadcrumb schema baked into every post.
Self-healing bindings
- Self-repair secrets (CF_API_TOKEN/CF_ACCOUNT_ID/CF_PROJECT/CF_D1_ID/CF_R2_NAME) let the site re-attach its own D1/R2 bindings if the Pages project loses them.
/api/healthliveness endpoint for external monitors.
Fixed in 1.0.0 (since 0.2.0)
curl … | bash|python|nodeinstallers read from/dev/ttyso prompts work when piped from curl.- Pages-create silently dropping D1/R2 bindings — installer now PATCHes the project config after creation.
- Admin Updates tab classifies transient GitHub 502/403/429 as warnings ("try again in 30s") instead of red broken-system errors.
- Cover SVG live renderer inlines R2 backgrounds as base64 so CORS oddities never blank out hero images.
- Client-side WebP compression on cover-editor uploads (10–15× smaller).
- Multiple CodeQL findings (workflow permissions, error-stack leakage, double-escape in scrape, command-injection in CLI).
Compatibility
- Cloudflare Pages with Functions (Workers).
- D1 (free tier covers ~10GB storage).
- R2 (free tier covers 10GB stored + 1M Class-A ops/month).
- Workers AI (free tier covers ~10k reqs/day on Llama 3.3 70B + Flux 1 schnell).
- Node 18+ (CLI installer only).
Full changelog
See CHANGELOG.md.