-
-
Save lmorchard/f1f2508a9586d8e92efd84686c029f16 to your computer and use it in GitHub Desktop.
| // To use this script: | |
| // | |
| // 1. Copy this whole gist | |
| // 2. Log into your account on substack.com | |
| // 3. On a substack.com page, open the JavaScript console in your browser's web dev tools | |
| // 4. Paste this into the console and hit return. | |
| // 5. You should see substack-publications.opml has been downloaded. | |
| // | |
| // If you'd like to grab this code and improve it or turn it into a better tool, go right ahead! | |
| // Maybe drop me a toot at @lmorchard@hackers.town or @lmorchard@toot.lmorchard.com if you liked it. | |
| fetch("https://substack.com/api/v1/subscriptions") | |
| .then(res => res.json()) | |
| .then(subscriptionsData => { | |
| const feeds = subscriptionsData.publications.map(item => ({ | |
| text: item.name, | |
| type: "rss", | |
| htmlUrl: item.base_url, | |
| xmlUrl: item.base_url + "/feed" | |
| })); | |
| const nodes = feeds.map(feed => ` | |
| <outline type="rss" | |
| text="${feed.text}" | |
| htmlUrl="${feed.htmlUrl}" | |
| xmlUrl="${feed.xmlUrl}" /> | |
| `.trim()); | |
| const opml = ` | |
| <opml version='2.0'> | |
| <body> | |
| <outline text="Substack Publications" title="Substack Publications"> | |
| ${nodes.join("\n")} | |
| </outline> | |
| </body> | |
| </opml> | |
| `.trim(); | |
| const data = new Blob([opml], {type: "application/xml"}); | |
| const url = window.URL.createObjectURL(data); | |
| const a = document.createElement("a"); | |
| a.textContent = "DOWNLOAD SUBSTACK OPML EXPORT"; | |
| a.href = url; | |
| a.download = "substack-publications.opml"; | |
| document.body.insertBefore(a, document.body.firstChild); | |
| a.click(); | |
| }); |
Some may wish to add the
, { mode: 'no-cors' }
fetch option. Works very nicely. Thanks.
This was great, thank you :)
i tried to do this in august 2026 and got some errors, but this updated version worked:
(async () => {
const res = await fetch("/api/v1/subscriptions?tvOnly=false", {credentials: "include", headers: {accept: "application/json"}});
if (!res.ok) return console.error(res.status, await res.text());
const data = await res.json();
const pubs = new Map();
(function walk(n) {
if (!n || typeof n !== "object") return;
if (Array.isArray(n)) return n.forEach(walk);
const url = n.base_url || (n.subdomain ? https://${n.subdomain}.substack.com : null);
const name = n.name || n.title;
if (url && name) pubs.set(url.replace(//$/, ""), {name, url: url.replace(//$/, "")});
Object.values(n).forEach(walk);
})(data);
console.log(found ${pubs.size} publications);
if (!pubs.size) return console.log(data);
const esc = s => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
const nodes = [...pubs.values()].map(f =>
<outline type="rss" text="${esc(f.name)}" title="${esc(f.name)}" htmlUrl="${esc(f.url)}" xmlUrl="${esc(f.url)}/feed" />);
const opml = <?xml version="1.0" encoding="UTF-8"?>\n<opml version="2.0"><head><title>Substack Publications</title></head><body>\n<outline text="Substack Publications" title="Substack Publications">\n${nodes.join("\n")}\n</outline>\n</body></opml>;
const url = URL.createObjectURL(new Blob([opml], {type: "text/x-opml"}));
const a = Object.assign(document.createElement("a"), {href: url, download: "substack-publications.opml"});
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 5000);
})();
works well! Thanks!