Created
June 6, 2026 15:40
-
-
Save zachcp/2a77fa9e0742edfae14c80ce9af7a803 to your computer and use it in GitHub Desktop.
Substack --> OPML
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Substack → OPML exporter | |
| // Usage: navigate to https://substack.com/@zachcp/reads, scroll to bottom to | |
| // load all subscriptions, then paste into browser console. | |
| // Downloads substack_subscriptions.opml to your Downloads folder. | |
| (async () => { | |
| // Collect all unique publication URLs from the page. | |
| // Each subscription card links to the publication root (e.g. https://foo.substack.com | |
| // or https://foo.com for custom domains). | |
| const anchors = [...document.querySelectorAll("a[href]")]; | |
| const seen = new Set(); | |
| const feeds = []; | |
| for (const a of anchors) { | |
| const href = a.href; | |
| // Keep only substack subdomain or plausible custom-domain publication links. | |
| // Filter out nav, profile, settings, and archive links. | |
| if ( | |
| !href || | |
| href.includes("/p/") || // individual posts | |
| href.includes("/profile/") || | |
| href.includes("/settings") || | |
| href.includes("/about") || | |
| href.includes("substack.com/@") || // profile pages | |
| href.includes("substack.com/s/") || | |
| href.endsWith("/reads") || | |
| href.endsWith("/notes") || | |
| seen.has(href) | |
| ) continue; | |
| // Accept *.substack.com roots or any URL that is a direct child of its host | |
| // (i.e. the card link to the publication homepage) | |
| const url = new URL(href); | |
| if (url.pathname !== "/" && url.pathname !== "") continue; | |
| seen.add(href); | |
| // Grab the nearest text as title — look for a heading inside the card | |
| const card = a.closest("div, article, li") ?? a; | |
| const heading = card.querySelector("h1, h2, h3, h4, strong, [class*='title'], [class*='name']"); | |
| const title = (heading?.textContent ?? a.textContent ?? url.hostname) | |
| .trim() | |
| .replace(/\s*by(?=\s|\p{Lu}|\d).*/iu, "") // strip "by AuthorName..." (with or without space) | |
| .replace(/Subscribed?$/i, "") // strip trailing "Subscribe" / "Subscribed" | |
| .trim() | |
| .replace(/&/g, "&") | |
| .replace(/"/g, """) | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">"); | |
| const base = href.replace(/\/$/, ""); | |
| feeds.push({ title: title || url.hostname, feedUrl: `${base}/feed`, htmlUrl: base }); | |
| } | |
| if (!feeds.length) { | |
| console.error("No feeds found. Make sure you're on https://substack.com/@zachcp/reads and have scrolled to load all subscriptions."); | |
| return; | |
| } | |
| const outlines = feeds.map( | |
| ({ title, feedUrl, htmlUrl }) => | |
| ` <outline type="rss" text="${title}" title="${title}" xmlUrl="${feedUrl}" htmlUrl="${htmlUrl}"/>` | |
| ); | |
| const opml = `<?xml version="1.0" encoding="UTF-8"?> | |
| <opml version="1.0"> | |
| <head> | |
| <title>Substack Subscriptions</title> | |
| <dateCreated>${new Date().toUTCString()}</dateCreated> | |
| </head> | |
| <body> | |
| <outline text="Substack" title="Substack"> | |
| ${outlines.join("\n")} | |
| </outline> | |
| </body> | |
| </opml>`; | |
| const blob = new Blob([opml], { type: "text/x-opml" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = "substack_subscriptions.opml"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| console.log(`Downloaded OPML with ${feeds.length} feeds.`); | |
| console.table(feeds); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment