GuidesFEEDS
Fetch an RSS feed in JavaScript and display it in HTML
To show an RSS feed on a web page, fetch it through https://proxy.cors.dev/ followed by the feed URL, parse the XML with DOMParser, and build the list with DOM methods. The proxy is needed because most feeds send no Access-Control-Allow-Origin header, so a direct fetch() from the page fails. The same code handles RSS 2.0 and Atom feeds, and the free tier needs no signup and no API key while returning XML responses up to 1 MiB.
Why fetch() fails for most feeds
A page can read a response from another origin only if the response has an Access-Control-Allow-Origin header. Many feed servers leave it out: when we checked, the BBC News feed https://feeds.bbci.co.uk/news/rss.xml and https://hnrss.org/frontpage sent none. Some feeds send it: https://github.blog/feed/ and https://dev.to/feed answer with Access-Control-Allow-Origin: *, and a plain fetch() works for them. For the others Chrome logs No 'Access-Control-Allow-Origin' header is present on the requested resource and fetch() rejects with TypeError: Failed to fetch.
# No output: the feed sends no CORS header, so browsers block it
curl -sI -H 'Origin: https://example.com' https://feeds.bbci.co.uk/news/rss.xml | grep -i access-control-allow-origin
# access-control-allow-origin: * means fetch() works without a proxy
curl -sI -H 'Origin: https://example.com' https://github.blog/feed/ | grep -i access-control-allow-originThe header has to come from the feed's server; your page cannot add it. Without it you have two options: fetch the feed on a server you run, or through a CORS proxy. For a general walkthrough, see fix CORS errors.
Fetch and parse the feed
fetchFeed() puts https://proxy.cors.dev/ in front of the feed URL and sends credentials: 'omit'. When a request fails, the proxy sets the X-Cors-Error response header, for example response_too_large or upstream_timeout, and the function includes it in the thrown error. DOMParser with application/xml turns the text into an XML document, where a parsererror element means the response was not valid XML. RSS 2.0 puts each entry in an item element and Atom in an entry element; the selector item, entry matches both, and each match becomes an object with title, link, date and summary.
const PROXY = 'https://proxy.cors.dev/';
async function fetchFeed(feedUrl) {
const response = await fetch(PROXY + feedUrl, { credentials: 'omit' });
if (!response.ok) {
throw new Error(`Feed request failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
const xml = new DOMParser().parseFromString(await response.text(), 'application/xml');
if (xml.querySelector('parsererror')) throw new Error('The response is not valid XML');
// RSS 2.0 feeds use <item>, Atom feeds use <entry>
return [...xml.querySelectorAll('item, entry')].map((entry) => {
const text = (selector) => entry.querySelector(selector)?.textContent.trim() ?? '';
const link = entry.querySelector('link[rel="alternate"]') ?? entry.querySelector('link');
return {
title: text('title'),
link: link?.getAttribute('href') || link?.textContent.trim() || '',
date: text('pubDate') || text('published') || text('updated'),
summary: text('description') || text('summary') || text('content'),
};
});
}
const items = await fetchFeed('https://feeds.bbci.co.uk/news/rss.xml');
console.log(JSON.stringify(items.slice(0, 3), null, 2));Display the items in HTML
Feed content comes from someone else's server, so treat it as untrusted. Set text with textContent or DOM methods, never with innerHTML. The render code only sets href for http: and https: links, which blocks javascript: URLs. Summaries often contain HTML (the hnrss.org feed puts <p> and <a> tags in description); the code turns them into plain text with DOMParser and text/html, and a document created by DOMParser does not run scripts.
// Markup: <ul id="feed"></ul>
const list = document.querySelector('#feed');
list.replaceChildren(
...items.slice(0, 10).map((item) => {
const entry = document.createElement('li');
const link = document.createElement('a');
const summary = document.createElement('p');
// Feed content is untrusted: allow only http(s) links and plain text
if (/^https?:\/\//i.test(item.link)) link.href = item.link;
link.textContent = item.title;
summary.textContent = new DOMParser().parseFromString(item.summary, 'text/html').body.textContent;
const published = new Date(item.date);
if (!Number.isNaN(published.valueOf())) {
const time = document.createElement('time');
time.dateTime = published.toISOString();
time.textContent = published.toLocaleDateString();
entry.append(time, ' ');
}
entry.append(link, summary);
return entry;
}),
);RSS 2.0 and Atom fields
RSS 2.0 and Atom name the same fields differently. The table shows where fetchFeed() looks for each value.
| Field | RSS 2.0 | Atom |
|---|---|---|
| Entries | item elements | entry elements |
| title | item > title | entry > title |
| link | Text of item > link | href of entry > link, rel="alternate" first |
| date | item > pubDate | entry > published, then updated |
| summary | item > description | entry > summary, then content |
Convert RSS to JSON
fetchFeed() already returns plain JavaScript objects, and JSON.stringify(items) turns them into JSON. No RSS-to-JSON conversion service or API key is needed: the proxy returns the feed's original XML and the conversion happens in the browser.
Limits
Feeds that include the full text of every article can go over the 1 MiB cap. If a feed is too large, use a shorter summary feed if the site offers one, fetch it on your own server, or use managed access. Managed access returns up to 6 MiB and costs $5/month on Pro.
| Limit | Free | Pro |
|---|---|---|
| Price | $0 | $5 per month |
| Account | None: no key or signup | Account with a Connection key |
| Requests | No monthly quota | 500,000 per monthly billing period |
| Rate limit | Shared pool with fair-use limits per IP | 600 requests per minute, 10 concurrent |
| Response size | 1 MiB | 6 MiB |
| Methods | GET and HEAD | GET, HEAD, POST, PUT, PATCH and DELETE |
When to fetch feeds on a server
- Server code such as Node.js, a Cloudflare Worker or a static site generator's build step is not subject to CORS and can fetch any feed directly; see Cloudflare Workers CORS.
- Use a server when the feed needs cookies, a login or an API key, as a public proxy should never carry credentials.
- Use a server to fetch a feed once and serve many visitors from a cache, or for feeds larger than 1 MiB.
Good questions.
How do I fetch an RSS feed with JavaScript?
Call fetch() with https://proxy.cors.dev/ in front of the feed URL when the feed sends no CORS headers. Parse the text with DOMParser and application/xml. Then select the item or entry elements.
Why do I get a CORS error when fetching an RSS feed?
The feed's server does not send Access-Control-Allow-Origin, so the browser blocks your page from reading the response. The fix is a server you run or a CORS proxy, because the page cannot add the header.
Can I display an RSS feed on a static HTML page?
Yes, the code runs in the page and needs no backend. It works on static hosts such as GitHub Pages. Add a <ul id="feed"> element and run the fetch and render code.
Does this work for Atom feeds?
Yes, the code reads Atom entry elements, takes the link from the href attribute and the date from published or updated. YouTube channel feeds are Atom: YouTube RSS feed.
How large can the feed be?
Feeds can be up to 1 MiB on the free tier. Larger responses fail with HTTP 502 and X-Cors-Error: response_too_large. Managed access on Pro allows responses up to 6 MiB.
Do I need an RSS to JSON service like rss2json?
No, DOMParser reads the XML in the browser. Calling JSON.stringify() on the parsed items gives JSON. No conversion service and no API key are involved.