Skip to content

OPML

OPML 2.0 is a subscription-list interchange format, used to move feed lists between readers. Neurowire can export a mesh or construct to OPML and import an OPML file into a mesh.

Not a feed format

OPML is not a feed serializer. It is not one of the serialize() formats (nwf, atom, rss, json, md) and it does not carry articles. It is lossy interchange: it lists which sources a mesh or construct subscribes to, so you can hand that list to a feed reader, or build a mesh from a reader's export.

DirectionFunctionPackage
Export a meshmeshToOpml(mesh)@neurowire/core (packages/core/src/opml.ts)
Export a constructconstructToOpml(construct)@neurowire/core
Import to a meshopmlToMesh(xml, name?)@neurowire/ingest (packages/ingest/src/opml.ts)

Export

ts
import { meshToOpml, constructToOpml } from '@neurowire/core'

const opml = meshToOpml(mesh)
const repoOpml = constructToOpml(construct)

meshToOpml

Serializes a mesh to a flat OPML 2.0 list: one <outline> per source. Each source outline carries type="rss", text and title set to the source name, and both xmlUrl and htmlUrl pointing at the source URL. The OPML head/title is the mesh name.

constructToOpml

Serializes a construct with two-level nesting: one category <outline> per mesh, each wrapping its source outlines. This mirrors the construct's per-mesh grouping. A reference member ({ ref }) carries no sources of its own, so it emits an empty category outline (named after the ref); a mesh with zero sources likewise emits an empty category outline.

Import

ts
import { opmlToMesh } from '@neurowire/ingest'

const mesh = opmlToMesh(xml, 'My Reader')

opmlToMesh parses an OPML 2.0 subscription list into a Mesh:

  • Every <outline> that has an xmlUrl becomes a source { name, url }. The name is the outline's text, else its title, else the URL's host (falling back to the raw URL).
  • Nested categories are walked recursively and flattened: a mesh has no per-source grouping, so two-level structure (as exported from a construct) collapses to one list. Outlines without an xmlUrl (bare categories) are skipped, but their children are still visited.
  • The mesh name is the name argument (trimmed), else the OPML head/title, else "imported".
  • The result is validated against the core Mesh schema (MeshSchema.parse), so a malformed result throws.

opmlToMesh throws on malformed XML or when the document has no <opml> root.

Sample OPML

A mesh exported with meshToOpml:

xml
<?xml version="1.0" encoding="utf-8"?>
<opml version="2.0">
  <head>
    <title>AI News</title>
  </head>
  <body>
    <outline type="rss" text="Anthropic" title="Anthropic" xmlUrl="https://www.anthropic.com/news" htmlUrl="https://www.anthropic.com/news"/>
    <outline type="rss" text="OpenAI" title="OpenAI" xmlUrl="https://openai.com/blog/rss.xml" htmlUrl="https://openai.com/blog/rss.xml"/>
  </body>
</opml>

A construct exported with constructToOpml (two levels):

xml
<?xml version="1.0" encoding="utf-8"?>
<opml version="2.0">
  <head>
    <title>Daily</title>
  </head>
  <body>
    <outline text="AI News" title="AI News">
      <outline type="rss" text="Anthropic" title="Anthropic" xmlUrl="https://www.anthropic.com/news" htmlUrl="https://www.anthropic.com/news"/>
    </outline>
    <outline text="Dev" title="Dev">
      <outline type="rss" text="Rust Blog" title="Rust Blog" xmlUrl="https://blog.rust-lang.org/feed.xml" htmlUrl="https://blog.rust-lang.org/feed.xml"/>
    </outline>
  </body>
</opml>

CLI

The opml subcommand group has export and import:

bash
# Export a mesh or construct to OPML 2.0 (stdout, or -o <file>)
neurowire opml export --mesh ai-news.json > ai-news.opml
neurowire opml export --construct daily.json -o daily.opml

# Import an OPML file or URL into a mesh JSON
neurowire opml import subscriptions.opml -o mesh.json --name "My Reader"

opml export needs --mesh <file> or --construct <file>. opml import takes a file path or http(s) URL; the mesh name comes from --name, else the OPML head/title, else "imported".

See the CLI guide and Meshes.