This guide walks through everything needed to deploy ghost-llm to Cloudflare Workers, from account setup through to a live webhook receiving events from Ghost.
- A Cloudflare account (free tier is sufficient)
- A running Ghost instance (self-hosted or Ghost Pro)
- Node.js 18 or later
- npm 9 or later
Verify your Node version:
node --version # should be >= 18npm installThis installs Wrangler (Cloudflare's CLI) and the TypeScript types for Workers.
npx wrangler loginThis opens a browser window. Log in to your Cloudflare account and grant Wrangler access. You only need to do this once per machine.
To confirm the login worked:
npx wrangler whoamiThis prints every account your login has access to along with its ID:
Account Name: Personal Blog Account ID: aaa111bbb222...
Account Name: Work Account ID: ccc333ddd444...
If wrangler whoami shows more than one account, you must pin the correct one in wrangler.toml. Without this, Wrangler may silently deploy to the wrong account.
Open wrangler.toml and set the account_id field:
account_id = "aaa111bbb222..." # ← paste your target account ID hereYou can also find your account ID in the Cloudflare dashboard URL:
dash.cloudflare.com/<account-id>/workers
Note:
account_idis not a credential — it's safe to commit to source control.
To verify the correct account is targeted before deploying:
npx wrangler deploy --dry-runThis compiles and reports what would be deployed without uploading anything.
The Worker stores the generated files in Cloudflare KV. You need two namespaces: one for production, one for local development.
# Production namespace
npx wrangler kv namespace create LLMS_KV
# Preview namespace (used by wrangler dev)
npx wrangler kv namespace create LLMS_KV --previewEach command prints output like this:
✅ Created namespace "ghost-llm-LLMS_KV" with id "abc123def456..."
Copy both IDs and paste them into wrangler.toml:
[[kv_namespaces]]
binding = "LLMS_KV"
id = "abc123def456..." # ← production ID here
preview_id = "xyz789ghi012..." # ← preview ID hereIf you see "A KV namespace with that title already exists" a previous session already created it. Run
npx wrangler kv namespace listto retrieve the existing IDs.
Open wrangler.toml and update the GHOST_URL variable to your Ghost instance's root URL. No trailing slash.
[vars]
GHOST_URL = "https://your-blog.ghost.io"For a self-hosted Ghost instance this might look like https://blog.example.com.
- In Ghost Admin, go to Settings → Integrations
- Click Add custom integration
- Give it a name (e.g.
LLM Files) and click Create - Copy the Content API Key — you'll need it in Step 7
npm run deployWrangler compiles the TypeScript, bundles the Worker, and uploads it to Cloudflare. On success it prints your Worker's URL:
✅ Deployed ghost-llm to https://ghost-llm.<your-subdomain>.workers.dev
Note this URL — you will need it for the Ghost redirects in Step 9.
If prompted to register a workers.dev subdomain select yes. This is a one-time step per Cloudflare account. All future Workers you deploy use the same subdomain automatically.
Why deploy before setting secrets? Secrets are attached to a named Worker that already exists in Cloudflare. Deploying first creates that Worker. If you try to set secrets before deploying, Wrangler will ask if it should create a blank Worker stub — that works too, but deploying first is the cleaner path.
Secrets are encrypted values stored by Cloudflare. They are never written to wrangler.toml or checked into source control.
npx wrangler secret put GHOST_CONTENT_API_KEYWrangler will prompt you to paste the Content API key from Step 5. Press Enter to confirm.
To verify it's registered:
npx wrangler secret listThe Worker runs on a cron schedule (every hour by default) but the KV store is empty until the cron fires. Cloudflare does not provide a "Run now" button in the dashboard, so the easiest way to seed immediately is to temporarily set the cron to every minute, wait for it to fire, then restore the original schedule.
8a — Set the cron to every minute
Edit wrangler.toml:
[triggers]
crons = ["* * * * *"]Then deploy:
npm run deploy8b — Wait up to 60 seconds
Monitor the Logs tab in the Cloudflare dashboard (Workers & Pages → ghost-llm → Logs) until you see a "Scheduled rebuild complete" entry.
8c — Restore the hourly schedule
Edit wrangler.toml back:
[triggers]
crons = ["0 * * * *"]Then redeploy:
npm run deployVerify both files are live:
curl https://ghost-llm.<your-subdomain>.workers.dev/llms.txt
curl https://ghost-llm.<your-subdomain>.workers.dev/llms-full.txtGhost manages its own CDN and does not allow Cloudflare to proxy traffic on Ghost-hosted domains. The way to expose yourblog.com/llms.txt is with Ghost's built-in redirect system, which issues a 301 redirect to the Worker URL.
Create a redirects.yaml file with the following content, replacing the target URLs with your actual Worker URL:
301:
/llms.txt: https://ghost-llm.<your-subdomain>.workers.dev/llms.txt
/llms-full.txt: https://ghost-llm.<your-subdomain>.workers.dev/llms-full.txtUpload it to Ghost:
- In Ghost Admin, go to Settings → Labs
- Scroll to Redirects and click Upload redirects file
- Select your
redirects.yamlfile and click Upload
Ghost will immediately start issuing 301 redirects for those two paths.
Verify:
curl -I https://yourblog.com/llms.txt
# Should show: HTTP/2 301 and Location: https://ghost-llm...workers.dev/llms.txt
curl -L https://yourblog.com/llms.txt
# -L follows the redirect and returns the full file contentNote: Most HTTP clients and LLMs follow 301 redirects automatically, so
yourblog.com/llms.txtwill work transparently as the canonical URL.
To run the Worker locally against a live Ghost instance:
npm run devWrangler starts a local server at http://localhost:8787. The preview KV namespace is used (separate from production data).
Seed the local KV store by triggering the scheduled handler:
curl "http://localhost:8787/cdn-cgi/handler/scheduled"Then test the endpoints:
curl http://localhost:8787/llms.txt
curl http://localhost:8787/llms-full.txtNote:
wrangler devuses the preview KV namespace (preview_idinwrangler.toml), not the production namespace. The Worker still reads from your live Ghost instance — Ghost is the data source in both environments. Only where the generated files are written differs. Any files stored in preview KV are lost when you stopwrangler dev. Production KV data is never touched during local development.
After changing src/index.ts, redeploy with:
npm run deployWrangler does a zero-downtime deployment — the previous version continues serving requests until the new version is ready.
To tail live logs from the deployed Worker:
npx wrangler tailEach incoming request and any console.error output appears in real time. Useful for debugging webhook failures.
If you ever rename the Worker or move it to a different account, the workers.dev URL will change. Because the public-facing URLs (blog.big.fan/llms.txt etc.) go through Ghost redirects, the blog domain address stays the same — you only need to update one thing:
- Ghost redirects — upload a new
redirects.yamlin Ghost Admin → Settings → Labs pointing to the new Worker URL
No DNS changes, no code changes, no KV migration required.
Tip: Use
302(temporary) redirects inredirects.yamlduring initial setup and testing. Switch to301(permanent) once the Worker URL is confirmed stable. This avoids browsers and HTTP caches locking in a stale destination.
To update a secret (e.g. if you rotate your Ghost API key):
npx wrangler secret put GHOST_CONTENT_API_KEYPaste the new value when prompted. The change takes effect immediately — no redeployment needed.
llms.txt returns 404
The KV store has not been seeded yet. Follow Step 8 to trigger an immediate cron run.
Cron shows errors in the Cloudflare dashboard
Go to Workers & Pages → ghost-llm → Logs to see the error message. The most common cause is an incorrect GHOST_URL (check for a trailing slash) or a missing/expired GHOST_CONTENT_API_KEY.
Custom domain routes return 404 Ensure the domain is proxied through Cloudflare (orange cloud in DNS settings, not grey). DNS-only records bypass Workers routing entirely.
wrangler login opens a blank page
Try npx wrangler login --no-browser and follow the printed URL manually.
KV reads return stale content
KV has eventual consistency. Changes written during a cron run may take up to 60 seconds to propagate globally. The Cache-Control: max-age=3600 header on responses means a CDN edge may cache the file for up to an hour. For immediate freshness during testing, add ?v=$(date +%s) to the URL to bypass the cache.