Skip to content
Back to skills

Pages Seo

ASecurity

Use this skill whenever the user mentions pages-seo (the self-hosted programmatic-SEO + daily-AI-blog toolkit for Cloudflare Pages), or asks how to install / update / repair a pages-seo site, or asks about the AI prompt setup at seo.benjaminb.xyz/ai-setup. Loads the project's install paths, common failure modes, and per-mode playbooks so the user does not have to re-explain context every conversation.

  • 6 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 25, 2026
ai-agentsgobashsqlnodegitapidatabasebackend

Works with

  • claude code
  • cli
  • api

Security analysis

A92/100
  • mediumUses curl or wget to download content

Pro shows the line behind each finding and how to fix it

Scanned September 25, 2026

npx -y skills add Benjamin-Bloch/pages-seo --skill pages-seo --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pages Seo?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pages Seo
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/benjamin-bloch-pages-seo/badge)](https://www.skillsdirectory.com/skills/benjamin-bloch-pages-seo)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pages-seo
description: Use this skill whenever the user mentions pages-seo (the self-hosted programmatic-SEO + daily-AI-blog toolkit for Cloudflare Pages), or asks how to install / update / repair a pages-seo site, or asks about the AI prompt setup at seo.benjaminb.xyz/ai-setup. Loads the project's install paths, common failure modes, and per-mode playbooks so the user does not have to re-explain context every conversation.
---

# pages-seo

Self-hosted programmatic-SEO + daily-AI-blog toolkit for Cloudflare
Pages. Workers AI by default, 8 cloud providers as fallback. D1 for
storage, R2 for images. Source: <https://github.com/Benjamin-Bloch/pages-seo>.
Live demo + docs: <https://seo.benjaminb.xyz>.

## Quick router — pick the right playbook

| User's situation | Playbook |
|---|---|
| "Install pages-seo" / "Set up a new site" | [Install](#install-playbook) |
| "Update to the latest version" / "Sync upstream" | [Update](#update-playbook) |
| "It's broken" / "cron stopped" / "admin 404" | [Repair](#repair-playbook) |
| "Add a feature" / "Edit the code" | [Code-change conventions](#code-conventions) |

If the user gives you their slug or site URL, use the URLs throughout
instead of placeholders like `<my-slug>`. Always validate slug against
`^[a-z][a-z0-9-]{1,32}$`.

## Install playbook

Use the canonical installer at
`https://seo.benjaminb.xyz/install/run.sh`. It uses `wrangler`
(no GitHub App, no API tokens to paste) and is idempotent.

Steps when running this yourself:
1. `wrangler whoami` — halt if not authenticated.
2. `curl -fsSL https://seo.benjaminb.xyz/install/run.sh -o /tmp/pages-seo-install.sh`.
3. Read the script first so you know what it does.
4. Ask the user (in one message, not four) for: project slug, site
   name, admin email, admin password. Validate slug syntax.
5. Run the installer, piping answers in order. Stream stdout.
6. Verify: `GET https://<slug>.pages.dev/api/health` returns 200 with
   `db: "ok"`.
7. Verify: `GET https://<slug>.pages.dev/admin` returns 200.
8. Surface the magic-link tmpfile path (NEVER print the link itself —
   it contains the admin password in a URL fragment).

Done criteria: `/api/health` is healthy AND `/admin` returns 200 AND
the user knows where the first-run link file lives.

## Update playbook

Updates are idempotent — re-running the installer with the same slug
detects existing D1/R2/Pages project and only re-uploads code. **Do
not drop or recreate the D1 database**.

Steps:
1. `curl -s <site>/api/version` — check current vs latest tag. Stop
   if `up_to_date: true`.
2. `curl -s https://api.github.com/repos/Benjamin-Bloch/pages-seo/releases/latest`
   — summarise what's new in 3 bullets max, **flag breaking changes**.
3. Re-run installer with the same slug.
4. Poll `<site>/api/version` (10s interval, 5min max) until `sha`
   matches the new release.
5. Verify `<site>/api/health`: `db: ok`, `cron_likely_alive: true`,
   `jobs.in_flight_stuck: 0`.

If the in-admin "N commits behind" banner persists after a successful
deploy, that's a known marker-staleness UX quirk. Either click "Mark
as up to date" in /admin → Updates, OR `POST /api/admin/update/dismiss`
with the admin bearer.

## Repair playbook

Diagnose by ladder — cheapest check first, only escalate on failure.
**Never delete D1 or the Pages project.** D1 holds every post the user
has ever generated.

| Rung | Check | If it fails… |
|---|---|---|
| 1 | `curl -sI <site>` | Connection refused → Pages project gone. `wrangler pages project list`. |
| 2 | `curl -s <site>/api/health \| jq` | See [Health field interpretation](#health-field-interpretation) below. |
| 3 | `wrangler tail pages-seo-cron` | Most common: ADMIN_TOKEN drift. See [Token rotation](#token-rotation). |
| 4 | D1 stuck-jobs query | See [Stuck-job recovery](#stuck-job-recovery). |
| 5 | `wrangler pages deployment list` | Latest deploy may have failed. Redeploy. |

### Health field interpretation

`/api/health` JSON fields and what they mean:

- `db: "ok"` → backend reachable.
- `db: "unbound"` → D1 binding lost. Re-bind:
  `wrangler pages project edit --d1 DB=<d1-id>`. Look up `<d1-id>` in
  `wrangler d1 list`.
- `db: "error"` → schema drift or D1 outage. Re-apply
  `schema/init.sql` via `wrangler d1 execute <db-name> --remote --file=schema/init.sql`.
- `posts.cron_likely_alive: false` → no post in 36+ hours. Go to
  rung 3.
- `jobs.in_flight_stuck > 0` → a generation step died silently. Go
  to rung 4.

### Token rotation

Cron `ADMIN_TOKEN` drift is the most common silent failure. Rotate
**both sides in the same step** or the cron stays 401-locked:

```bash
NEW=$(openssl rand -hex 32)
echo "$NEW" | wrangler pages secret put ADMIN_TOKEN --project-name <slug>
echo "$NEW" | wrangler secret put ADMIN_TOKEN --name pages-seo-cron
```

### Stuck-job recovery

```bash
wrangler d1 execute <db-name> --remote --command="
  SELECT id, status, error, updated_at FROM blog_jobs
  WHERE status NOT IN ('published','failed')
  ORDER BY updated_at DESC LIMIT 5"
```

Read `error`. Typical causes:
- `provider_budget_exceeded` → AI free-tier exhausted; add a fallback
  provider key in /admin.
- `provider_timeout` → flaky upstream; mark failed and rerun.
- `*_http_404` / `model_not_found` → the pinned model id was retired by
  the provider. Check `/admin` → Settings → Providers, or clear the
  `<PROVIDER>_TEXT_MODEL` override so the current default applies.

## Code conventions

If the user asks for a code change in their fork:

- **File layout**: Cloudflare Pages Functions in `functions/api/**.js`,
  static assets in `public/`, cron Worker in `cron-worker/`, D1 schema
  in `schema/init.sql` (bundled into `functions/_lib/schema.js` via
  `node scripts/bundle-schema.js`).
- **Model ids**: `functions/_lib/models.js` is the single source of
  truth for every provider's default model, its selectable
  alternatives, the env-override key and the bundled price. Never
  hardcode a model id anywhere else — the admin UI fetches this
  catalogue from `/api/admin/providers`. Bump `CATALOGUE_REV` when a
  default or price changes, and run `npm run check-models`.
- **Schema changes**: edit `schema/init.sql`, then re-run the bundler.
  Schema must stay additive (`CREATE TABLE IF NOT EXISTS`,
  `ALTER TABLE … ADD COLUMN`) — no destructive migrations.
- **Auth**: every admin endpoint must call `adminGate(env, request)`
  before doing anything. Do not weaken this.
- **Error responses**: use `json(status, body)` from `functions/_lib/util.js`.
  Never put a raw Error in the body — it strips `.stack`/`.cause` for
  you, but only via the replacer.
- **Cache headers**: personalised content `cache-control: no-store`;
  public content `public, max-age=…, s-maxage=…, stale-while-revalidate=…`.
- **Never commit `wrangler.toml`** — only `wrangler.template.toml`
  ships in the repo (real D1/R2 ids stay local).

## What NOT to do (under any circumstances)

- ❌ `wrangler d1 delete` — destroys every post.
- ❌ `wrangler pages project delete`.
- ❌ Edit `wrangler.toml` with the user's real ids and commit it.
- ❌ Modify `functions/_lib/auth.js` to widen `adminGate`.
- ❌ Print the magic-link URL to a chat or stdout — it contains the
  admin password in a fragment.
- ❌ Run `git push --force` on the user's fork.

## Reference URLs

- Install: <https://seo.benjaminb.xyz/install>
- Repair UI: <https://seo.benjaminb.xyz/repair>
- AI prompt picker: <https://seo.benjaminb.xyz/ai-setup>
- Docs + error reference: <https://seo.benjaminb.xyz/docs>
- Version API: <https://seo.benjaminb.xyz/api/version>
- Source: <https://github.com/Benjamin-Bloch/pages-seo>

For the tool-specific prompt variants (Claude Code, Codex, Copilot,
chat), see `GET /api/ai-prompt?tool=…&mode=…`. The prompts at
seo.benjaminb.xyz/ai-setup and this skill share the same playbooks.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…