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