
Short answer: To serve an RSS feed from a Cloudflare Worker, write a module Worker whose fetch handler reads your articles from an API, a KV namespace or a D1 database, builds the RSS document as text with every value wrapped in CDATA sections, adds an ETag computed with the Web Crypto API and returns the response with the application/rss+xml content type. Take the last build date from the newest article, answer a matching If-None-Match header with a 304, cache the result at the edge for a short time, test the route with curl and a validator, then give the public address to an auto-poster.
Cloudflare Workers run your JavaScript or TypeScript close to the reader, without a server to maintain. They are a natural place for a feed: the document is small, requests are frequent, and the answer is almost always the same as a minute ago. A Worker can sit in front of a static site that has no feed of its own, turn a JSON endpoint of a headless CMS into valid RSS, or serve a feed for a product catalogue, a changelog or a directory. This guide builds a complete Worker with stable identifiers, correct dates, conditional requests and edge caching, explains each decision, and ends with the checks that keep the feed dependable once other systems rely on it. The same ideas apply to other edge runtimes, but the examples use the Workers API.
A Worker makes sense when your content is already available as data but not as a feed: a headless CMS with a JSON API, a database behind an HTTP endpoint, a spreadsheet exported to KV, a static site generator without a feed plugin. If your content lives in a blog or a CMS that already publishes a feed, use that one instead; see how to find your WordPress feed link. If you deploy to another serverless platform, compare the Netlify Functions guide और the AWS Lambda guide.
The benefit for automation is speed and stability. An auto-poster such as PostRSS checks the feed often, and a Worker answers from the edge in a few milliseconds, with a cached copy and a 304 for unchanged content. The origin behind it sees only a trickle of requests.
Create a Worker with the Wrangler command line tool. The example reads articles from a JSON endpoint of a headless CMS, which is configured as a variable, so it works with any system that returns a list of published items.
npm create cloudflare@latest feed-worker
cd feed-worker
npx wrangler devAdd the endpoint to the configuration file as a plain variable named ARTICLES_URL, and the public address of the site as SITE_URL. The endpoint must return an array of objects with the fields slug, title, summary, imageUrl, imageBytes, imageType and publishedAt, newest first, where publishedAt is an ISO 8601 date string. Items scheduled for the future should be filtered out by the source; the Worker filters once more to be safe.
The code below fetches the list, filters and limits it, builds the XML as text, hashes the result and answers the request. Because values are placed in CDATA sections, characters such as ampersands and angle brackets inside titles and summaries cannot break the document. The only sequence that must be handled is the CDATA terminator, which the helper splits safely.
export interface Env {
ARTICLES_URL: string;
SITE_URL: string;
}
interface Article {
slug: string;
title: string;
summary: string;
imageUrl?: string;
imageBytes?: number;
imageType?: string;
publishedAt: string;
}
function cdata(value: string): string {
return '<![CDATA[' + value.split(']]>').join(']]]]><![CDATA[>') + ']]>';
}
async function sha256Hex(text: string): Promise<string> {
const data = new TextEncoder().encode(text);
const digest = await crypto.subtle.digest('SHA-256', data);
return [...new Uint8Array(digest)]
.slice(0, 16)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
function buildFeed(site: string, items: Article[]): string {
const newest = items.length ? new Date(items[0].publishedAt) : new Date(0);
const parts: string[] = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
'<channel>',
'<title>' + cdata('Example Journal') + '</title>',
'<link>' + site + '/</link>',
'<description>' + cdata('News and guides from Example Journal.') + '</description>',
'<language>en</language>',
'<lastBuildDate>' + newest.toUTCString() + '</lastBuildDate>',
'<atom:link href="' + site + '/feed.xml" rel="self" type="application/rss+xml"/>',
];
for (const item of items) {
const url = site + '/articles/' + item.slug + '/';
parts.push(
'<item>',
'<title>' + cdata(item.title) + '</title>',
'<link>' + url + '</link>',
'<guid isPermaLink="true">' + url + '</guid>',
'<pubDate>' + new Date(item.publishedAt).toUTCString() + '</pubDate>',
'<description>' + cdata(item.summary) + '</description>',
);
if (item.imageUrl) {
parts.push(
'<enclosure url="' + item.imageUrl + '" length="' + (item.imageBytes ?? 0) +
'" type="' + (item.imageType ?? 'image/jpeg') + '"/>',
);
}
parts.push('</item>');
}
parts.push('</channel>', '</rss>');
return parts.join('\n');
}
function etagMatches(header: string | null, etag: string): boolean {
if (!header) return false;
return header
.split(',')
.map((token) => token.trim().replace('W/', ''))
.some((token) => token === '*' || token === etag);
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
if (url.pathname !== '/feed.xml') return new Response('Not found', { status: 404 });
if (request.method !== 'GET' && request.method !== 'HEAD') {
return new Response('Method not allowed', { status: 405, headers: { Allow: 'GET, HEAD' } });
}
const cache = caches.default;
const cached = await cache.match(request);
if (cached) return cached;
const upstream = await fetch(env.ARTICLES_URL, { cf: { cacheTtl: 60 } });
if (!upstream.ok) return new Response('Feed unavailable', { status: 502 });
const all = (await upstream.json()) as Article[];
const now = Date.now();
const items = all.filter((a) => Date.parse(a.publishedAt) <= now).slice(0, 20);
const body = buildFeed(env.SITE_URL, items);
const etag = '"' + (await sha256Hex(body)) + '"';
const headers = new Headers({
'Content-Type': 'application/rss+xml; charset=utf-8',
'Cache-Control': 'public, max-age=300',
ETag: etag,
});
const response = etagMatches(request.headers.get('If-None-Match'), etag)
? new Response(null, { status: 304, headers })
: new Response(body, { status: 200, headers });
if (request.method === 'GET') ctx.waitUntil(cache.put(request, response.clone()));
return response;
},
};Run npx wrangler deploy and attach the Worker to a route on your own domain, for example example.com/feed.xml. The Cache API used above works on custom domains, and it does not store anything when the Worker runs on the workers.dev address, so test the caching behaviour on the real route.
Workers have no built-in XML writer, so a feed is usually assembled as text. The dangerous part is every value that comes from a person: a title with an ampersand, a summary with an angle bracket, a quotation mark in an attribute. A CDATA section tells the parser to treat everything inside as plain text, which removes the need for an escaping routine. The one sequence that cannot appear inside is the terminator, so the helper splits it into two sections. Values that go into attributes, such as the address of an image, are not wrapped; keep them under your control and encode them with encodeURI if they come from outside. When the source of your articles is untrusted, validate the addresses before they reach the feed.
Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. A validator lets them ask a cheap question: has the document changed since the version I hold? The Worker hashes the finished text with the Web Crypto API, quotes the hash as an entity tag and compares it with the If-None-Match header. A match produces a 304 response with no body.
The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the last build date comes from the newest article and not from the clock. If you wrote the current time into that element, the hash would change on every request, no client would ever receive a 304 and the validator would be useless. The same rule applies to any value generated at request time. A compressing layer may turn a strong tag into a weak one with a W/ prefix, which is why the comparison removes it.
| Feed element | Source | Rule |
|---|---|---|
| title | Article title | Plain text inside CDATA |
| link | Public address of the page | Absolute, with https, returning 200 directly |
| guid | Same address, or a stored identifier | Never changes after publication |
| pubDate | Publication timestamp | toUTCString of the stored date, never the current time |
| description | Short summary | One or two sentences of plain text |
| enclosure | Featured image | Address, size in bytes and media type |
The identifier is the most important decision. An auto-poster remembers which items it has already shared by their GUID. If the value changes when someone edits a slug or republishes an article, the same story is shared again. If slugs may change, store a permanent identifier in the source and write it in the guid element with isPermaLink set to false.
Two layers of caching work together here. The Cache API keeps the finished response at the data centre that served it, and the Cache-Control header lets shared caches and clients keep it for five minutes. The short cacheTtl on the upstream request protects your origin when many data centres ask at once. Keep these lifetimes short: a feed that lags behind the website by an hour defeats the purpose of automation. If you publish an urgent article, purge the cached copy from the dashboard or with the API. For the background, read how auto-posting tools use ETags and conditional requests और what 301, 304, 404 and 500 mean for auto-posting.
A feed behind Cloudflare can be hit by the same security features that protect the rest of the site. A managed challenge or a strict bot rule may return a 403 or an HTML challenge page to an auto-poster, which then sees a broken feed. Create a rule that skips challenges for the feed path, or make sure the Worker answers before such a rule applies; the guide to fixing 403 and bot protection errors for auto-posting explains the symptoms and the fix.
Run the Worker locally with npx wrangler dev, then check it from the outside:
curl -i http://localhost:8787/feed.xml and confirm a 200 status, the right content type and an ETag header.There is a longer checklist in the feed validation guide and a short curl routine in the guide to debugging a feed with curl.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed route, connect your accounts and choose how each post is built from the title, the description or the page title. PostRSS checks the feed as often as every minute and shares each new item, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon and Telegram. Keyword filters, UTM parameters and posting windows let you tune each channel, and webhooks connect PostRSS to your own systems. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
A Cloudflare Worker can turn any list of articles into a dependable RSS feed: fetch the published items, filter out future ones, protect every value with CDATA, use a stable GUID, take the last build date from the newest item and send the result with the right content type. Add a hash-based ETag so repeat requests end in a 304, keep edge caching short, make sure bot protection does not block the route and test with curl and a validator. Keep the address unchanged and connect it to an auto-poster, and every item you publish can reach your social channels automatically.
Yes. A Worker can fetch your articles from an API, a KV namespace or a database, build the XML as text and return it with the application/rss+xml content type, usually in a few milliseconds.
No. Because a feed has a small fixed structure, building the text yourself is fine, as long as every value is protected with CDATA sections or an escaping routine. Libraries are optional.
Most often the body changes between requests, for example because the last build date is the current time. Take that date from the newest item and the hash stays the same until the content changes.
They can. Challenges and strict bot rules sometimes return a 403 or a challenge page to automated readers. Add a rule that skips them for the feed path and test the feed from outside your network.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new items are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
PostRSS बनाने वाली टीम Internet Solutions की ओर से। हर प्रोडक्ट आपका समय अपने तरीके से बचाता है।