RSS 66 sotsiaalvõrgustikku: Facebook, Instagram, X, LinkedIn, Telegram ja teised Blogi Partnerprogramm Kontaktid
Logi sisse Alusta tasuta
Updated: 2026-10-28
React Router RSS Feed: A Resource Route, Dates and Caching

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.

Why a feed belongs in a resource route

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.

Register the route

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.

Write the loader

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.

Dates must be valid and in the past

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.

Escape everything

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.

Headers and caching

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.

Where each field comes from

Feed elementSource in your appRule
Titlepost titlePlain text, escaped
Linksite address plus slugAbsolute, https, returns 200
Datepost date in UTCIn the past, never changes
Summarydescription fieldOne or two complete sentences
Identifierthe permanent linkDoes not change when the title does
Imagecover image addressAbsolute, 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.

Add an image to every item

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.

Make the feed discoverable

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.

Test before you share the address

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.xml

Work through a short list of checks:

  • The response is a 200 with an XML content type, and the body starts with an XML declaration and an rss element.
  • Every link is absolute, uses https and points to your real domain, not localhost.
  • The newest post is first, every date is in the past and the order does not change between requests.
  • Paste the public address into the W3C Feed Validation Service and fix every error.
  • Request the file twice and confirm that the second response is not slower and that the content is identical.

There is a longer routine in the feed validation guide and a short one in debugging a feed with curl.

Common mistakes to avoid

  • Returning HTML by accident. A route with a component renders a page; export only a loader for the feed.
  • Relative links. Readers and auto-posters need absolute addresses with https.
  • Unescaped text. One ampersand makes the file invalid; always escape titles and summaries.
  • Future posts in the feed. An article is shared before it exists on the website.
  • No cache header. Every request hits your data source, and a burst of bots slows the site.
  • Changing the address. Subscribers and auto-posters lose the feed without a warning.

How PostRSS works with a React Router feed

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.

Seotud lugemine

The bottom line

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.

FAQ

What is a resource route in React Router?

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.

Where do I register the feed in React Router?

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.

Why does my feed show as plain text or download in the browser?

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.

Do I need a package to build the XML?

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.

Can PostRSS read a feed built with React Router?

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.

New guides, once a month

What changed in the networks, what broke, and how to fix it before it costs you reach.

We send a confirmation e-mail first. Unsubscribe any time.

Veel tööriistu meie meeskonnalt

Loonud Internet Solutions, PostRSS-i taga olev meeskond. Iga toode säästab aega omal moel.