A Cloudflare Worker that lets anyone (i.e AI-bots) view any post on your Ghost blog as clean Markdown, either by changing the URL extension to .md or by sending Accept: text/markdown to the normal post URL.
https://yourblog.com/my-post/ <- normal HTML post
https://yourblog.com/my-post.md <- same post as Markdown
curl -H 'Accept: text/markdown' https://yourblog.com/my-post/ <- same post as Markdown
https://yourblog.com/llms.txt <- site index for llms.txt-compatible clients
The returned Markdown includes YAML frontmatter with title, date, tags, canonical URL, slug, description, author, language, published and updated timestamps, and feature image metadata.
The worker can also generate a simple llms.txt file that points models and tools at the latest post-level Markdown URLs.
The worker sits between visitors and your Ghost blog via Cloudflare's network:
For regular page visits, it passes the request through to Ghost unchanged, injects a <link rel="alternate" type="text/markdown"> tag, and adds a matching Link response header so browsers and tools can discover the Markdown version.
Cloudflare Workers don't have a native DOM, but the Turndown library's browser build requires document and DOMParser. This project uses a local copy of Turndown's Node.js build (src/turndown.js) which relies on @mixmark-io/domino for DOM parsing instead. The nodejs_compat compatibility flag is required in wrangler.toml to support this.
- A Ghost blog -- self-hosted (e.g. on Docker, etc.)
- A Cloudflare account (free plan works) with your blog's domain added
- Node.js v20.3 or later
- npm (comes with Node.js)
The worker uses Ghost's Content API (read-only) to fetch posts. You need to create an integration to get an API key.
- Log in to your Ghost Admin panel at
https://yourblog.com/ghost/ - Go to Settings (gear icon in the bottom-left)
- Scroll down to Advanced and click Integrations
- Click Add custom integration
- Give it a name (e.g.
Markdown Worker) - Click Add
- Copy the Content API Key -- you'll need it in Step 4
The Content API key is read-only. It can only read published posts, so it's safe to use in a worker.
Your blog's domain must be routed through Cloudflare so the worker can intercept requests.
- Sign up at cloudflare.com
- Click Add a site and enter your domain
- Select the Free plan (or any plan)
- Cloudflare will scan your existing DNS records. Verify they look correct
- Update your domain's nameservers at your registrar to the ones Cloudflare provides
- Wait for nameserver propagation (usually a few minutes, can take up to 24 hours)
Make sure the DNS record pointing to your Ghost server has the orange cloud (Proxied) enabled, not "DNS only". This is required for the worker to run.
Clone this repo and install dependencies:
git clone https://github.com/YOUR_USERNAME/ghost-markdown-worker.git
cd ghost-markdown-worker
npm installEdit wrangler.toml:
name = "ghost-markdown-worker"
main = "src/index.js"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
# Route the worker on your Ghost blog's domain.
# Replace with your actual domain:
routes = [{ pattern = "yourblog.com/*", zone_name = "yourblog.com" }]
[vars]
# The URL where Ghost is actually reachable.
# If Ghost is on the same domain, you can omit this.
# If Ghost is behind Cloudron or a reverse proxy, set the origin URL:
GHOST_URL = "https://yourblog.com"compatibility_flags -- nodejs_compat is required because the HTML-to-Markdown conversion uses domino for DOM parsing, which relies on Node.js APIs.
routes -- tells Cloudflare which domain/path this worker should handle. Use your blog's public domain.
GHOST_URL -- the origin URL where Ghost's API is accessible. This is important if:
- Ghost runs on a subdomain (
https://ghost.example.com) but your blog is atexample.com - Ghost is behind Cloudron or another reverse proxy
- In most cases, this is the same as your public blog URL
First, log in to Cloudflare via Wrangler (opens a browser window):
npx wrangler loginSet your Ghost Content API key as an encrypted secret:
npx wrangler secret put GHOST_API_KEYPaste the Content API key you copied from Ghost in Step 1 when prompted.
Deploy the worker:
npm run deployThat's it! Your worker is live. Test it:
curl https://yourblog.com/any-post-slug.md
curl -H 'Accept: text/markdown' https://yourblog.com/any-post-slug/
curl https://yourblog.com/llms.txtSuccessful Markdown responses are already cached by the Worker at the edge using Cloudflare's Workers Cache API.
- No extra dashboard cache rule is required for the basic setup
- The current Worker response headers cache Markdown for 5 minutes and allow a short stale window while a fresh copy is generated
- Markdown negotiated from the HTML post URL is cached separately from the HTML representation of that same URL
- This cache is per Cloudflare data center, so the first
.mdrequest in a new region may still hit Ghost - Cache API behavior only works on your proxied custom domain, not in the Workers editor or Playground preview
If you want a longer or shorter cache window, change the Cache-Control header in src/index.js.
Create a .dev.vars file in the project root with your secrets:
GHOST_API_KEY=your_content_api_key_here
GHOST_URL=https://yourblog.com
.dev.varsis already in.gitignore-- never commit this file.
Start the dev server:
npm run devWrangler will start a local server (usually at http://localhost:8787). Test with:
curl http://localhost:8787/my-post.md
curl -H 'Accept: text/markdown' http://localhost:8787/my-post/Every Markdown file includes YAML frontmatter:
---
title: "My Blog Post Title"
slug: "my-blog-post-title"
description: "Short post summary"
author: "Jane Doe"
lang: "en"
date: "2024-06-15"
published_at: "2024-06-15T08:30:00.000Z"
updated_at: "2024-06-16T10:12:00.000Z"
feature_image: "https://yourblog.com/content/images/2024/06/cover.jpg"
tags: ["javascript", "cloudflare", "ghost"]
canonical_url: "https://yourblog.com/my-blog-post-title/"
---The worker can generate https://yourblog.com/llms.txt using Ghost site settings plus the latest published posts from the Ghost Content API.
The file includes:
- the site title
- the site description
- a short note that
.mdURLs andAccept: text/markdownare available - a list of recent published posts pointing at their Markdown URLs
The worker handles Ghost's custom HTML cards:
| Ghost Card | Markdown Output |
|---|---|
Image card (kg-image-card) |
 |
Bookmark card (kg-bookmark-card) |
[Title](url) |
| Code blocks with language | Fenced code blocks with language identifier |
| Standard HTML | Converted via Turndown |
On regular HTML pages, the worker injects a discovery tag in <head>:
<link rel="alternate" type="text/markdown" href="https://yourblog.com/my-post.md" />It also adds the corresponding HTTP response header:
Link: <https://yourblog.com/my-post.md>; rel="alternate"; type="text/markdown"The worker preserves nested post paths, so /notes/my-post/ advertises /notes/my-post.md.
This is skipped for the homepage, Ghost admin pages, Ghost asset paths (/assets/, /content/), collection pages such as /tag/... and /author/..., pagination paths like /page/2/, and existing file paths. HTML and Markdown responses that participate in negotiation also include Vary: Accept.
You haven't set the secret yet. Run:
npx wrangler secret put GHOST_API_KEY- Check that the post is published (drafts aren't available via Content API)
- Check that the slug matches exactly -- visit your post in Ghost Admin and check the URL slug in post settings
- Make sure your DNS record in Cloudflare is set to Proxied (orange cloud), not DNS-only
- Verify the
routespattern inwrangler.tomlmatches your domain - Check the worker is deployed:
npx wrangler deployments list
This means the worker is using Turndown's browser build instead of the Node.js build. Make sure:
src/turndown.jsexists (the local copy of the Node.js build)src/index.jsimports from'./turndown.js', not from'turndown'compatibility_flags = ["nodejs_compat"]is set inwrangler.toml
- Verify
GHOST_URLpoints to the correct origin where Ghost is running - Test the Content API directly:
curl "https://yourblog.com/ghost/api/content/posts/?key=YOUR_KEY&limit=1" - Check worker logs:
npx wrangler tail
MIT
