Compare commits

..

48 commits

Author SHA1 Message Date
1244ace745 change style and timezone
All checks were successful
/ deploy (push) Successful in 11s
2026-02-25 17:03:49 +01:00
08573a0b59 output not public
All checks were successful
/ deploy (push) Successful in 11s
2026-02-25 16:29:54 +01:00
f80ae93ab6 update deploy
Some checks failed
/ deploy (push) Failing after 10s
2026-02-25 16:29:15 +01:00
b6e4f23389 update to a fork
Some checks failed
/ deploy (push) Failing after 8s
2026-02-25 16:27:11 +01:00
9065ee59f4 spaces
All checks were successful
/ deploy (push) Successful in 15s
2026-02-25 15:51:57 +01:00
be59c826bd comment
Some checks failed
/ deploy (push) Failing after 16s
2026-02-25 15:50:11 +01:00
d3806751c9 deploy good path ?
Some checks failed
/ deploy (push) Failing after 16s
2026-02-25 15:44:21 +01:00
d8b7053fb5 depolouych anges
All checks were successful
/ deploy (push) Successful in 15s
2026-02-25 15:41:02 +01:00
941c1abf5b workdir
All checks were successful
/ deploy (push) Successful in 15s
2026-02-25 15:25:40 +01:00
b83efcbfde rebuild
Some checks failed
/ deploy (push) Failing after 15s
2026-02-25 15:24:35 +01:00
9fdb414160 install rsync in deploy
Some checks failed
/ deploy (push) Failing after 15s
2026-02-25 15:17:11 +01:00
8c72cafc90 add my feeds
Some checks failed
/ deploy (push) Failing after 15s
2026-02-25 15:05:08 +01:00
George Mandis
d9857c501e
Removing Glitch (RIP) (#26) 2026-01-10 06:09:19 -05:00
kmfd
dbfe46a05d
Update README.md (#23) 2025-01-20 05:21:35 -05:00
George Mandis
434ada0298
Update issue templates 2024-02-11 13:51:35 -05:00
dependabot[bot]
ae12164e0b
Bump word-wrap from 1.2.3 to 1.2.4 (#17) 2023-10-12 23:04:33 -04:00
George Mandis
e81ed2355a
2.0.2: Bug fixes and package updates (#16) 2023-06-03 15:33:26 -04:00
George Mandis
0fa785b41d
Update README.md 2022-12-04 20:16:50 -05:00
George Mandis
0fbe6c16ad
Updates for package, node and README (#12) 2022-12-04 17:49:50 -05:00
George Mandis
bdb8bf8ef4
Updated demo/default feeds list (#11) 2022-12-04 17:37:31 -05:00
George Mandis
adb1227b95
Update README.md 2022-12-04 17:33:17 -05:00
dependabot[bot]
e9f570ab07
Bump node-fetch from 3.1.0 to 3.1.1 (#7)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-31 11:48:44 -07:00
Antoni
7e9d187d37
Fixed typo in README.md (#9) 2022-08-31 11:48:32 -07:00
Marko Vujanic
58fa8d0d47
Fix typo de;ay -> delay (#8) 2022-07-06 08:25:02 -04:00
George Mandis
bbab21c0ca Version bumpb, license update, added info 2021-12-05 13:35:24 -08:00
George Mandis
11a4042bdf Updating default template adding Bubo version 2021-12-05 13:35:10 -08:00
George Mandis
16a2525518 Adding title fallback + Bubo version to output + formatting 2021-12-05 13:34:59 -08:00
George Mandis
4afe47bf4f Adding feed to error message for more context 2021-12-04 22:57:46 -08:00
George Mandis
d115ffb490
Update README.md
Added Netlify badge
2021-12-05 01:47:07 -05:00
George Mandis
da784fc6e8
Update README.md 2021-11-29 04:19:19 -05:00
George Mandis
e2fc5e7a2b Updated netlify toml file 2021-11-29 00:59:06 -08:00
George Mandis
2ded57cc44 Updated template 2021-11-29 00:54:44 -08:00
George Mandis
c9e98d79b6
Introducing Bubo 2.0.0 (#6)
Converting to TypeScript!
2021-11-29 03:46:32 -05:00
George Mandis
78250bd9a2
Update feeds.json 2021-11-26 01:10:36 -05:00
George Mandis
5c0a4a2523
Update README.md
Typo fixes
2021-11-17 21:07:19 -08:00
George Mandis
0456e0ef0e Fixed feed branch 2021-11-14 20:03:25 -08:00
George Mandis
4a3bb0b1b7 adding nvmrc 2021-11-14 20:01:27 -08:00
George Mandis
b451adf35b Updated default feeds 2021-11-14 19:57:10 -08:00
George Mandis
5c84d7402e Added missing const. How did I not ever catch this? 2021-11-14 19:51:42 -08:00
George Mandis
c4aa99d086 Fixed merge conflict in README 2021-11-14 19:32:03 -08:00
George Mandis
38bfbbdc75 Fixed JSON feed parsing issues + bumped ot v1.0.1 2021-11-14 19:29:41 -08:00
George Mandis
c94e07f727
Update README.md
typo fixes
2021-09-17 15:57:44 -07:00
George Mandis
cc489f3e86
Update README.md
Added showcase section
2021-09-16 15:59:42 -07:00
George Mandis
e7ea24d487
Merge pull request #5 from georgemandis/add-license-1
Create LICENSE
2021-09-16 10:51:50 -07:00
George Mandis
70f275ac3f
Create LICENSE 2021-09-16 10:50:26 -07:00
George Mandis
967b941862
Update FUNDING.yml 2021-05-09 15:20:35 -04:00
George Mandis
09cc28e69c
Update README.md 2021-05-09 14:56:56 -04:00
George Mandis
2aafb532f8 updating package-lock 2021-05-09 11:51:08 -07:00
16 changed files with 715 additions and 2266 deletions

View file

@ -0,0 +1,32 @@
on:
push:
branches:
- main
schedule:
- cron: '0 * * * *' # every hour
jobs:
deploy:
runs-on: ubuntu-latest
container:
volumes:
- /var/www/bubo:/var/www/bubo
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm install
- name: Install rsync
run: apt-get update && apt-get install -y rsync
- name: Build Bubo
run: npm run build
- name: List built files
run: ls -la ./output/
- name: Deploy to web root
run: |
rsync -avz --delete --exclude='.git' --exclude='.forgejo' \
./output/ /var/www/bubo/
chmod -R 755 /var/www/bubo

12
.github/FUNDING.yml vendored
View file

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: georgemandis
patreon: georgemandis
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://george.mand.is/sponsor

View file

@ -1,36 +0,0 @@
name: Publish with GitHub Actions
# Uncommen the "on" block below to use
on:
push:
branches:
- master
schedule:
# Run this script every 15 minutes
- cron: '*/15 * * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout master branch
uses: actions/checkout@v2
with:
ref: master
- name: Setup Node.js and install dependencies
uses: actions/setup-node@v1
with:
node-version: 11.4.0
- run: npm install
- name: Run build and parse RSS feeds
- run: npm run build --if-present
- name: Commit latest build to ./output folder
uses: github-actions-x/commit@v2.3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
push-branch: 'master'
commit-message: 'Published latest changes to RSS feeds'
force-add: 'true'
files: output/*
name: GitHub Action Bubo Bot
email: action@github.com

5
.gitignore vendored
View file

@ -1 +1,4 @@
node_modules/* node_modules/*
output/index.html
cache.json
data.json

1
.node-version Normal file
View file

@ -0,0 +1 @@
v18.15.0

View file

@ -1,73 +1,37 @@
# 🦉 Bubo Reader # 🦉 Bubo Reader (Fork)
Bubo Reader is a somewhat irrationally minimalist <acronym title="Really Simple Syndication">RSS</acronym> and <acronym title="JavaScript Object Notation">JSON</acronym> feed reader you can deploy on [Netlify](https://netlify.com) in a few steps or [Glitch](https://glitch.com) in even fewer steps! The goal of the project is to generate a webpage that shows a list of links from a collection of feeds organized by category and website. That's it. ![screenshot](./demo.png)
It is named after this [silly robot owl](https://www.youtube.com/watch?v=MYSeCfo9-NI) from Clash of the Titans (1981). [Demo Site](https://kevinfiol.com/rss-reader/)
You can read more about how this project came about in my blog post '[Introducing Bubo RSS: An Absurdly Minimalist RSS Feed Reader](https://george.mand.is/2019/11/introducing-bubo-rss-an-absurdly-minimalist-rss-feed-reader/)' This is a personal fork of the excellent [Bubo Reader](https://github.com/georgemandis/bubo-rss) by George Mandis. I've made several opinionated changes to the setup, including replacing dependencies with more compact alternatives. Please see below for deployment instructions.
## Getting Started Original blogpost: [Introducing Bubo RSS: An Absurdly Minimalist RSS Feed Reader](https://george.mand.is/2019/11/introducing-bubo-rss-an-absurdly-minimalist-rss-feed-reader/)
How to deploy Bubo Reader in a few easy steps with Netlify or Glitch: Blogpost about my fork: [A minimal RSS Feed Reader](https://kevinfiol.com/blog/a-minimal-rss-feed-reader/)
### Deploying to Glitch Some changes I made:
The quickest way is to remix the project on Glitch: * Replace `nunjucks` with template strings (less dependencies for shorter build times)
[https://glitch.com/edit/#!/bubo-rss](https://glitch.com/edit/#!/bubo-rss) * Replace `node-fetch` with Node's native `fetch`
* Many styling changes, including using the `:target` CSS selector to switch between groups (inspired by https://john-doe.neocities.org/)
* The build script now sorts the feeds in each group by which one has the latest updates (this greatly improves the experience, imo)
* An "All Articles" view
* Privacy-redirect support via config file
* Dark mode via `@media (prefers-color-scheme: dark)`
Just changed some feeds in `./src/feeds.json` file and you're set! If you'd like to modify the style or the template you can changed `./output/style.css` file or the `./src/template.html` file respectively. ## How to build
There is also a special `glitch` branch you can clone if you prefer: Node `>=18.x` required.
[https://github.com/georgemandis/bubo-rss/tree/glitch](https://github.com/georgemandis/bubo-rss/tree/glitch)
The only difference between this branch and `master` is that it spins up a server using [Express](https://expressjs.com/) to serve your `./output/index.html` file on Glitch. Everything else is the same. ```shell
npm install
npm run build
```
### Deploying to Netlify ## How to host on Github Pages
- [Fork the repository](https://github.com/georgemandis/bubo-rss/fork) 1. Fork this repo!
- From your forked repository go to and edcit `src/feeds.json` to manage your feeds and categories 2. Enable [Github Pages](https://pages.github.com/) for your repo (either as a project site, or user site)
- [Create a new site](https://app.netlify.com/start) on Netlify from GitHub 3. Configure `.github/workflows/build.yml` to your liking
* Uncomment the `schedule` section to enable scheduled builds
The deploy settings should automatically import from the `netlify.toml` file. All you'll need to do is confirm and you're ready to go!
### Keeping Feeds Updated
#### Using Netlify Webhooks
To keep your feeds up to date you'll want to [setup a Build Hook](https://www.netlify.com/docs/webhooks/#incoming-webhooks) for your Netlify site and use another service to ping it every so often to trigger a rebuild. I'd suggest looking into:
- [IFTTT](https://ifttt.com/)
- [Zapier](https://zapier.com/)
- [EasyCron](https://www.easycron.com/)
If you already have a server running Linux and some command-line experience it might be simpler to setup a [cron job](https://en.wikipedia.org/wiki/Cron).
#### Using GitHub Actions
This approach is a little different and requires some modifications to the repository. Netlify started billing for [build minutes](https://www.netlify.com/pricing/faq/) very shortly after I published this project. Running `npm build` and downloading all of the RSS feeds took up a substantial number of this minutes, particulary if you had some kind of process pinging the webhook and trigger a build every 15 minutes or so.
How is the The GitHub Action-based approach different? The same build process runs, but this time it's on GitHub's servers via the Action. It then **commits** the newly created file generated at `./output/index.html` back into the repository. Netlify still gets pinged when the repository is updated, but skips the `npm run build` step on their end, which significantly reduces the number of build minutes required.
**Short Answer**: use the [`github-action-publishing`](https://github.com/georgemandis/bubo-rss/tree/github-action-publishing) branch for now if you'd prefer to use GitHub Actions to run your builds.
The GitHub Action is setup to build and commit directly to the `master` branch, which is not the best practice. I'd suggest creating a separate branch to checkout and commit changes to in the Action. You could then specify that same branch as the one to checkout and publish on Netlify.
## Anatomy of Bubo Reader
- `src/index.html` - a [Nunjucks](https://mozilla.github.io/nunjucks/) template that lets you change how the feeds are displayed
- `output/style.css` - a CSS file to stylize your feed output
- `src/feeds.json` - a JSON file containing the URLs for various site's feeds separated into categories
- `src/index.js` - the script that loads the feeds and does the actual parsinga and rendering
## Demos
You can view live demos here:
- [https://bubo-rss-demo.netlify.com/](https://bubo-rss-demo.netlify.com/)
- [http://bubo-rss.glitch.me/](http://bubo-rss.glitch.me/)
Not the most exciting-looking demos, I'll admit, but they work!
## Support
If you found this useful please consider sponsoring me or this project. If you'd rather run this on your own server please consider using one of these affiliate links to setup a micro instance on [Linode](https://www.linode.com/?r=8729957ab02b50a695dcea12a5ca55570979d8b9), [Digital Ocean](https://m.do.co/c/31f58d367777) or [Vultr](https://www.vultr.com/?ref=8403978).

View file

@ -1,3 +0,0 @@
[build]
publish = "output/"

View file

@ -1,30 +1,276 @@
:root {
--font-size: 14px;
--color: #111;
--bg-color: #fffff8;
--faded-bg: #f9f9f2;
--title-font-size: 16px;
--title-font-weight: 600;
--main-padding-right: 8rem;
}
@media (prefers-color-scheme: dark) {
:root {
--color: #ddd;
--bg-color: #151515;
--faded-bg: #1b1b1b;
}
}
@media screen and (max-width: 900px) {
:root {
--main-padding-right: 0;
}
article.item {
margin-right: 0 !important;
margin-left: 0 !important;
}
.menu-label,
.menu-btn {
display: block !important;
position: absolute;
top: 0;
right: 0;
z-index: 99;
}
.menu-btn {
display: none !important;
}
.menu-label {
padding: 1rem 2rem;
background-color: var(--faded-bg);
}
.menu-label::after {
content: 'groups';
}
.menu-btn:checked ~ .sidebar {
display: block !important;
}
.menu-btn:checked ~ main {
display: none !important;
}
.menu-btn:checked ~ .menu-label::after {
content: 'back';
}
.sidebar {
display: none !important;
padding: 1rem;
position: absolute;
top: 0;
left: 5px;
height: 100%;
background-color: var(--bg-color);
}
.sidebar
> header
> .group-selector {
list-style: none;
padding: 0;
}
.sidebar
> header
> .group-selector
> li {
font-size: 1.2em;
}
}
@keyframes details-show {
from {
opacity:0;
transform: var(--details-translate, translateY(-0.5em));
}
}
body { body {
font-family:system-ui; color: var(--color);
font-size: 18px; background-color: var(--bg-color);
margin: 0;
padding: 0;
font-family: serif;
font-size: var(--font-size);
overflow: hidden;
}
details[open] > *:not(summary),
section {
animation: details-show 100ms ease-in-out;
}
h1, h2, h3 {
font-family: monospace;
}
a:link {
color: inherit;
}
a:visited {
color: #b58c8c;
}
a:hover {
opacity: .75;
}
summary {
position: sticky;
top: 0;
padding-top: 0.65rem;
padding-bottom: 0.65rem;
user-select: none;
cursor:pointer;
font-family: monospace;
background-color: var(--bg-color);
}
summary:hover span,
summary:hover div {
opacity:.75;
}
.menu-btn,
.menu-label {
display: none;
}
.group-selector a,
.group-selector a:visited {
color: inherit;
font-family: monospace;
line-height: 1.5em;
}
.feed-title {
font-weight: var(--title-font-weight);
font-size: var(--title-font-size);
}
.feed-url, .feed-timestamp {
color:#aaa;
}
.feed-url {
/**/
}
.feed-timestamp {
margin-left: 1.45rem;
}
.monospace {
font-family: monospace;
}
.inline {
display: inline;
}
.app {
display: flex;
gap: 2rem;
padding: 0 0rem 1rem 1rem;
}
.sidebar {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
header {
padding-top: 1rem;
}
main {
flex: 5;
height: 100vh;
overflow-y: auto;
padding-right: var(--main-padding-right);
}
article.item {
max-width: 85%;
padding: 0.15rem 0.75rem;
margin-left: 1.5rem;
margin-right: 1.5rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
background-color: var(--faded-bg);
}
#all-articles > article.item {
margin-left: 0;
}
article.item header.item__header {
font-size: var(--title-font-size);
}
.item__feed-url {
opacity: 0.25;
}
ul.article-links {
list-style: none;
padding-left: 0;
}
ul.article-links > li {
display: inline-block;
margin-right: .5rem;
}
footer {
opacity: 0.25;
font-size: 0.75em;
}
footer:hover {
opacity: 1;
}
section {
z-index: 1;
/* ! Everything below is needed ! */
display: none;
height: 100%;
width: 100%;
background-color: var(--bg-color);
}
section > h2 {
margin-top: 0;
padding-top: 19px;
}
section:target { /* Show section */
display: block;
}
section:target ~ .default-text {
display: none;
} }
.default-text {
details:focus, text-align: center;
details:focus-within, position: relative;
details:hover { top: 5%;
/* background:#ffeb3b; */ font-family: monospace;
/* outline:2px #000 solid; */ font-size: 2em;
} }
details ul li {
}
summary {
cursor:pointer;
}
summary:hover {
opacity:.75;
}
.feed-url {
color:#aaa;
}

2002
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,19 @@
{ {
"name": "bubo-reader", "name": "reader",
"version": "1.0.0", "version": "1.0.0",
"description": "A somewhat dumb but effective feed reader (RSS, JSON & Twitter)", "description": "A somewhat dumb but effective feed reader (RSS, JSON & Twitter)",
"main": "src/index.js", "type": "module",
"scripts": { "engines": {
"build": "node src/index.js > output/index.html", "node": ">=18.x"
"test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "scripts": {
"build": "node src/build.js",
"write": "node src/build.js --write",
"cached": "node src/build.js --cached"
},
"author": "kevinfiol",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"node-fetch": "^2.6.1",
"nunjucks": "^3.2.0",
"rss-parser": "^3.6.3" "rss-parser": "^3.6.3"
} }
} }

208
src/build.js Normal file
View file

@ -0,0 +1,208 @@
/**
* 🦉 Bubo RSS Reader
* ====
* Dead, dead simple feed reader that renders an HTML
* page with links to content from feeds organized by site
*
*/
import Parser from 'rss-parser';
import { resolve } from 'node:path';
import { readFileSync, writeFileSync } from 'node:fs';
import { template } from './template.js';
const WRITE = process.argv.includes('--write');
const USE_CACHE = !WRITE && process.argv.includes('--cached');
const CACHE_PATH = './src/cache.json';
const OUTFILE_PATH = './output/index.html';
const CONTENT_TYPES = [
'application/json',
'application/atom+xml',
'application/rss+xml',
'application/xml',
'application/octet-stream',
'text/xml'
];
const config = readCfg('./src/config.json');
const feeds = USE_CACHE ? {} : readCfg('./src/feeds.json');
const cache = USE_CACHE ? readCfg(CACHE_PATH) : {};
await build({ config, feeds, cache, writeCache: WRITE });
async function build({ config, feeds, cache, writeCache = false }) {
let allItems = cache.allItems || [];
const parser = new Parser();
const errors = [];
const groupContents = {};
for (const groupName in feeds) {
groupContents[groupName] = [];
const results = await Promise.allSettled(
Object.values(feeds[groupName]).map(url =>
fetch(url, { method: 'GET' })
.then(res => [url, res])
.catch(e => {
throw [url, e];
})
)
);
for (const result of results) {
if (result.status === 'rejected') {
const [url, error] = result.reason;
errors.push(url);
console.error(`Error fetching ${url}:\n`, error);
continue;
}
const [url, response] = result.value;
try {
// e.g., `application/xml; charset=utf-8` -> `application/xml`
const contentType = response.headers.get('content-type').split(';')[0];
if (!CONTENT_TYPES.includes(contentType))
throw Error(`Feed at ${url} has invalid content-type.`)
const body = await response.text();
const contents = typeof body === 'string'
? await parser.parseString(body)
: body;
const isRedditRSS = contents.feedUrl && contents.feedUrl.includes("reddit.com/r/");
if (!contents.items.length === 0)
throw Error(`Feed at ${url} contains no items.`)
contents.feed = url;
contents.title = contents.title || contents.link;
groupContents[groupName].push(contents);
// item sort & normalization
contents.items.sort(byDateSort);
contents.items.forEach((item) => {
// 1. try to normalize date attribute naming
const dateAttr = item.pubDate || item.isoDate || item.date || item.published;
item.timestamp = new Date(dateAttr).toLocaleDateString();
// 2. correct link url if it lacks the hostname
if (item.link && item.link.split('http').length === 1) {
item.link =
// if the hostname ends with a /, and the item link begins with a /
contents.link.slice(-1) === '/' && item.link.slice(0, 1) === '/'
? contents.link + item.link.slice(1)
: contents.link + item.link;
}
// 3. parse subreddit feed comments
if (isRedditRSS && item.contentSnippet && item.contentSnippet.startsWith('submitted by ')) {
// matches anything between double quotes, like `<a href="matches this">foo</a>`
const quotesContentMatch = /(?<=")(?:\\.|[^"\\])*(?=")/g;
let [_submittedBy, _userLink, contentLink, commentsLink] = item.content.split('<a href=');
item.link = contentLink.match(quotesContentMatch)[0];
item.comments = commentsLink.match(quotesContentMatch)[0];
}
// 4. redirects
if (config.redirects) {
// need to parse hostname methodically due to unreliable feeds
const url = new URL(item.link);
const tokens = url.hostname.split('.');
const host = tokens[tokens.length - 2];
const redirect = config.redirects[host];
if (redirect) item.link = `https://${redirect}${url.pathname}${url.search}`;
}
// 5. escape html in titles
item.title = escapeHtml(item.title);
});
// add to allItems
allItems = [...allItems, ...contents.items];
} catch (e) {
console.error(e);
errors.push(url)
}
}
}
const groups = cache.groups || Object.entries(groupContents);
if (writeCache) {
writeFileSync(
resolve(CACHE_PATH),
JSON.stringify({ groups, allItems }),
'utf8'
);
}
// for each group, sort the feeds
// sort the feeds by comparing the isoDate of the first items of each feed
groups.forEach(([_groupName, feeds]) => {
feeds.sort((a, b) => byDateSort(a.items[0], b.items[0]));
});
// sort `all articles` view
allItems.sort((a, b) => byDateSort(a, b));
const now = getNowDate(config.timezone_offset).toString();
const html = template({ allItems, groups, now, errors });
writeFileSync(resolve(OUTFILE_PATH), html, { encoding: 'utf8' });
console.log(`Reader built successfully at: ${OUTFILE_PATH}`);
}
/**
* utils
*/
function parseDate(item) {
let date = item
? (item.isoDate || item.pubDate)
: undefined;
return date ? new Date(date) : undefined;
}
function byDateSort(dateStrA, dateStrB) {
const [aDate, bDate] = [parseDate(dateStrA), parseDate(dateStrB)];
if (!aDate || !bDate) return 0;
return bDate - aDate;
}
function getNowDate(offset = 0) {
let d = new Date();
const utc = d.getTime() + (d.getTimezoneOffset() * 60000);
d = new Date(utc + (3600000 * offset));
return d;
}
function readCfg(path) {
let contents, json;
try {
contents = readFileSync(resolve(path), { encoding: 'utf8' });
} catch (e) {
console.warn(`Warning: Config at ${path} does not exist`);
return {};
}
try {
json = JSON.parse(contents);
} catch (e) {
console.error('Error: Config is Invalid JSON: ' + path);
process.exit(1);
}
return json;
}
function escapeHtml(html) {
return html.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('\'', '&apos;')
.replaceAll('"', '&quot;');
}

6
src/config.json Normal file
View file

@ -0,0 +1,6 @@
{
"timezone_offset": 1.0,
"redirects": {
"medium": "scribe.rip"
}
}

View file

@ -1,24 +1,13 @@
{ {
"Web Development": [ "feeds": [
"https://hacks.mozilla.org/feed/", "https://la-poterie-des-chemins-creux.fr/feed/"
"https://blog.mozilla.org/feed/",
"https://web.dev/feed.xml",
"https://v8.dev/blog.atom",
"https://alistapart.com/main/feed/",
"https://css-tricks.com/feed/",
"https://dev.to/feed"
], ],
"Blogs": [ "blogs": [
"https://drewdevault.com/blog/index.xml",
"https://maia.crimew.gay/feed.xml",
"https://george.mand.is/feed.xml", "https://george.mand.is/feed.xml",
"https://joy.recurse.com/feed.atom" "https://invisibleup.com/atom.xml",
], "https://www.wheresyoured.at/rss/",
"My GitHub Projects": [ "https://solar.lowtechmagazine.com/fr/posts/index.xml"
"https://github.com/georgemandis.atom",
"https://github.com/snaptortoise/konami-js/releases.atom",
"https://github.com/snaptortoise/konami-js/commits/master.atom",
"https://github.com/javascriptforartists/cheer-me-up-and-sing-me-a-song/commits/master.atom",
"https://github.com/georgemandis/circuit-playground-midi-multi-tool/commits/master.atom",
"https://github.com/georgemandis/remote-working-list/commits/master.atom",
"https://github.com/georgemandis/tweeter-totter/commits/master.atom"
] ]
} }

View file

@ -1,89 +0,0 @@
/**
* 🦉 Bubo RSS Reader
* ====
* Dead, dead simple feed reader that renders an HTML
* page with links to content from feeds organized by site
*
*/
const fetch = require("node-fetch");
const Parser = require("rss-parser");
const parser = new Parser();
const nunjucks = require("nunjucks");
const env = nunjucks.configure({ autoescape: true });
const feeds = require("./feeds.json");
env.addFilter("formatDate", function(dateString) {
const formattedDate = new Date(dateString).toLocaleDateString()
return formattedDate !== 'Invalid Date' ? formattedDate : dateString;
});
env.addGlobal('now', (new Date()).toUTCString() );
// parse XML or JSON feeds
function parseFeed(response) {
const contentType = response.headers.get("content-type")
? response.headers.get("content-type").split(";")[0]
: false;
const rssFeed = [contentType]
.map(item =>
[
"application/atom+xml",
"application/rss+xml",
"application/xml",
"text/xml",
"text/html" // this is kind of a gamble
].includes(item)
? response.text()
: false
)
.filter(_ => _)[0];
const jsonFeed = [contentType]
.map(item =>
["application/json"].includes(item) ? response.json() : false
)
.filter(_ => _)[0];
return rssFeed || jsonFeed || false;
}
(async () => {
const contentFromAllFeeds = {};
const errors = [];
for (group in feeds) {
contentFromAllFeeds[group] = [];
for (let index = 0; index < feeds[group].length; index++) {
try {
const response = await fetch(feeds[group][index]);
const body = await parseFeed(response);
const contents =
typeof body === "string" ? await parser.parseString(body) : body;
contents.feed = feeds[group][index];
contents.title = contents.title ? contents.title : contents.link;
contentFromAllFeeds[group].push(contents);
// try to normalize date attribute naming
contents.items.forEach(item => {
const timestamp = new Date(item.pubDate || item.isoDate || item.date).getTime();
item.timestamp = isNaN(timestamp) ? (item.pubDate || item.isoDate || item.date) : timestamp;
});
} catch (error) {
errors.push(feeds[group][index]);
}
}
}
const output = env.render("./src/template.html", {
data: contentFromAllFeeds,
errors: errors
});
console.log(output);
})();

View file

@ -1,48 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>🦉 Bubo Reader</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>🦉 Bubo Reader</h1>
{% for group, feeds in data %}
<h2>{{ group }}</h2>
{% for feed in feeds %}
<details>
<summary>
<span class="feed-title">{{ feed.title }}</span>
<span class="feed-url">({{ feed.feed }})</span>
</summary>
<ul>
{% for item in feed.items %}
<li>
{{ item.timestamp | formatDate }} - <a href="{{ item.link }}" target='_blank' rel='noopener norefferer nofollow'>{{ item.title }}</a>
</li>
{% endfor %}
</ul>
</details>
{% endfor %}
{% endfor %}
{% if errors | length > 0 %}
<h2>Errors</h2>
<p>There were errors trying to parse these feeds:</p>
<ul>
{% for error in errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
<hr>
<p>
Last updated {{ now }}. Powered by <a href="https://github.com/georgemandis/bubo-rss">Bubo Reader</a>, a project by <a href="https://george.mand.is">George Mandis</a>
</p>
</body>
</html>

110
src/template.js Normal file
View file

@ -0,0 +1,110 @@
const forEach = (arr, fn) => {
let str = '';
arr.forEach(i => str += fn(i) || '');
return str;
};
const article = (item) => `
<article class="item">
<header class="item__header">
<a href="${item.link}" target='_blank' rel='noopener norefferer nofollow'>
${item.title}
</a>
</header>
<small>
${item.feedUrl ? `<span class="item__feed-url monospace">${item.feedUrl}</span>` : ''}
${item.feedTitle ? `<span class="item__feed-title">${item.feedTitle}</span>` : ''}
<ul class="article-links">
<li class="monospace">${item.timestamp || ''}</li>
${item.comments ? `
<li><a href="${item.comments}" target='_blank' rel='noopener norefferer nofollow'>comments</a></li>
` : ''
}
</ul>
</small>
</article>
`;
export const template = ({ allItems, groups, errors, now }) => (`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>rss reader</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="app">
<input type="checkbox" class="menu-btn" id="menu-btn" />
<label class="menu-label" for="menu-btn"></label>
<div class="sidebar">
<header>
<h1 class="inline" style="user-select: none;">rss</h1>
<ul class="group-selector">
<li><a href="#all-articles">all articles</a></li>
${forEach(groups, group => `
<li><a href="#${group[0]}">${group[0]}</a></li>
`)}
</ul>
</header>
<footer>
${errors.length > 0 ? `
<h2>Errors</h2>
<p>There were errors trying to parse these feeds:</p>
<ul>
${forEach(errors, error => `
<li>${error}</li>
`)}
</ul>
` : ''
}
<p>
Last updated ${now}. Powered by <a href="https://github.com/kevinfiol/rss-reader">Bubo Reader</a>, a project by <a href="https://george.mand.is">George Mandis</a> and <a href="https://kevinfiol.com">Kevin Fiol</a>.
</p>
</footer>
</div>
<main>
<section id="all-articles">
<h2>all articles</h2>
${forEach(allItems, item => article(item))}
</section>
${forEach(groups, ([groupName, feeds]) => `
<section id="${groupName}">
<h2>${groupName}</h2>
${forEach(feeds, feed => `
<details>
<summary>
<span class="feed-title">${feed.title}</span>
<span class="feed-url">
<small>
(${feed.feedUrl})
</small>
</span>
<div class="feed-timestamp">
<small>Latest: ${feed.items[0] && feed.items[0].timestamp || ''}</small>
</div>
</summary>
${forEach(feed.items, item => article(item))}
</details>
`)}
</section>
`)}
<div class="default-text">
<p>welcome to bubo rss reader</p>
<p>select a feed group to get started</p>
</div>
</main>
</div>
</body>
</html>
`);