Skip to content

Releases: Benjamin-Bloch/pages-seo

1.1.0 — Claude Fable 5, faster blog images, security hardening

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 10 Jun 11:35

Added

  • Claude Fable 5 support. The Anthropic provider now defaults to claude-fable-5, Anthropic's newest model. Override with ANTHROPIC_TEXT_MODEL as 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 repoints blog_posts automatically.
  • Proper 404s. A branded 404.html replaces 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 ships og:image and twitter:card=summary_large_image.

Security

  • Constant-time admin-token comparison (bearer / X-Admin-Token).
  • Baseline security headers: X-Content-Type-Options: nosniff + Referrer-Policy on all pages; X-Frame-Options: DENY + Cache-Control: no-store on 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

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 07 Jun 07:37

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 /install now becomes the no-terminal
    path, and it actually works end-to-end. Removed from the
    marketing hero CTA row, the install-page section, the
    /install chooser, and the README. The
    err-deploy-button-auth-10000 entry 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_TOKEN and
    INDEXNOW_KEY auto-generate on first /api/setup call;
    SITE_NAME / SITE_URL auto-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.example is now
    commented out by default so Cloudflare's deploy UI shows zero
    required fields. package.json ships a cloudflare.bindings
    block with descriptions so any forker who DOES uncomment a key
    gets context.

Changed

  • /install step 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 read env.INDEXNOW_KEY
    with no D1 fallback, so installs that never set the secret
    silently never pinged Bing/Yandex/Seznam. Added
    functions/_lib/indexnow_key.js mirroring the admin_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 read settings.site_name, but
    /api/setup was writing site_name_db. Added a derived alias
    in loadSettings() that resolves site_name from
    (env-or-site_name_db). One fix unblocked 5 call sites.
  • getHost() in IndexNow also only checked env.SITE_URL. Now
    resolves through getSiteIdentity() (env-or-D1).
  • Workers Builds deploy command broke on wrangler: command not found.
    deploy.sh called bare wrangler; Workers Builds CI doesn't
    install wrangler globally. Added wrangler as a devDependency,
    rewrote the deploy script to use npx wrangler pages deploy,
    added a cloudflare-deploy.sh wrapper that turns auth-10000
    errors into actionable build-log messages instead of a generic
    stack trace. The old script lives at deploy:cli.

pages-seo v1.0.5 — AI dedup, internal links, quality scoring, RSS feed

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 06 Jun 20:17

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 new status='review' state instead of going live.
    Public site treats 'review' as 404; admin still sees them. The
    operator can force_publish: true to 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-slug migrates broken legacy slugs
    with automatic 301 redirects (via a new blog_post_redirects
    table created on first use).
  • Category-aware topic rotation. functions/_lib/topics.js now
    tags each topic with a category (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 a Feed: line in robots.txt. No
    config required — uses the request's own host.
  • Richer /api/version. External tools and other installs now
    get recent_commits[] (last 20 on main), release_notes (body
    of the latest GH release), and commits_since_tag (count of
    main commits ahead of the latest tag). Backward-compatible: all
    v1 fields preserved.
  • Beefier /api/health. Adds posts.count,
    posts.last_published, posts.hours_since_last,
    posts.cron_likely_alive, and jobs.in_flight_stuck so 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_TOKEN was stale. The cron Worker had a
    different ADMIN_TOKEN from 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 its d1 create output 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
    ship wrangler.template.toml so 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.py and
    run.sh now 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.
  • /install page 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-install with the
    real working install one-liners. The CLI was never published to
    npm.

Changed

  • Migration: blog_posts gains embedding, embedding_model,
    embedding_at columns (all nullable; backfilled via the new
    POST /api/admin/blog/embed-backfill endpoint).
  • Migration: blog_post_redirects table created lazily by the
    rename-slug endpoint. No separate migration needed.
  • /api/version cache headers now include
    stale-while-revalidate=86400 so a GitHub blip never breaks
    external consumers.

pages-seo v1.0.4

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 21 May 20:19

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 config
  • POST /api/admin/google-search-console — save JSON + options
  • DELETE /api/admin/google-search-console — clear all
  • POST /api/admin/google-search-console/test — live test

How to enable

  1. /admin → Settings → Search engines — expand the "How to get a service-account JSON" panel and follow the 6 steps (~3 min).
  2. Paste the JSON, click Save Google credentials, then Test connection.
  3. 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

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 21 May 20:01

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 stacked build() calls each fetching a cache-busted widget.js. The browser surfaced this as constant network activity / "page is refreshing constantly".
  • The fix wraps listener-binding in a _widgetWired once-guard (matching the bindOnce.done pattern 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

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 21 May 19:54

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_tokens raised 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 → SEO upgraded 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 | bash

The one-liner now defaults to PAGES_SEO_REF=v1.0.2.

Full changelog

See CHANGELOG.md.

pages-seo v1.0.1

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 21 May 19:32

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/apply was 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 a branch field 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_apply was 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 — only env.GITHUB_TOKEN is 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 | bash

The one-liner now defaults to PAGES_SEO_REF=v1.0.1.

Full changelog

See CHANGELOG.md.

pages-seo v1.0.0

Choose a tag to compare

@Benjamin-Bloch Benjamin-Bloch released this 21 May 19:15

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.js

Not 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 /install provisions 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); set PAGES_SEO_REF=main for bleeding edge.

AI bootstrap + diagnose-then-fix

  • /ai-setup generates 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 /repair page 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/health liveness endpoint for external monitors.

Fixed in 1.0.0 (since 0.2.0)

  • curl … | bash|python|node installers read from /dev/tty so 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.