DevToys Web Pro iconDevToys Web ProBlog
Çeviri: LocalePack logoLocalePack
Bizi değerlendirin:
Tarayıcı uzantısını deneyin:
← Back to Blog

OPML and RSS: Parsing Feed Subscriptions, Extracting URLs, and Migrating Between Readers

7 min read

Every major RSS reader can export your subscriptions as an OPML file and import one back. OPML is the de-facto interchange format for feed lists — yet its structure is rarely explained in detail. This guide covers how OPML files are built, how to extract feed URLs programmatically, how to generate OPML from a flat list of feeds, and what to watch for when merging subscription lists from multiple readers. Use the OPML to RSS converter and the RSS to OPML converter to follow along with your own files.

What Is OPML

OPML stands for Outline Processor Markup Language. Dave Winer created it in 2000 as a generic format for hierarchical outlines — originally for outliner software, not RSS. Its adoption as the standard container for subscription lists was opportunistic: Winer also invented RSS, and OPML was already a convenient tree format. By the mid-2000s, every major feed reader had adopted it for import and export.

The format has not changed substantially since version 2.0 (2007). It is plain XML with one required convention: a <body> element containing nested <outline> elements. For RSS subscriptions, each feed gets its own <outline> with a handful of attributes. Everything else — categories, folders, metadata — is expressed through nesting and additional attributes.

OPML File Structure

An OPML document has three parts: the root <opml> element, a <head> with metadata, and a <body> with the actual outlines.

<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
  <head>
    <title>My Feed Subscriptions</title>
    <dateCreated>Sun, 20 Apr 2026 00:00:00 +0000</dateCreated>
    <ownerName>Jane Smith</ownerName>
  </head>
  <body>
    <outline text="Technology" title="Technology">
      <outline type="rss"
               text="Hacker News"
               title="Hacker News"
               xmlUrl="https://news.ycombinator.com/rss"
               htmlUrl="https://news.ycombinator.com/"/>
      <outline type="rss"
               text="The Verge"
               title="The Verge"
               xmlUrl="https://www.theverge.com/rss/index.xml"
               htmlUrl="https://www.theverge.com/"/>
    </outline>
    <outline type="rss"
             text="NASA Breaking News"
             title="NASA Breaking News"
             xmlUrl="https://www.nasa.gov/rss/dyn/breaking_news.rss"
             htmlUrl="https://www.nasa.gov/"/>
  </body>
</opml>

Key attributes on feed outlines:

  • type="rss" — marks this outline as a feed subscription (required for feed readers to recognize it)
  • xmlUrl — the actual feed URL that a reader polls for new items; this is the one you need when extracting feeds
  • htmlUrl — the human-readable website URL; useful for display but not for polling
  • text — the display name shown in the reader; required by the spec
  • title — often duplicates text; some exporters omit one or the other

Minimal OPML with Multiple Feeds

<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
  <head><title>Dev Feeds</title></head>
  <body>
    <outline type="rss" text="CSS-Tricks"
             xmlUrl="https://css-tricks.com/feed/"
             htmlUrl="https://css-tricks.com/"/>
    <outline type="rss" text="Smashing Magazine"
             xmlUrl="https://www.smashingmagazine.com/feed/"
             htmlUrl="https://www.smashingmagazine.com/"/>
    <outline type="rss" text="A List Apart"
             xmlUrl="https://alistapart.com/main/feed/"
             htmlUrl="https://alistapart.com/"/>
    <outline type="rss" text="web.dev"
             xmlUrl="https://web.dev/feed.xml"
             htmlUrl="https://web.dev/"/>
    <outline type="rss" text="MDN Blog"
             xmlUrl="https://developer.mozilla.org/en-US/blog/rss.xml"
             htmlUrl="https://developer.mozilla.org/en-US/blog/"/>
    <outline type="rss" text="GitHub Blog"
             xmlUrl="https://github.blog/feed/"
             htmlUrl="https://github.blog/"/>
    <outline type="rss" text="Node.js Blog"
             xmlUrl="https://nodejs.org/en/feed/blog.xml"
             htmlUrl="https://nodejs.org/en/blog/"/>
  </body>
</opml>

RSS Reader Import and Export Support

ReaderOPML ImportOPML ExportNotes
FeedlyYesYesExport from Organize → Import/Export; preserves folders
InoreaderYesYesSubscription → Import/Export; tags become folders in OPML
NewsBlurYesYesAccount → Manage → Import feeds; folder structure preserved
The Old ReaderYesYesImport/Export in settings; flat list only on free tier
NetNewsWireYesYesFile → Import Subscriptions; native macOS/iOS app

Most readers import OPML by creating folders for top-level category outlines and placing feed outlines inside them. An outline without type="rss" and without children is typically ignored.

Extracting Feed URLs Programmatically

When you need to pull just the feed URLs out of an OPML file, three approaches cover the main environments.

XPath

XPath is the most concise option. The expression //outline[@type="rss"]/@xmlUrl selects the xmlUrl attribute from every <outline> element that has type="rss", at any nesting depth.

# Extract all feed URLs with xmllint
xmllint --xpath '//outline[@type="rss"]/@xmlUrl' subscriptions.opml \
  | grep -oP '(?<=xmlUrl=")[^"]+'

JavaScript — DOMParser

In the browser or a Deno/Node.js environment with a DOM library, DOMParser parses the XML and querySelectorAll handles the selection:

function extractFeedUrls(opmlString) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(opmlString, "application/xml");

  const outlines = doc.querySelectorAll('outline[type="rss"]');
  return Array.from(outlines).map((el) => ({
    title: el.getAttribute("text") || el.getAttribute("title") || "",
    xmlUrl: el.getAttribute("xmlUrl") || "",
    htmlUrl: el.getAttribute("htmlUrl") || "",
  }));
}

// Usage
const feeds = extractFeedUrls(opmlText);
console.log(feeds.map((f) => f.xmlUrl));

Python — xml.etree.ElementTree

import xml.etree.ElementTree as ET

def extract_feed_urls(path):
    tree = ET.parse(path)
    root = tree.getroot()
    feeds = []
    # iter() traverses all descendants regardless of nesting depth
    for outline in root.iter("outline"):
        if outline.get("type") == "rss":
            feeds.append({
                "title": outline.get("text") or outline.get("title", ""),
                "xmlUrl": outline.get("xmlUrl", ""),
                "htmlUrl": outline.get("htmlUrl", ""),
            })
    return feeds

feeds = extract_feed_urls("subscriptions.opml")
for f in feeds:
    print(f["xmlUrl"])

Categorization with Nested Outlines

Folders in an OPML file are represented by <outline> elements that have no type attribute (or type="folder") and contain child <outline> elements as feeds. The text attribute on the parent becomes the folder name in the reader.

<outline text="Design" title="Design">
  <outline type="rss" text="Sidebar.io"
           xmlUrl="https://sidebar.io/feed.xml"
           htmlUrl="https://sidebar.io/"/>
  <outline type="rss" text="UX Collective"
           xmlUrl="https://uxdesign.cc/feed"
           htmlUrl="https://uxdesign.cc/"/>
</outline>

OPML allows arbitrary nesting depth, but most readers only honor one or two levels of folders. Deeply nested structures are typically flattened on import.

Generating OPML from a Flat List of Feeds

If you have a plain text list of RSS feed URLs and want to import them into a reader, the RSS to OPML converter handles the wrapping automatically. For programmatic generation:

function generateOpml(feeds, title = "My Feeds") {
  const outlines = feeds
    .map(
      ({ text, xmlUrl, htmlUrl }) =>
        `  <outline type="rss" text="${escapeXml(text)}" title="${escapeXml(text)}"\n` +
        `           xmlUrl="${escapeXml(xmlUrl)}"\n` +
        `           htmlUrl="${escapeXml(htmlUrl || "")}"/>`
    )
    .join("\n");

  return `<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
  <head><title>${escapeXml(title)}</title></head>
  <body>
${outlines}
  </body>
</opml>`;
}

function escapeXml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

Feed URL Discovery

When building an OPML file from scratch, you often need to discover the feed URL for a given website. Three reliable methods:

  • HTML autodiscovery link: Most blogs include a <link> tag in their HTML <head>:
    <link rel="alternate" type="application/rss+xml"
          title="My Blog RSS" href="/feed.xml">
    Fetch the page and extract href from any link[rel="alternate"][type="application/rss+xml"] or link[rel="alternate"][type="application/atom+xml"] element.
  • Well-known paths: Try /feed, /rss, /rss.xml, /feed.xml, /atom.xml, /index.xml in order. Many platforms (WordPress, Ghost, Hugo) use these defaults.
  • CMS conventions: WordPress appends ?feed=rss2 to any URL. Substack uses /feed. Medium uses https://medium.com/feed/@username.

Pitfalls and Edge Cases

  • Mixed Atom and RSS feeds: OPML uses type="rss" for both RSS and Atom feeds by convention. The actual feed format is determined by the content at xmlUrl, not the OPML attribute. Do not filter by type to distinguish them — fetch and inspect the feed root element (<rss> vs <feed> with Atom namespace).
  • Duplicate URLs after merging: When combining OPML files from multiple readers, the same feed often appears more than once, sometimes with different display names. Deduplicate on xmlUrl after normalizing the URL (lowercase scheme and host, strip trailing slashes, remove default ports).
  • Unicode in titles: OPML is XML and must be UTF-8. Most exporters handle this correctly, but some older readers emit Latin-1 without declaring it in the XML declaration, which breaks standard parsers. If you encounter parse errors on valid-looking files, try re-encoding to UTF-8 before parsing.
  • Missing type attribute: Some exporters omit type="rss" on leaf outlines. If you rely on type-based filtering, also accept outlines that have an xmlUrl attribute but no explicit type.
  • Relative xmlUrl values: Rare but possible in hand-crafted files. Resolve against the document base URL before polling.

Convert between OPML and RSS feed lists directly in your browser with the OPML to RSS converter and the RSS to OPML converter. For a broader look at format conversion tools, see the data converters guide.