Everything in the previous three lessons reads a page that was built for people and tries to recover the data from it. Feeds and sitemaps are the opposite: files that sites publish specifically so machines can find their content. When one exists, using it is cheaper, more stable and more polite than scraping. A redesign that breaks every scraper you wrote leaves the feed untouched, because the feed is a promise the publisher made to every reader app and aggregator that consumes it.
This lesson covers both directions. Reading other people's feeds to track new content, and generating a sitemap so search engines can find yours.
Feeds are the polite alternative
A typical "monitor these competitors' blogs" job, done by scraping, looks like this: fetch each blog's index page every hour, extract the post links with Link Scraper, diff against yesterday's list, then fetch and convert each new post. That is several calls per site per hour, a parser that breaks when the theme changes, and a crawler hitting someone's server more than it needs to.
The same job with a feed is one call per site per poll. The feed lists the recent posts with titles, dates, summaries and links, in a format that has not changed meaningfully in two decades. You only fetch the full article when a post is new and you actually need its text.
Feeds come in two formats, RSS and Atom. They differ in element names and date formats, and readers handle both. You can usually find a site's feed in one of three ways:
- In the page head. Sites advertise feeds with
<link rel="alternate" type="application/rss+xml" href="...">(orapplication/atom+xml). View the source of the home page or blog index and search foralternate. - At a conventional path.
/feed,/feed/,/rss,/rss.xml,/atom.xml,/index.xmland/blog/feedcover a large share of sites. WordPress sites almost always answer at/feed/. - From the platform. Many hosted platforms expose feeds per author, category or tag, which are often more useful than the main feed.
RSS to JSON
RSS to JSON takes a feed URL and returns source (the feed's host), articles (how many items were parsed), maxReached, and feed[], where each item has website, title, pubDate, description and link. It costs 10 credits.
curl -X POST "https://api.apiverve.com/v1/rsstojson" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://www.nasa.gov/rss/dyn/breaking_news.rss"}'Each item in feed has title, pubDate, description and link. pubDate is a string in whatever format the feed uses.
It reads Atom as well as RSS: The Verge's index.xml is an Atom feed and came back in the same shape. That is convenient, and it hides a difference you have to handle yourself.
pubDate is a string, not a date
The NASA feed (RSS) returned "pubDate": "Thu, 22 Oct 2026 16:20:37 +0000", the RFC 822 format RSS uses. The Verge feed (Atom) returned "pubDate": "2026-10-22T18:15:00.000Z", ISO 8601. Same field, two formats, depending on the source. Sorting them as strings puts Thursday after Wednesday only by luck. Parse every date into a real timestamp at the boundary.
In JavaScript, new Date(value) parses both of those formats. In Python, datetime.fromisoformat handles neither reliably on 3.9 (it rejects the trailing Z before 3.11), and email.utils.parsedate_to_datetime handles only RFC 822, so try one and fall back to the other. Always keep the timezone; a feed that publishes in -0700 and one that publishes in Z sort correctly only as aware datetimes.
description is usually a teaser
Most feeds publish a summary, not the full article. The GitHub changelog feed ends each description with … followed by "The post ... appeared first on The GitHub Blog." The Verge's descriptions stop mid-word with "… Read the full story at The Verge." Some descriptions contain leftover markup or tab characters. Treat description as a preview for display and triage. When you need the full text, follow link and use URL to Markdown from lesson one, and only for the items that pass your filter.
Your aggregator sorts items from many feeds by `pubDate` as strings and the order is visibly wrong. What is the cause?
Answer the check above to continue.
Sitemaps: the other machine-readable map
A sitemap is an XML file listing a site's URLs, optionally with a last-modified date, a change frequency and a priority for each. Search engines read it to discover pages they might not reach by following links. For you as a data engineer, sitemaps matter in two directions.
Reading someone else's. A site's sitemap is a list of its pages that the site wants found. It usually lives at /sitemap.xml, and robots.txt often names it with a Sitemap: line. Large sites split it into a sitemap index that points to several child sitemaps. When you need the URLs of a site's product pages or articles, the sitemap is a far better starting point than crawling: one or a few files instead of thousands of page fetches. It is XML; fetch it with your own HTTP client and parse it, or turn it into JSON with XML to JSON. The lastmod values, when a site maintains them honestly, tell you which pages changed since your last pass.
Generating your own. That is what Sitemap Generator does. It does not crawl anything. You send a list of URLs and it returns a valid sitemaps.org XML document. It costs 2 credits.
curl -X POST "https://api.apiverve.com/v1/sitemapgenerator" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"urls":["/","/pricing","/docs/getting-started"],"baseUrl":"https://example.com","includeLastMod":false}'Relative paths are joined to baseUrl. urlCount is the number of url entries written.
The response gives you sitemap (the XML string), urlCount, format (XML), protocol (sitemaps.org/0.9), and a settings object echoing defaultChangeFreq, defaultPriority, includeLastMod and baseUrl. sizeBytes and sizeKb are premium.
Per-URL settings are passed by sending objects instead of strings in urls, each with url and any of priority, changeFreq and lastMod; anything you leave out uses the defaults from the top-level changeFreq (default weekly) and priority (default 0.5).
Two defaults to override
includeLastMod is true by default, and the date it writes is today. Generating a sitemap for two URLs with the default produced <lastmod>2026-10-22</lastmod> on both, which was the day of the call, not the day either page changed. Regenerate nightly and every page on your site claims to have changed every night. Search engines learn to ignore lastmod from sites that do that, which throws away the one signal that could get your updated pages recrawled sooner. Either pass a real lastMod per URL from your CMS or database, or set includeLastMod to false.
It does not deduplicate. Sending /pricing twice produced two identical <url> entries and a urlCount of 4 for four inputs. Duplicates in a sitemap are sloppy at best. Deduplicate, and normalise trailing slashes consistently, before you call.
// Node 18+, ESM
import { writeFile } from 'node:fs/promises';
import { apiverve } from './apiverve.mjs';
// In a real site these come from your database or CMS, with real modification dates.
const pages = [
{ path: '/', updated: '2026-10-01' },
{ path: '/pricing', updated: '2026-09-14' },
{ path: '/pricing', updated: '2026-09-14' }, // duplicate, removed below
{ path: '/docs/getting-started', updated: '2026-10-20' },
];
const unique = [...new Map(pages.map((p) => [p.path, p])).values()];
const data = await apiverve('sitemapgenerator', {
baseUrl: 'https://example.com',
urls: unique.map((p) => ({ url: p.path, lastMod: p.updated })),
changeFreq: 'monthly',
});
await writeFile('sitemap.xml', data.sitemap);
console.log(`wrote ${data.urlCount} URLs`);The sitemaps.org protocol caps a single sitemap at 50,000 URLs and 50 MB uncompressed. Past that, split into several files and publish a sitemap index that lists them. Then reference the sitemap (or the index) from robots.txt with a Sitemap: line, and submit it in Google Search Console and Bing Webmaster Tools so you can see how many URLs were accepted.
Choosing your source
You now have every way to get content from the web covered in this course, and the order to try them in is roughly the order of politeness:
- A feed, when you want to know what is new. One small call per poll, stable format.
- A sitemap, when you want the full list of a site's pages. One file, maintained by the site.
- Metadata, when you want to know what a page is. Two credits, no body text.
- Text or Markdown conversion, when you need the content itself, for the specific pages you actually need.
- Screenshots, when only the rendered result answers the question.
Every step down that list costs more and asks more of the site you are reading. Starting at the top and going only as far as the task needs is both the cheapest design and the one that keeps you welcome.
That completes Extract Web Content. The next course in the Web Data Developer certification moves from reading pages to judging them, with SEO checks, tech stack detection, DNS, SSL and domain intelligence: Analyze Websites.