
Short answer: A Hono RSS feed is one route. Register app.get('/rss.xml', ...), load the newest published posts from your data source, build RSS 2.0 XML with escaped text and absolute links, and return it with c.body(xml, 200, headers) and the content type application/rss+xml. The same code runs on Cloudflare Workers, Bun, Deno and Node.js. Add a short cache lifetime, validate the output once, and connect the address to an auto-poster so every new post is shared on social media automatically.
Hono is a small, fast web framework built on the standard Request and Response objects of the web platform. Because it avoids runtime-specific APIs, the same application can run on Cloudflare Workers, Bun, Deno, Node.js and other hosts. That makes it popular for blogs, documentation sites and APIs that live at the edge. Hono does not ship a feed generator, but producing RSS takes about forty lines. This guide shows the route, a data source on Cloudflare D1, caching, the pitfalls that break feeds, and how to share each item automatically.
An RSS feed is an ordinary HTTP response: some XML, a status code and a content type. In Hono, a handler receives a context object and returns a response, so there is nothing special to configure. No plugin is needed; you only need to decide where the posts come from and how to turn them into XML.
The data can come from many places: a D1 database, a KV namespace, Markdown files imported at build time, a headless CMS or another API. The approach for a CMS without native feeds is described in auto-posting from a headless CMS without native RSS; the Hono route below works with any of them.
This example reads posts from a Cloudflare D1 table named posts with the columns id, slug, title, summary, image, published_at y status:
import { Hono } from 'hono';
type Bindings = { DB: D1Database };
const app = new Hono<{ Bindings: Bindings }>();
const SITE = 'https://www.example.com';
const esc = (value: string | null) => (value ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
app.get('/rss.xml', async (c) => {
const { results } = await c.env.DB
.prepare(
'SELECT id, slug, title, summary, image, published_at FROM posts '
+ 'WHERE status = ? ORDER BY published_at DESC LIMIT ?'
)
.bind('published', 20)
.all();
const items = results.map((post: any) => {
const link = SITE + '/blog/' + post.slug;
const image = post.image ? new URL(post.image, SITE).href : '';
return '<item>'
+ '<title>' + esc(post.title) + '</title>'
+ '<link>' + esc(link) + '</link>'
+ '<guid isPermaLink="false">post-' + post.id + '</guid>'
+ '<pubDate>' + new Date(post.published_at).toUTCString() + '</pubDate>'
+ '<description>' + esc(post.summary) + '</description>'
+ (image ? '<enclosure url="' + esc(image) + '" type="image/jpeg" length="0"/>' : '')
+ '</item>';
});
const xml = '<?xml version="1.0" encoding="UTF-8"?>'
+ '<rss version="2.0"><channel>'
+ '<title>Example Blog</title>'
+ '<link>' + SITE + '/blog</link>'
+ '<description>News and articles from Example</description>'
+ items.join('')
+ '</channel></rss>';
return c.body(xml, 200, {
'Content-Type': 'application/rss+xml; charset=utf-8',
'Cache-Control': 'public, max-age=300',
});
});
export default app;On Cloudflare Workers, export default app is all that is needed, with the D1 binding named DB in the project configuration. The feed is then served at /rss.xml on your Worker’s domain or custom route.
The helper esc turns ampersands and angle brackets into entities. Without it, a title such as “Tips & tricks” produces invalid XML and many readers and auto-posters stop at the first error. If your descriptions contain HTML, escape them or wrap them in a CDATA section; the rules are in CDATA and HTML in RSS descriptions.
The query filters on status and sorts by published_at, newest first, with a limit of 20. A feed does not need the whole archive, and a short feed is quicker to fetch. Sort by the publication date, not by the update date, otherwise an edited old post jumps to the top.
The GUID uses the database ID, which never changes. If the GUID were the slug, renaming a post would make it look new and an auto-poster could share it twice; see why your RSS feed auto-posts the same article twice. The date is written with toUTCString(), which produces the RFC 822 format that RSS expects, as covered in RSS date formats and pubDate errors.
The route does not depend on the host, only the data access and the entry point change:
fetch method, so export default app works as it is.Deno.serve(app.fetch).@hono/node-server, whose serve function takes the application’s fetch handler.Outside Cloudflare there is no D1 binding, so replace the query with your own data access, for example a SQL client, a Markdown index or a CMS request. Everything below the query stays the same.
Items in a feed are read outside your website, so every address must be complete. A relative link such as /blog/my-post or an image path such as /img/cover.jpg works in a browser on your site but fails in a reader or on a social network. The call new URL(path, SITE) in the example turns a relative image path into an absolute one and leaves addresses that are already absolute unchanged. Problems of this kind are described in relative URLs in RSS feeds.
Choose images of about 1200 pixels wide, and set the correct type for each file instead of hard-coding JPEG when your covers can also be PNG or WebP. Preview sizes for each network are listed in social media image sizes.
Feed readers and auto-posters fetch the feed often, and the data changes rarely. Two layers of caching keep the route cheap:
hono/cache stores responses in the Cache API on Cloudflare Workers, so repeated requests do not touch the database at all.Keep the lifetime short. A feed that is cached for a day delays every share by a day, which defeats the purpose of automation. Five minutes is a sensible balance for most blogs.
Many Hono sites keep posts as Markdown files instead of a database. In that case, build an index of the posts during the build step, with the title, slug, date and summary of each file, and import that index into the route. The handler then maps over an array exactly as in the example. This also keeps the feed independent of any runtime storage, and it makes the date field easy to control, because it comes from the front matter of each file. Remember to exclude drafts from the index.
Hono routes accept parameters, so one handler can serve a feed for every category or tag. Register app.get('/category/:name/rss.xml', ...), read the value with c.req.param('name') and add a category = ? condition to the query. Reuse the XML builder by moving it into a function that takes a list of posts and a title. Separate feeds let you send news to one channel, tutorials to another and announcements to a team chat, instead of publishing everything everywhere. Do not interpolate the parameter into the SQL text; always pass it with bind, as in the example. The idea of routing by category is explained in category filtering for auto-posting.
Add a link tag to the head of your pages so that browsers, readers and auto-posters can find the feed without being told: <link rel="alternate" type="application/rss+xml" title="Example Blog" href="https://www.example.com/rss.xml">. Use the same title as the channel title in the feed. If you publish several feeds, add one link tag for each, with a different title. Discovery is a convenience for readers and browser extensions; an auto-poster still needs the feed address pasted once, so keep the address stable and never move it without a redirect.
toUTCString() so every reader sees the same moment.curl -i and check that the status is 200 and the content type is application/rss+xml.The framework is documented at hono.dev. For more ways to inspect a feed from the command line, see debugging an RSS feed with curl.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your rss.xml 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 post, with its image and link, to the channels you choose among 66 supported networks, messengers, team chats and blogs, including LinkedIn, X, Bluesky, Mastodon, Telegram and DEV. Keyword filters, UTM parameters, hashtags from categories and posting windows let you tune each channel, and the REST API 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 Hono RSS feed is a single GET route that returns XML. Load the newest published posts, escape every text value, use a stable ID for the GUID, make links and images absolute and send the content type application/rss+xml with a short cache lifetime. The same route runs on Cloudflare Workers, Bun, Deno and Node.js. Validate it once, and every post you publish can reach your social channels automatically.
No. Hono is a general web framework. You add the feed as a normal route that builds RSS XML from your posts and returns it with the correct content type.
Yes. Export the Hono application as the default export, bind your database or storage in the project configuration and the route works at the Worker address or a custom route.
Any address works. The names /rss.xml and /feed.xml are common, and you can also add a link tag with the type application/rss+xml to your page head so tools can discover it.
Check that the query returns rows, that the status filter matches the stored value and that the data binding exists in the deployed environment. A preview deployment may use an empty database.
Yes. Paste the public feed address as a source in PostRSS, connect your social accounts and each new item is posted automatically. Make sure the address opens without a login first.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Creadas por Internet Solutions, el equipo detrás de PostRSS. Cada una te ahorra tiempo a su manera.