
Short answer: To serve an RSS feed from an Oak application on Deno, register a GET route for /feed.xml on the router, read the newest published rows with the built-in SQLite module, build the XML from a template string with an escaping helper, format dates in UTC with toUTCString and set the application/rss+xml content type. Oak adds no validator by itself, so hash the finished body with the Web Crypto API into an ETag, compare it with the If-None-Match header and answer a match with a 304. Take the last build date from the newest item so the body stays identical between changes, test the route with curl and a validator, then give the public address to an auto-poster.
Oak is a middleware framework for Deno, modelled on Koa. It runs on a runtime that uses TypeScript out of the box, asks for permissions explicitly and needs no package manifest to start. Services built with it, such as APIs, dashboards and content sites, often publish something that people want to follow: articles, release notes, offers, events or listings. A feed is the standard way to let readers, aggregators and auto-posters watch that content without an account or an API key. This guide builds a complete Oak RSS feed with a clean query, stable identifiers, correct dates 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 the service publishes things that people outside it want to follow:
If your content already lives in a blog or a CMS, use the feed it provides instead; see how to find your WordPress feed link. If you build with Deno but prefer a full-stack framework, the approach is the same in Fresh; only the route file and the response object change.
Install a recent version of Deno, create a folder and a single file called feed.ts. The example uses the SQLite module that ships with the runtime from version 2.2, so it needs no extra database package, but the query and the loop stay the same with PostgreSQL or MySQL and their drivers. Oak itself is imported from the JSR registry with a version range, and Deno downloads it on the first run.
deno run --allow-net --allow-read --allow-write=data --allow-env feed.tsThe permission flags are a useful safeguard: the program may listen on the network, read files, write only inside the data folder and read environment variables, and nothing else. Keep the public address of the site in configuration rather than reading it from the incoming request. Behind a reverse proxy the request may show an internal host name or a plain http scheme, and a feed full of internal addresses is useless to every reader. The example reads the address from an environment variable with a safe default.
The module below prepares the query once, builds the document from a template string, computes a validator with the Web Crypto API and serves everything from one route. The condition on the publication time matters: without it, an item scheduled for tomorrow would appear in the feed today, and an auto-poster would share it before it exists on the website. Publication times are stored as UTC unix timestamps.
import { Application, Router } from "jsr:@oak/oak@^17";
import { DatabaseSync } from "node:sqlite";
const SITE_URL = Deno.env.get("SITE_URL") ?? "https://www.example.com";
type Post = {
id: number;
title: string;
slug: string;
summary: string;
publishedAt: number;
};
const db = new DatabaseSync("data/site.db");
const newestPosts = db.prepare(
"SELECT id, title, slug, summary, published_at AS publishedAt FROM posts " +
"WHERE status = 'published' AND published_at <= ? " +
"ORDER BY published_at DESC LIMIT 20",
);
function escapeXml(text: string): string {
return text
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
function rfc822(timestamp: number): string {
return new Date(timestamp * 1000).toUTCString();
}
function buildFeed(posts: Post[]): string {
const built = rfc822(posts.length ? posts[0].publishedAt : 0);
const items = posts
.map((post) => `
<item>
<title>${escapeXml(post.title)}</title>
<link>${SITE_URL}/${post.slug}/</link>
<guid isPermaLink="false">post-${post.id}</guid>
<pubDate>${rfc822(post.publishedAt)}</pubDate>
<description>${escapeXml(post.summary)}</description>
</item>`)
.join("");
return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Example News</title>
<link>${SITE_URL}/</link>
<description>The latest posts from Example News</description>
<language>en</language>
<lastBuildDate>${built}</lastBuildDate>
<atom:link href="${SITE_URL}/feed.xml" rel="self" type="application/rss+xml"/>${items}
</channel>
</rss>
`;
}
async function etagOf(body: string): Promise<string> {
const bytes = new TextEncoder().encode(body);
const digest = await crypto.subtle.digest("SHA-256", bytes);
const hex = Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, "0")).join("");
return `"${hex.slice(0, 16)}"`;
}
const router = new Router();
router.get("/feed.xml", async (ctx) => {
const now = Math.floor(Date.now() / 1000);
const posts = newestPosts.all(now) as unknown as Post[];
const body = buildFeed(posts);
const etag = await etagOf(body);
ctx.response.headers.set("ETag", etag);
ctx.response.headers.set("Cache-Control", "public, max-age=300");
if (ctx.request.headers.get("If-None-Match") === etag) {
ctx.response.status = 304;
return;
}
ctx.response.headers.set("Content-Type", "application/rss+xml; charset=utf-8");
ctx.response.body = body;
});
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
await app.listen({ port: 8080 });Run it with the command shown earlier and open the route in a browser or with curl. You should see well-formed XML with at most twenty items, newest first. The statement is prepared once at start-up and reused for every request, which keeps the route fast. The SQLite module used here is synchronous, which is fine for a small table; with a networked database you would await an asynchronous driver in the same place.
Oak does not add validators on its own, so the route builds one. It hashes the finished body with SHA-256, keeps the first sixteen hexadecimal characters, wraps them in quotation marks as the HTTP specification requires and compares the result with the If-None-Match header of the request. A match means the client already holds exactly these bytes, so the route sets the status to 304 and sends no body. A poll that finds nothing new then costs one query and a few hundred bytes of headers.
The hash is computed from the body, which brings one rule: the body must not change unless the content changes. That is why the example takes the last build date from the newest item and not from the clock. If you wrote the current time there, the hash would change on every request, the ETag would never match and the 304 answer would never be sent. The same rule applies to any generator that stamps the current time into its output, in any language. The background to this technique is in the guide to ETags and conditional GET.
| Feed field | Source in the example | Rule to keep |
|---|---|---|
| title | The title column | Plain text, escaped by the helper |
| link | Site address plus slug | Absolute address that returns 200 directly |
| guid | The row id with a prefix | Never changes, even if the slug changes |
| pubDate | The publication timestamp in UTC | RFC 822 format with GMT, never in the future |
| description | The summary column | One or two sentences that make sense alone |
| lastBuildDate | The newest publication timestamp | Changes only when the 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. Using the database id with a fixed prefix, and marking it as not a permanent link, avoids that. The problem and its fixes are described in why a feed auto-posts the same article twice.
RSS dates follow RFC 822, with English abbreviations for days and months. The toUTCString method of a JavaScript date produces exactly that shape, with GMT as the zone, which every parser accepts. It always uses UTC, so the output never depends on the time zone of the server. Do not use toISOString for an RSS document; that format belongs to Atom feeds, and many RSS readers reject it. Do not use toString either, because it writes the local zone of the machine. If a validator complains about dates, read the guide to RSS date format errors.
A template string is convenient, but it inserts values exactly as they are. A title with an ampersand would make the document invalid, and a title with an angle bracket could inject markup. The helper in the example escapes the four characters that matter in text and in attribute values, and every dynamic value passes through it, apart from numbers and the site address that you control. If you want HTML inside a description, wrap it in a CDATA section as explained in the CDATA guide, and make sure the content cannot contain the closing sequence of a CDATA section.
Auto-posters, readers and search engines request a feed often, and almost always nothing has changed. The Cache-Control header in the example allows shared caches to keep the response for five minutes, and the ETag lets everything else end in a 304. Choose the period with care: a long one delays every new post by the same amount, because caches keep serving the old list until it expires. A few minutes is a good balance for most sites. If the table grows large, add an index on the status and publication columns so that the query stays fast. More on this trade-off is in the guide to feed caching.
Run the program under a process manager or in a container, and put a reverse proxy in front of it that terminates HTTPS. The deno compile command can produce a single executable if you prefer to ship one file. Keep the public address in your own setting rather than trusting forwarded headers, so that links in the feed are always correct. Check that the proxy does not strip the ETag and Cache-Control headers and that it does not add its own long cache that keeps old XML for too long. After every deployment, request the feed once to confirm that it still returns 200 and that the address is exactly the one you gave to the auto-poster. If the feed ever stops updating, this checklist shows what to inspect first.
An auto-poster is more useful when each item carries a picture. Add an enclosure element to the item template with the address, the length in bytes and the type of the image as attributes, and fill it from an image column. Use an absolute https address and a file of reasonable size. Without an image, many networks show a plain text card. How tools read these elements is explained in the guide to enclosure tags.
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.
Oak needs only a router, a prepared query and a few lines of TypeScript to serve a dependable RSS feed: select published items dated in the past, escape every value, use a stable GUID, format dates in UTC with toUTCString, derive the last build date from the newest item and send the result with the right content type. Add a hash-based ETag from the Web Crypto API so repeat requests end in a 304. Test the route with curl and a validator, keep the address unchanged and connect it to an auto-poster. After that, every item you publish can reach your social channels automatically.
No. Oak has no feed support, but a template string with an escaping helper produces a valid RSS document, and the route returns it with the right content type.
Yes, for a conditional response. Oak does not compute validators by itself in this setup, so the route hashes the body, sets the ETag header and returns 304 when the request carries a matching If-None-Match value.
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.
Between ten and twenty of the newest published items is enough. An auto-poster only needs recent items, and a smaller file is faster to download and parse.
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.
Dibina oleh Internet Solutions, pasukan di sebalik PostRSS. Setiap produk menjimatkan masa anda dengan cara tersendiri.