
Short answer: In a Sails.js application, an RSS feed is one route that points to an action returning XML. Add the route to the routes file, load the newest published records with Waterline, build the document with escaped text and absolute links, set the RSS content type and let the response answer with a 304 status when nothing has changed. Test the live address with curl and a validator. When the address, the links and the dates stay stable, an auto-poster can read the feed and share every new post on your social networks.
Sails is an MVC framework for Node.js that sits on top of Express. It generates routes, models and controllers, and it is often used for APIs and for server-rendered sites with a Waterline database layer. A feed fits it well: the data already lives in a model, the route is one line of configuration and the action is a short function. The risks are the usual ones for a hand-built feed: text that breaks the XML, links that are not absolute, dates in the wrong format and an identifier that changes when somebody edits a post. This guide builds a complete action, explains each choice and ends with a test routine.
Routes in Sails are listed in a single configuration file. Add one entry for the feed address and point it to an action. The action name follows the folder structure of the controllers.
// config/routes.js
module.exports.routes = {
"GET /": { action: "home/index" },
"GET /feed.xml": { action: "feed/show" },
};If you use blueprint routes, make sure that the feed address does not collide with an automatic route, and check that no policy in the policies file blocks anonymous requests to the action, because an auto-poster has no login. The same idea in other Node.js frameworks is covered in the Express feed guide e the NestJS feed guide.
A feed item needs a title, a permanent address, a publication date and a summary. Add those fields to the model of your posts, and keep the slug separate from the title so that it never changes after the first save.
// api/models/Post.js
module.exports = {
attributes: {
slug: { type: "string", required: true, unique: true },
title: { type: "string", required: true },
summary: { type: "string", required: true },
published: { type: "boolean", defaultsTo: false },
publishedAt: { type: "number", description: "Epoch milliseconds" },
},
};The date is stored as a number of milliseconds, which sorts correctly in every database that Waterline supports and avoids time zone surprises. The feed converts it to the format that the RSS specification expects. If you store dates in another type, convert them in one place only.
The action loads the newest published posts, builds the XML and sends it. Keep it in its own file so that it can be tested without the rest of the application.
// api/controllers/feed/show.js
const SITE = "https://example.com";
const ENT = {
"&": "&" + "amp;",
"<": "&" + "lt;",
">": "&" + "gt;",
'"': "&" + "quot;",
"'": "&" + "apos;",
};
const esc = (s) => String(s || "").replace(/[&<>"']/g, (c) => ENT[c]);
module.exports = async function show(req, res) {
const posts = await Post.find({
where: { published: true, publishedAt: { "<=": Date.now() } },
sort: "publishedAt DESC",
limit: 20,
});
const newest = posts.length ? posts[0].publishedAt : 0;
const items = posts.map((p) => {
const url = `${SITE}/blog/${p.slug}`;
return `<item>
<title>${esc(p.title)}</title>
<link>${esc(url)}</link>
<guid isPermaLink="true">${esc(url)}</guid>
<pubDate>${new Date(p.publishedAt).toUTCString()}</pubDate>
<description>${esc(p.summary)}</description>
</item>`;
}).join("\n");
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
<title>Example Journal</title>
<link>${SITE}</link>
<description>News and guides from Example Journal.</description>
<lastBuildDate>${new Date(newest).toUTCString()}</lastBuildDate>
${items}
</channel></rss>`;
res.set("Content-Type", "application/rss+xml; charset=utf-8");
res.set("Cache-Control", "public, max-age=300");
return res.send(xml);
};Several details are deliberate. The query keeps drafts and future posts out. The channel date comes from the newest post and not from the clock, so that the file does not change when nothing was published. The identifier of each item is its permanent address. And the address is built from a fixed constant, so every link is absolute. Action files in Sails may also be written in the newer style with inputs and exits; the body stays the same, and you can reach the response object from the action context to set the headers.
The response object of Sails is the one from Express, and Express creates a weak validator for a string body and compares it with the header that the client sends. When the two match, the framework answers with a 304 status and no body. You can see this in practice by requesting the feed twice with curl and sending back the validator from the first answer. If you want the validator to depend on something other than the text, for example the date of the newest post, you can set the header yourself before sending. A 304 answer saves bandwidth for you and for every tool that polls the feed. The mechanism is explained in the guide to ETags and conditional requests, and the status codes in the guide to feed status codes.
| Feed element | Source in the code | Rule |
|---|---|---|
| Title | title attribute | Plain text, escaped |
| Link and identifier | SITE and slug | Absolute, https, never changes |
| Publication date | publishedAt | In the past, never the current time |
| Description | summary attribute | One or two complete sentences |
| Channel date | Newest publishedAt | Changes only when a post is added |
| Cache header | max-age in the action | Short enough that new posts appear quickly |
The identifier deserves the most attention. If the slug follows the title and someone corrects a typo, the address changes, and an auto-poster that stores identifiers will treat the old and the new address as two different posts. Generate the slug when the record is created and never change it afterwards. The causes and cures are covered in why a feed auto-posts the same article twice.
The query already keeps drafts and future posts out. That matters, because a draft that leaks into the feed is shared on social networks before it exists on your website. If you import old posts into the database, give them their original dates, because a feed full of items with today’s date makes an auto-poster share your whole archive in one afternoon. If you publish on a schedule, make sure that the publication date is the moment the post becomes visible, not the moment that it was written. For the effect of old dates see the guide to feed order and backdated posts.
Auto-posters build the best social posts from a feed that gives them a short summary and a picture. Write a real summary for every post instead of cutting the text automatically, because an automatic cut can end in the middle of a sentence and that is exactly what the auto-poster will copy. If a post has an image, add it as an enclosure with an absolute address, a type and a length. Whether to include the full text or only a summary is discussed in full text or excerpt in an RSS feed. Text arrives from the database as Unicode, and the response declares UTF-8, but control characters that XML forbids can still come from pasted content, so strip them when you save the record. See why special characters and emoji break feeds.
It also helps to keep the feed code in one small helper with its own test. Give the function a post whose title contains an ampersand, a quotation mark and an emoji, and confirm that the output parses as XML. That single test catches the most common production failure, and it protects you later when somebody changes the model and forgets that the feed depends on it.
Add a link element to the head of your layout view, with the relation set to alternate, the type set to the RSS media type and the absolute address of the feed. A person or a tool that only knows your home page can then find the feed on its own. The full explanation is in the guide to feed autodiscovery.
Lift the application and request the route locally, then test the deployed address from outside, because a local test says nothing about the proxy and the certificate in front of your application.
sails lift
curl -i http://localhost:1337/feed.xml
curl -i https://example.com/feed.xml
curl -i -H 'If-None-Match: "paste-the-etag-here"' https://example.com/feed.xmlA longer routine is in the feed validation guide, and a shorter one in debugging a feed with curl.
PostRSS has been publishing RSS and Atom feeds to social networks since 2014. Paste the address of your feed, 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 entry, 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. See the PostRSS features page and the plan limits on the pricing page. More developer guides are under the Technical Guide tag.
A Sails feed is one route and one action, but it has to get the details right: a query that returns only published posts with stable slugs, escaped text, absolute links, a channel date taken from the newest post, the correct content type and a validator that lets clients skip unchanged files. Make sure that no policy blocks the route, test the live address with curl and a validator, then connect it to an auto-poster. After that, every post you publish reaches your readers and your social channels without extra work.
Set the Content-Type header on the response to the RSS media type and send the XML string as the body. The response object is the same one that Express provides.
A policy is probably applied to the action, or to all actions by default. Allow anonymous access for the feed action in the policies file.
A number of milliseconds sorts reliably and converts to the date format that RSS expects. Whatever type you choose, convert it to a UTC string in one place only.
Express generates a validator for a string response and answers with a 304 status when the client sends the same value back. You can also set the validator yourself from the date of the newest post.
Yes. PostRSS reads any valid RSS or Atom address that is reachable without a login. Paste the address, connect your channels and new posts are shared as soon as they appear.
What changed in the networks, what broke, and how to fix it before it costs you reach.
Realizzati da Internet Solutions, il team dietro PostRSS. Ognuno ti fa risparmiare tempo in modo diverso.