
Short answer: In React Router framework mode you publish an RSS feed with a resource route: a route module that exports a loader and no component, so that it returns a Response with the XML instead of rendering a page. Register the route at a path such as /rss.xml, build the XML from your posts with absolute links and valid dates, escape every text value, send an XML content type and a cache header, and test the deployed address with curl and a validator. Then give that address to an auto-poster so that each new post is shared on your social networks.
React Router version 7 is the successor of Remix, and its framework mode brings the loaders, actions and server rendering that Remix users know into a single package. A feed is a good example of something that is not a page: it has no interface, it is read by machines, and it must be correct down to the last character. React Router has a clean answer for this kind of endpoint, which is called a resource route. This guide shows how to build one, how to avoid the usual traps with dates, escaping and caching, and how to make sure the feed keeps working when an auto-poster relies on it.
A normal route in React Router renders a component inside the layout of your site. A feed cannot do that, because a reader expects a plain XML document with no HTML around it. A resource route is a route module without a default component. When a request matches it, React Router calls its loader and sends back whatever response the loader returns, unchanged. That is exactly what a feed needs. The same pattern is used for sitemaps, image generation and JSON endpoints.
If you came from Remix, the idea is identical and you can compare it with the Remix resource route guide. The main differences are the route configuration file and the generated types, which are explained below.
In framework mode, routes are declared in a routes file at the top of the app folder. Add a line for the feed and point it to a module that you will create next.
// app/routes.ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("blog/:slug", "routes/post.tsx"),
route("rss.xml", "routes/rss.ts"),
] satisfies RouteConfig;The path is the address of the feed on your site, so this one answers at /rss.xml. Choose the name once and keep it, because subscribers and auto-posters store the address. If you use file-based routing instead of an explicit routes file, name the module so that the dot in the file name is escaped as the documentation describes; otherwise the route is nested and the address is not what you expect.
The loader fetches your posts, builds the XML and returns it with the right headers. The example below uses a function named getPosts that stands for whatever source you have: markdown files, a database or a content service.
// app/routes/rss.ts
import type { Route } from "./+types/rss";
import { getPosts } from "~/lib/posts.server";
const SITE = "https://example.com";
function esc(value: string) {
return value
.replaceAll("&", "&" + "amp;")
.replaceAll("<", "&" + "lt;")
.replaceAll(">", "&" + "gt;");
}
export async function loader({ request }: Route.LoaderArgs) {
const posts = (await getPosts())
.filter((p) => new Date(p.date) <= new Date())
.sort((a, b) => +new Date(b.date) - +new Date(a.date))
.slice(0, 20);
const items = posts
.map(
(p) => `
<item>
<title>${esc(p.title)}</title>
<link>${SITE}/blog/${p.slug}</link>
<guid isPermaLink="true">${SITE}/blog/${p.slug}</guid>
<pubDate>${new Date(p.date).toUTCString()}</pubDate>
<description>${esc(p.summary)}</description>
</item>`,
)
.join("");
const xml = `<?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>
<atom:link href="${SITE}/rss.xml" rel="self" type="application/rss+xml" />${items}
</channel>
</rss>`;
return new Response(xml, {
headers: {
"Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "public, max-age=0, s-maxage=900",
},
});
}Because the module has no default export, React Router treats it as a resource route. The type import comes from the types that the framework generates for each route, so the loader arguments are checked by the compiler. If you do not use generated types, a plain object parameter works as well, and the request is rarely needed for a feed.
The pubDate of an RSS 2.0 item follows the format of an email date, and the toUTCString method produces a value that validators accept. Two cautions apply. First, a date parsed from a string without a time zone is read in the zone of the server, so a post written in the evening can move to the next day when the code runs in a different region. Store dates with an explicit offset, or in UTC, and use the same value everywhere. Second, never include posts dated in the future. An auto-poster shares a feed item as soon as it appears, and a scheduled article that is visible in the feed but not yet on the website gives readers a broken link. The filter in the loader above removes those posts.
An RSS file is XML, and a single unescaped ampersand makes it invalid. Titles such as Q and A, summaries with a less-than sign and names with accents are common sources of trouble. The helper above escapes the three characters that matter in text content. If you include the full article in a content element, wrap it in a CDATA section, and make sure that the text itself does not contain the closing sequence of a CDATA section. For more on special characters and encodings, see why special characters and emoji break auto-posted content.
The loader returns two headers, and both matter. The content type tells readers and tools that the file is XML; a wrong type is a frequent reason for a feed that opens as plain text in a browser, and it is covered in the content type header guide. The cache header lets a CDN keep the file for a few minutes, so that a burst of requests from readers and bots does not hit your database every time. Fifteen minutes is a reasonable default for a blog. If you publish rarely and want faster updates, lower it. If your host does not cache at the edge, the header still helps browsers and tools that respect it.
You can go further with conditional requests: compute a hash of the XML, send it as an ETag, and return a 304 response when the request already carries the same value. The mechanics are explained in the guide to ETags and conditional requests.
| Feed element | Source in your app | Rule |
|---|---|---|
| Title | post title | Plain text, escaped |
| Link | site address plus slug | Absolute, https, returns 200 |
| Date | post date in UTC | In the past, never changes |
| Summary | description field | One or two complete sentences |
| Identifier | the permanent link | Does not change when the title does |
| Image | cover image address | Absolute, at least 1200 pixels wide |
Take particular care of the identifier. If a post gets a new slug, its link and identifier change, and an auto-poster that stores identifiers treats the new address as a new post. Keep slugs stable, and add redirects when you must rename. The causes and fixes are covered in why a feed auto-posts the same article twice.
Auto-posters build previews from the item itself, so an image address in the feed makes each shared post more attractive. Add an enclosure or a media element with the absolute address of the cover image, and make sure the file is served over https and is large enough for a good preview. If you only rely on the page, the Open Graph image of each post page is used instead; see why auto-posted links show the wrong image.
Add a link element to the head of your root layout so that browsers, feed readers and tools can find the feed from any page. In React Router you can return it from a links function or write it directly in the root document.
// app/root.tsx (inside the head)
<link
rel="alternate"
type="application/rss+xml"
title="Example Journal"
href="https://example.com/rss.xml"
/>With that in place, a person or a tool that only knows your home page can find the feed by itself. The details are in the guide to feed autodiscovery.
Run the application as it will run in production and look at the response before you publish anything.
npm run build && npm run start
curl -i http://localhost:3000/rss.xml
curl -I https://example.com/rss.xmlWork through a short list of checks:
There is a longer routine in the feed validation guide and a short 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 summary 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.
React Router needs one resource route to publish a dependable feed: register a path, export a loader without a component, build the XML from published posts with absolute links, valid dates and escaped text, return an XML content type and a cache header, and keep the address and the slugs unchanged. Add an image to each item, make the feed discoverable from the root layout, test the deployed file with curl and a validator and connect it to an auto-poster. After that, every post you publish can reach your readers and your social channels automatically.
It is a route module that exports a loader or an action but no default component, so the response it returns is sent as is. It is the right tool for feeds, sitemaps and other machine-readable files.
In framework mode, add a route line to the routes file in the app folder, with the public path of the feed and the path of the module that contains the loader.
The content type header is probably missing or wrong. Return an XML type such as application/xml with a charset, and check the header with curl.
No. A template string with an escape helper is enough for a simple feed. A feed package is useful when you need many optional elements, but it is not required.
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.