Skip to Content
DevelopmentAPIOPML Support

OPML Support

Comprehensive guide to working with OPML (Outline Processor Markup Language) files for feed subscriptions.

Overview

OPML is an XML-based format for exchanging subscription lists between feed readers. rss.Today provides:

  • OPMLParser: Parse OPML subscription lists
  • OPMLGenerator: Generate OPML subscription lists

OPMLParser

Parse OPML files to extract feed subscription information.

Basic Usage

import { OPMLParser } from 'rss.today'; const parser = new OPMLParser(); const opml = await parser.parseOPML(opmlXml); console.log(opml.title); console.log(opml.outlines.length);

OPMLDocument Interface

interface OPMLDocument { version?: string; // OPML version (e.g., "1.0", "2.0") title?: string; // Document title dateCreated?: Date; // Creation date dateModified?: Date; // Last modification date ownerName?: string; // Owner name ownerEmail?: string; // Owner email outlines: OPMLOutline[]; // List of outlines }

OPMLOutline Interface

interface OPMLOutline { text?: string; // Display text title?: string; // Title attribute type?: string; // Type (e.g., "rss", "link") xmlUrl?: string; // RSS feed URL htmlUrl?: string; // Website URL description?: string; // Description category?: string[]; // Categories outlines?: OPMLOutline[]; // Nested outlines }

Parsing from File

import { OPMLParser } from 'rss.today'; import * as fs from 'fs/promises'; const parser = new OPMLParser(); const xml = await fs.readFile('./subscriptions.opml', 'utf-8'); const opml = await parser.parseOPML(xml); console.log(`Title: ${opml.title}`); console.log(`Outlines: ${opml.outlines.length}`); console.log(`Owner: ${opml.ownerName} <${opml.ownerEmail}>`);

Parsing Nested Outlines

const parser = new OPMLParser(); const opml = await parser.parseOPML(xml); // Iterate through top-level outlines opml.outlines.forEach(outline => { console.log(`Category: ${outline.text}`); if (outline.outlines) { // Process nested outlines outline.outlines.forEach(feed => { console.log(` Feed: ${feed.text}`); console.log(` URL: ${feed.xmlUrl}`); console.log(` Site: ${feed.htmlUrl}`); }); } });

Filtering by Type

const parser = new OPMLParser(); const opml = await parser.parseOPML(xml); // Get only RSS feeds const rssFeeds = opml.outlines.filter(outline => outline.type === 'rss'); console.log(`Found ${rssFeeds.length} RSS feeds`); rssFeeds.forEach(feed => { console.log(`${feed.text}: ${feed.xmlUrl}`); });

OPMLGenerator

Generate OPML subscription lists from scratch.

Basic Usage

import { OPMLGenerator, type OPMLDocument } from 'rss.today'; const opml: OPMLDocument = { title: 'My Subscriptions', outlines: [ { text: 'TechCrunch', type: 'rss', xmlUrl: 'https://techcrunch.com/feed/', htmlUrl: 'https://techcrunch.com' } ] }; const generator = new OPMLGenerator(); const opmlXml = generator.generateOPML(opml); console.log(opmlXml);

Complete OPML Document

import { OPMLGenerator, type OPMLDocument } from 'rss.today'; const opml: OPMLDocument = { version: '2.0', title: 'My Feed Subscriptions', dateCreated: new Date(), dateModified: new Date(), ownerName: 'John Doe', ownerEmail: 'john@example.com', outlines: [ { text: 'Technology', type: 'folder', outlines: [ { text: 'TechCrunch', type: 'rss', xmlUrl: 'https://techcrunch.com/feed/', htmlUrl: 'https://techcrunch.com', description: 'Technology news' }, { text: 'Ars Technica', type: 'rss', xmlUrl: 'https://feeds.arstechnica.com/arstechnica/index', htmlUrl: 'https://arstechnica.com', description: 'Technology news and analysis' } ] }, { text: 'News', type: 'folder', outlines: [ { text: 'BBC News', type: 'rss', xmlUrl: 'http://feeds.bbci.co.uk/news/rss.xml', htmlUrl: 'https://www.bbc.com/news', category: ['world', 'uk'] } ] } ] }; const generator = new OPMLGenerator(); const opmlXml = generator.generateOPML(opml);

Saving to File

import { OPMLGenerator, type OPMLDocument } from 'rss.today'; import * as fs from 'fs/promises'; const opml: OPMLDocument = { title: 'My Subscriptions', outlines: [ { text: 'Example Feed', type: 'rss', xmlUrl: 'https://example.com/feed.xml', htmlUrl: 'https://example.com' } ] }; const generator = new OPMLGenerator(); const opmlXml = generator.generateOPML(opml); await fs.writeFile('./subscriptions.opml', opmlXml); console.log('OPML file saved');

Complete Examples

Import Subscriptions from File

import { OPMLParser, Parser } from 'rss.today'; import * as fs from 'fs/promises'; async function importSubscriptions(filePath: string) { // Parse OPML file const xml = await fs.readFile(filePath, 'utf-8'); const opmlParser = new OPMLParser(); const opml = await opmlParser.parseOPML(xml); console.log(`Importing: ${opml.title}`); console.log(`Total feeds: ${countFeeds(opml.outlines)}\n`); // Parse each feed const feedParser = new Parser(); for (const outline of opml.outlines) { if (outline.type === 'rss' && outline.xmlUrl) { try { const feed = await feedParser.parseURL(outline.xmlUrl); console.log(`✓ ${outline.text}`); console.log(` ${feed.title}`); console.log(` Items: ${feed.items.length}\n`); } catch (error) { console.log(`✗ ${outline.text}`); console.log(` Error: ${error.message}\n`); } } else if (outline.outlines) { // Process nested outlines for (const feed of outline.outlines) { if (feed.type === 'rss' && feed.xmlUrl) { try { const parsed = await feedParser.parseURL(feed.xmlUrl); console.log(`✓ ${feed.text}`); console.log(` ${parsed.title}\n`); } catch (error) { console.log(`✗ ${feed.text}\n`); } } } } } } function countFeeds(outlines: any[]): number { let count = 0; for (const outline of outlines) { if (outline.type === 'rss') { count++; } else if (outline.outlines) { count += countFeeds(outline.outlines); } } return count; } importSubscriptions('./subscriptions.opml');

Export Feeds to OPML

import { Parser, OPMLGenerator, type OPMLDocument } from 'rss.today'; async function exportToOPML(feedUrls: string[], outputPath: string) { const parser = new Parser(); const outlines = []; for (const url of feedUrls) { try { const feed = await parser.parseURL(url); outlines.push({ text: feed.title || url, type: 'rss', xmlUrl: url, htmlUrl: feed.link, description: feed.description }); console.log(`Added: ${feed.title}`); } catch (error) { console.log(`Failed: ${url}`); } } const opml: OPMLDocument = { title: 'Exported Subscriptions', dateCreated: new Date(), dateModified: new Date(), outlines }; const generator = new OPMLGenerator(); const opmlXml = generator.generateOPML(opml); await fs.writeFile(outputPath, opmlXml); console.log(`Exported ${outlines.length} feeds to ${outputPath}`); } exportToOPML( [ 'https://techcrunch.com/feed/', 'https://arstechnica.com/feed/', 'https://www.theverge.com/rss/index.xml' ], './exports/subscriptions.opml' );

OPML Viewer

import { OPMLParser } from 'rss.today'; import * as fs from 'fs/promises'; async function viewOPML(filePath: string) { const xml = await fs.readFile(filePath, 'utf-8'); const parser = new OPMLParser(); const opml = await parser.parseOPML(xml); console.log('═══════════════════════════════════'); console.log(`OPML: ${opml.title}`); console.log('═══════════════════════════════════\n'); if (opml.ownerName) { console.log(`Owner: ${opml.ownerName}`); if (opml.ownerEmail) { console.log(`Email: ${opml.ownerEmail}`); } console.log(); } if (opml.dateCreated) { console.log(`Created: ${opml.dateCreated.toLocaleString()}`); } if (opml.dateModified) { console.log(`Modified: ${opml.dateModified.toLocaleString()}`); } console.log(); console.log('═══════════════════════════════════'); console.log('Subscriptions'); console.log('═══════════════════════════════════\n'); printOutlines(opml.outlines, 0); } function printOutlines(outlines: any[], indent: number) { const prefix = ' '.repeat(indent); for (const outline of outlines) { if (outline.type === 'rss') { console.log(`${prefix}📰 ${outline.text}`); console.log(`${prefix} Feed: ${outline.xmlUrl}`); if (outline.htmlUrl) { console.log(`${prefix} Site: ${outline.htmlUrl}`); } if (outline.description) { console.log(`${prefix} ${outline.description}`); } if (outline.category) { console.log(`${prefix} Tags: ${outline.category.join(', ')}`); } console.log(); } else if (outline.outlines) { console.log(`${prefix}📁 ${outline.text}`); console.log(); printOutlines(outline.outlines, indent + 1); } } } viewOPML('./subscriptions.opml');

Merge Multiple OPML Files

import { OPMLParser, OPMLGenerator, type OPMLDocument } from 'rss.today'; import * as fs from 'fs/promises'; async function mergeOPMLFiles(filePaths: string[], outputPath: string) { const parser = new OPMLParser(); const allOutlines: any[] = []; const feedUrls = new Set(); for (const filePath of filePaths) { console.log(`Processing: ${filePath}`); const xml = await fs.readFile(filePath, 'utf-8'); const opml = await parser.parseOPML(xml); console.log(` Found: ${countFeeds(opml.outlines)} feeds`); // Add unique feeds addUniqueFeeds(opml.outlines, allOutlines, feedUrls); } const merged: OPMLDocument = { title: 'Merged Subscriptions', dateCreated: new Date(), dateModified: new Date(), outlines: allOutlines }; const generator = new OPMLGenerator(); const opmlXml = generator.generateOPML(merged); await fs.writeFile(outputPath, opmlXml); console.log(`\nMerged ${feedUrls.size} feeds to ${outputPath}`); } function countFeeds(outlines: any[]): number { let count = 0; for (const outline of outlines) { if (outline.type === 'rss') { count++; } else if (outline.outlines) { count += countFeeds(outline.outlines); } } return count; } function addUniqueFeeds( outlines: any[], target: any[], seen: Set<string> ) { for (const outline of outlines) { if (outline.type === 'rss' && outline.xmlUrl) { if (!seen.has(outline.xmlUrl)) { seen.add(outline.xmlUrl); target.push(outline); } } else if (outline.outlines) { addUniqueFeeds(outline.outlines, target, seen); } } } mergeOPMLFiles( [ './opml1.opml', './opml2.opml', './opml3.opml' ], './merged-subscriptions.opml' );

Create Category-based OPML

import { OPMLGenerator, type OPMLDocument } from 'rss.today'; function createCategorizedOPML(feeds: Record<string, string[]>): OPMLDocument { const outlines = []; for (const [category, urls] of Object.entries(feeds)) { const categoryOutlines = urls.map(url => ({ text: url, type: 'rss', xmlUrl: url })); outlines.push({ text: category, type: 'folder', outlines: categoryOutlines }); } return { title: 'Categorized Subscriptions', dateCreated: new Date(), dateModified: new Date(), outlines }; } const feedsByCategory = { Technology: [ 'https://techcrunch.com/feed/', 'https://arstechnica.com/feed/', 'https://www.theverge.com/rss/index.xml' ], News: [ 'https://feeds.bbci.co.uk/news/rss.xml', 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml' ], Sports: [ 'https://www.espn.com/espn/rss/news', 'https://www.si.com/rss/si_topstories.rss' ] }; const opml = createCategorizedOPML(feedsByCategory); const generator = new OPMLGenerator(); const opmlXml = generator.generateOPML(opml); console.log(opmlXml);

Best Practices

Include Essential Metadata

const opml: OPMLDocument = { version: '2.0', title: 'My Subscriptions', dateCreated: new Date(), dateModified: new Date(), ownerName: 'Your Name', ownerEmail: 'your@example.com', outlines: [...] };

Use Nested Structure for Organization

const opml: OPMLDocument = { title: 'Organized Subscriptions', outlines: [ { text: 'Technology', type: 'folder', outlines: [ { text: 'Feed 1', type: 'rss', xmlUrl: '...' }, { text: 'Feed 2', type: 'rss', xmlUrl: '...' } ] } ] };

Provide Both xmlUrl and htmlUrl

{ text: 'Feed Name', type: 'rss', xmlUrl: 'https://example.com/feed.xml', // RSS feed URL htmlUrl: 'https://example.com' // Website URL }

Add Descriptions for Context

{ text: 'TechCrunch', type: 'rss', xmlUrl: 'https://techcrunch.com/feed/', htmlUrl: 'https://techcrunch.com', description: 'Technology news and analysis' }
Last updated on