
Short answer: To add an RSS feed to a Qwik City site, create a route folder named feed.xml with an index.ts file that exports an onGet request handler, load your published posts on the server, build the XML with CDATA-wrapped titles and summaries, and send a Response with the application/rss+xml content type. Use the publication date of each post, take the last build date from the newest post, hash the finished body into an ETag so repeat requests end in a 304, link the feed from every page, test it with curl and a validator, and give the public address to an auto-poster so new posts are shared automatically.
Qwik is built around resumability: the server renders the page, the browser downloads almost no JavaScript up front and code is loaded only when a visitor interacts. Qwik City is the routing and server layer that sits on top of it, with file-based routes, loaders, actions and endpoints. Teams pick it for fast blogs, documentation sites, shops and marketing pages. Most of those sites eventually publish things that people want to follow, such as articles, release notes or product listings, and a feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. Qwik City does not ship a feed generator, and it does not need one: an endpoint is an ordinary request handler that returns whatever you want, including XML. This guide builds a complete Qwik City RSS feed with correct dates, stable identifiers and conditional requests, explains each decision and ends with the checks that keep the feed dependable once other systems rely on it.
A feed is worth adding whenever new items appear on a schedule you do not control: a blog, a changelog, a directory of listings or a library of guides. Readers who follow it in a feed reader get every item in order. Search engines and aggregators discover new pages faster. And an auto-poster such as PostRSS can share each new item on social networks the moment it is published, which turns a quiet publishing routine into a steady stream of visitors.
If your content already lives in a CMS that provides a feed, use that feed instead. Build the route yourself when the content comes from your own database, from markdown files or from an API without a ready-made feed. If you are comparing frameworks, the same ideas are covered in the SvelteKit RSS feed guide, the Astro guide un the Remix resource route guide.
In Qwik City, a file named index.ts inside a route folder is an endpoint when it exports request handlers instead of a component. A handler named onGet runs for GET requests, onPost for POST requests, and onRequest for all methods. The handler receives a request event with the incoming request, helpers for headers and status codes, and a send function. Calling send with a standard Response object ends the request and returns that response untouched, which is exactly what a feed needs: you control the body, the status and every header.
The folder name sets the address. A folder called feed.xml inside the routes directory is served at /feed.xml, and the same pattern works for sitemap.xml or robots.txt. Nothing else in the project has to change, and the page components, layouts and loaders are not involved. This also means the feed is not wrapped in your site layout, so no HTML ever leaks into the document.
The endpoint needs a function that returns the newest published posts. The example below assumes a small module that reads from a database, but the same shape works with markdown files, a headless CMS or a REST API. The important part is the condition on the publication time: without it, a post scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website.
// src/lib/posts.ts
export interface Post {
slug: string;
title: string;
summary: string;
publishedAt: Date;
}
export async function getPublishedPosts(limit = 20): Promise<Post[]> {
const now = new Date();
const rows = await db.posts.findMany({
where: { publishedAt: { lte: now } },
orderBy: { publishedAt: 'desc' },
take: limit,
});
return rows.map((row) => ({
slug: row.slug,
title: row.title,
summary: row.summary,
publishedAt: new Date(row.publishedAt),
}));
}Create the file src/routes/feed.xml/index.ts. The code below builds the document, computes a validator and answers conditional requests. Titles and summaries are wrapped in CDATA sections, so ampersands, quotation marks and angle brackets in your content cannot break the XML. The only sequence that is illegal inside CDATA is its own terminator, and the helper splits it safely.
// src/routes/feed.xml/index.ts
import type { RequestHandler } from '@builder.io/qwik-city';
import { getPublishedPosts, type Post } from '~/lib/posts';
const SITE = 'https://example.com';
const cdata = (text: string) =>
'<![CDATA[' + text.split(']]>').join(']]]]><![CDATA[>') + ']]>';
function buildFeed(posts: Post[]): string {
const newest = posts.length ? posts[0].publishedAt : new Date(0);
const items = posts
.map((post) => {
const url = SITE + '/blog/' + encodeURIComponent(post.slug) + '/';
return [
'<item>',
'<title>' + cdata(post.title) + '</title>',
'<link>' + url + '</link>',
'<guid isPermaLink="true">' + url + '</guid>',
'<pubDate>' + post.publishedAt.toUTCString() + '</pubDate>',
'<description>' + cdata(post.summary) + '</description>',
'</item>',
].join('');
})
.join('');
return (
'<?xml version="1.0" encoding="UTF-8"?>' +
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel>' +
'<title>Example Journal</title>' +
'<link>' + SITE + '/</link>' +
'<description>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"/>' +
items +
'</channel></rss>'
);
}
async function etagFor(body: string): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
const hex = Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
return '"' + hex.slice(0, 32) + '"';
}
function etagMatches(header: string | null, etag: string): boolean {
if (!header) return false;
return header.split(',').some((token) => {
const value = token.trim().replace(/^W[/]/, '');
return value === '*' || value === etag;
});
}
export const onGet: RequestHandler = async ({ request, send }) => {
const posts = await getPublishedPosts(20);
const body = buildFeed(posts);
const etag = await etagFor(body);
const headers = { ETag: etag, 'Cache-Control': 'public, max-age=300' };
if (etagMatches(request.headers.get('If-None-Match'), etag)) {
send(new Response(null, { status: 304, headers }));
return;
}
send(
new Response(body, {
status: 200,
headers: { ...headers, 'Content-Type': 'application/rss+xml; charset=utf-8' },
}),
);
};Start the development server and open /feed.xml. The browser should show the XML or offer it for download, and the response should carry the right content type. The example imports the types from the package that Qwik City releases up to Qwik 1 use; if your project uses a newer package name for the router, import the request handler type from there, because the shape of the handler is the same.
A feed library is fine, and the rss package works well in a Qwik City project, but a short builder keeps the endpoint free of dependencies and portable to edge runtimes where some Node libraries are unavailable. The risk of building XML by hand is escaping, and CDATA removes that risk for the two fields that contain free text. Links and GUIDs are built from a slug that has been through encodeURIComponent, so they contain no characters that need escaping. If you ever add query strings to a link, escape the ampersand inside the URL or move the value into CDATA as well.
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 handler hashes the finished body 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, which saves bandwidth for both sides and keeps your server calm. Web Crypto is available in Node and in edge runtimes, which is why the example does not import a Node module.
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 post 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 applies to any value generated at request time. A compressing proxy may turn a strong tag into a weak one, which is why the comparison strips the W/ prefix.
| Feed element | Source | Rule |
|---|---|---|
| title | Post 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 date | UTC, never the current time |
| description | Short summary | One or two sentences of plain text |
| lastBuildDate | Newest post date | Changes only when content changes |
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 you may rename slugs, store a separate permanent identifier and write it into the guid element with isPermaLink set to false.
The Cache-Control header in the example lets shared caches keep the response for five minutes, and the ETag lets everyone revalidate cheaply after that. If you place a content delivery network in front of the site, keep the lifetime short so a new post does not wait long for its turn. A feed that lags behind the website by an hour defeats the purpose of automation. Also check that your host does not serve a cached copy of an old build: a feed that is generated at build time and never rebuilt is a common reason for items that never appear. For the background, read how auto-posting tools use ETags and conditional requests un why auto-posts are delayed and how to fix it.
Qwik City runs on many platforms through adapters: Node servers, Cloudflare Pages, Netlify, Vercel and others. The endpoint above runs at request time on any adapter that supports server-side rendering. If you deploy a fully static build, the endpoint has to be generated at build time instead, and the feed will only change when you rebuild and redeploy. In that case, either trigger a rebuild whenever you publish or use a small build script that writes feed.xml into the output folder. Whichever route you choose, open the deployed address after the first release, because a path that works in development can behave differently on the host.
Add a link element to the head of your root layout so browsers, feed readers and tools can find the feed from any page: a link with rel set to alternate, type set to application/rss+xml, the title of your site and the address of the feed. With that in place, a visitor or an auto-poster that only knows your home page can locate the feed itself. The details are in the guide to feed autodiscovery.
A few minutes of testing prevents days of confusion later. Check the running site from the outside:
curl -i https://example.com/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 endpoint, 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.
Qwik City needs only a route folder, a request handler and a short builder to serve a dependable RSS feed: select published posts dated in the past, wrap free text in CDATA, use a stable GUID, write every date in UTC, derive the last build date from the newest post and send the result as a Response with the right content type. Add a hash-based ETag so repeat requests end in a 304, link the feed from your layout, test it with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every post you publish can reach your social channels automatically.
No. Qwik City has no feed support, but an endpoint in a folder named feed.xml can return any XML you build, and a small function or the rss package produces valid RSS.
Create src/routes/feed.xml/index.ts and export an onGet handler. The folder name becomes the address, so the feed is served at /feed.xml.
Most often the body changes between requests, for example because the last build date is the current time. Take that date from the newest post and the hash stays the same until the content changes.
Yes, for a fully static site. Write feed.xml into the output folder with a build script and rebuild whenever you publish, otherwise the feed will be out of date.
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.
Izstrādājusi Internet Solutions — PostRSS komanda. Katrs produkts ietaupa laiku savā veidā.