|
20 | 20 | // holds feeds with stable well-known URLs and defers everything else to the |
21 | 21 | // published lists, where the list is somebody else's to maintain. |
22 | 22 |
|
| 23 | +import fs from "node:fs"; |
| 24 | + |
23 | 25 | /** |
24 | 26 | * Bing News query feed — the only search feed left. |
25 | 27 | * |
@@ -86,6 +88,53 @@ export function isDeadEndLink(url) { |
86 | 88 | } |
87 | 89 | } |
88 | 90 |
|
| 91 | +/** The vendored copy of what profullstack.com/feeds.opml serves. */ |
| 92 | +const PROFULLSTACK_OPML = new URL("./profullstack-feeds.opml", import.meta.url); |
| 93 | + |
| 94 | +/** slugify() from news.mjs, kept in step by a test rather than imported. */ |
| 95 | +function slug(label) { |
| 96 | + return String(label ?? "") |
| 97 | + .toLowerCase() |
| 98 | + .replace(/['’]/g, "") |
| 99 | + .replace(/[^a-z0-9]+/g, "-") |
| 100 | + .replace(/^-+|-+$/g, "") |
| 101 | + .slice(0, 32); |
| 102 | +} |
| 103 | + |
| 104 | +/** |
| 105 | + * The profullstack blogs, read from the vendored OPML rather than typed out. |
| 106 | + * |
| 107 | + * Read synchronously and at module load because defaultFeeds() is synchronous — |
| 108 | + * a fresh install must not wait on a network call, or on a promise, to show |
| 109 | + * anything at all. The file ships with the package (`files` includes `src`), so |
| 110 | + * it is there in both the npm and the install.sh channel. |
| 111 | + * |
| 112 | + * Parsed here with a small matcher instead of news.mjs's parseOpml, because |
| 113 | + * news.mjs imports this module and taking the import back the other way makes a |
| 114 | + * cycle. The matcher can afford to be small: this is our own file, flat, and a |
| 115 | + * test asserts the two agree on every feed it contains. |
| 116 | + * |
| 117 | + * A missing or unreadable file degrades to no profullstack defaults rather than |
| 118 | + * throwing, which would take `/news` down entirely over a packaging mistake. |
| 119 | + */ |
| 120 | +function profullstackFeeds() { |
| 121 | + let xml; |
| 122 | + try { xml = fs.readFileSync(PROFULLSTACK_OPML, "utf8"); } |
| 123 | + catch { return []; } |
| 124 | + |
| 125 | + const feeds = []; |
| 126 | + const seen = new Set(); |
| 127 | + for (const tag of xml.match(/<outline\b[^>]*>/gi) ?? []) { |
| 128 | + const attr = (name) => (new RegExp(`\\b${name}="([^"]*)"`, "i").exec(tag) ?? [])[1] ?? ""; |
| 129 | + const url = attr("xmlUrl"); |
| 130 | + if (!/^https?:\/\//i.test(url) || seen.has(url)) continue; |
| 131 | + seen.add(url); |
| 132 | + const title = attr("title") || attr("text") || url; |
| 133 | + feeds.push({ name: slug(title), title, url, site: attr("htmlUrl"), category: "profullstack" }); |
| 134 | + } |
| 135 | + return feeds; |
| 136 | +} |
| 137 | + |
89 | 138 | /** |
90 | 139 | * The feeds a fresh install reads. |
91 | 140 | * |
@@ -120,25 +169,18 @@ export const DEFAULT_FEEDS = [ |
120 | 169 |
|
121 | 170 | { name: "npr-politics", title: "NPR — Politics", url: "https://feeds.npr.org/1014/rss.xml", site: "https://www.npr.org", category: "politics" }, |
122 | 171 |
|
123 | | - // The profullstack blogs, read out of the box. This is the one published list |
124 | | - // small enough to be a default: profullstack.com/feeds.opml is 14 feeds, where |
125 | | - // smallweb is 33,000 and could only ever be searched. Kept in step with that |
126 | | - // file by hand rather than fetched, because defaultFeeds() is synchronous and |
127 | | - // a fresh install must not wait on a network call to show anything at all. |
128 | | - { name: "bittorrented-blog", title: "BitTorrented Blog", url: "https://bittorrented.com/blog/rss.xml", site: "https://bittorrented.com/blog", category: "profullstack" }, |
129 | | - { name: "bl0ggers-blog", title: "bl0ggers Blog", url: "https://bl0ggers.com/blog/rss.xml", site: "https://bl0ggers.com/blog", category: "profullstack" }, |
130 | | - { name: "c0mpute-blog", title: "c0mpute Blog", url: "https://c0mpute.com/blog/rss.xml", site: "https://c0mpute.com/blog", category: "profullstack" }, |
131 | | - { name: "c0upons-blog", title: "c0upons Blog", url: "https://c0upons.com/blog/rss.xml", site: "https://c0upons.com/blog", category: "profullstack" }, |
132 | | - { name: "coinpay-blog", title: "CoinPay Blog", url: "https://coinpayportal.com/blog/rss.xml", site: "https://coinpayportal.com/blog", category: "profullstack" }, |
133 | | - { name: "crawlproof-blog", title: "CrawlProof Blog", url: "https://crawlproof.com/blog/rss.xml", site: "https://crawlproof.com/blog", category: "profullstack" }, |
134 | | - { name: "d0rz-blog", title: "d0rz Blog", url: "https://d0rz.com/blog/rss.xml", site: "https://d0rz.com/blog", category: "profullstack" }, |
135 | | - { name: "logicsrc-blog", title: "LogicSRC Blog", url: "https://logicsrc.com/blog/rss.xml", site: "https://logicsrc.com/blog", category: "profullstack" }, |
136 | | - { name: "pairux-blog", title: "PairUX Blog", url: "https://pairux.com/blog/rss.xml", site: "https://pairux.com/blog", category: "profullstack" }, |
137 | | - { name: "qryptchat-blog", title: "QryptChat Blog", url: "https://qrypt.chat/blog/rss.xml", site: "https://qrypt.chat/blog", category: "profullstack" }, |
138 | | - { name: "saasrow-blog", title: "SaaSRow Blog", url: "https://www.saasrow.com/blog/rss.xml", site: "https://www.saasrow.com/blog", category: "profullstack" }, |
139 | | - { name: "sh1pt-blog", title: "sh1pt Blog", url: "https://sh1pt.com/blog/rss.xml", site: "https://sh1pt.com/blog", category: "profullstack" }, |
140 | | - { name: "threatcrush-blog", title: "ThreatCrush Blog", url: "https://threatcrush.com/blog/rss.xml", site: "https://threatcrush.com/blog", category: "profullstack" }, |
141 | | - { name: "ugig-blog", title: "ugig Blog", url: "https://ugig.net/blog/rss.xml", site: "https://ugig.net/blog", category: "profullstack" }, |
| 172 | + // The profullstack blogs, read out of the box. Not typed out here: they are |
| 173 | + // parsed from src/profullstack-feeds.opml, which is a copy of what |
| 174 | + // profullstack.com/feeds.opml actually serves. Two hand-maintained lists of |
| 175 | + // the same fourteen blogs is one more than can be kept in step, and the copy |
| 176 | + // that would go stale is this one — nobody editing the published OPML has a |
| 177 | + // reason to think about moshcode. Refresh it with: |
| 178 | + // |
| 179 | + // curl -sL https://profullstack.com/feeds.opml -o src/profullstack-feeds.opml |
| 180 | + // |
| 181 | + // test/profullstack-feeds.test.mjs checks that against the live file when |
| 182 | + // MOSHCODE_CHECK_FEED_DRIFT=1 is set. |
| 183 | + ...profullstackFeeds(), |
142 | 184 | ]; |
143 | 185 |
|
144 | 186 | /** |
|
0 commit comments