Initial release

This commit is contained in:
George Mandis 2019-11-27 18:17:31 -08:00
parent 8b654f3931
commit 62dd2b559c
9 changed files with 2066 additions and 192 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules/* node_modules/*
output/index.html

58
README.md Normal file
View File

@ -0,0 +1,58 @@
# 🦉 Bubo Reader
Bubo is a somewhat irrationally minimalist <acronym title="Really Simple Syndication">RSS</acronym> feed reader you can deploy on Netlify in a few simple steps. It is named after this [silly robot owl](https://www.youtube.com/watch?v=MYSeCfo9-NI) from Clash of the Titans (1981).
I created this one weekend after nostalgically lamenting the [demise of Google Reader](https://killedbygoogle.com/) many years ago. Many RSS feed reader services have sprouted up since then but they all do more than I need. I wanted something that:
- Had an absurdly simple interface that relied almost entirely on default HTML element behaviors and functionality
- Could be themed with CSS or mildly extended using JavaScript, if I wanted (but I decided not to)
- Didn't worry about pulling in the feed content into the reader's interface. I'm happy to read most content on the site it originated from. I just wanted a single dashboard to see when new stuff is published and available.
- Didn't rely on a database to see what I've read or keep an archive of content over time.
## What does "irrationally minimalist" mean?
Many RSS readers—including the former Google Reader—would pull the contents of a post into your feed so you could read everything in one place. Although I completely understand why someone would want to do that, I decided even that introduced too much complexity for my liking.
My goal with Bubo was to be able to see a list of the most recent posts from websites I like in one place with links to read them if I want. That's it. If I want to read something, I'll click through and read it on the publisher's site. If I want to keep track of what I've clicked on and read I can reflect that using the `a:visited` pseudo selector in my CSS.
Bubo does not store posts in a database or keep track of what I've read. If an item is no longer available in the site's feed then it no longer appears in Bubo. If I miss something, that's just life. I can live with that.
## What about authentication?
There is no authenticaton required for Bubo. Netlify does offer Basic Authentication under their [Pro plan](https://www.netlify.com/pricing/), which would be an easy solution to implement. You could probably also utilize their [Identity](https://www.netlify.com/docs/identity/?_ga=2.147267447.1334380953.1567004741-1681444902.1549770801) feature to add some authentication. I don't subscribe to any private or sensitive feeds, so at the moment that isn't much of a priority for this project.
## 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
## Adding Feeds
Find them in the site's source code and add them to the `feeds.json` file. This is the trickiest part of this whole setup I suppose.
The first version of this project used [Puppeteer](https://github.com/puppeteer/puppeteer) to extract the feeds from a site. This was actually quite cool, but would hang or fail periodicially. I was running this on its own server. It's on my list to look into converting this into a serverless version that could run using Netlify's Functions, but after using my own project for a month I realized it didn't make the thing feel much more usable to me. Builds were slow and there was a lot of work making sure things didn't timeout or use too much memory on the server. Simply parsing a list of known RSS feeds was much simpler and faster.
## Updating
The beauty of running Bubo on Netlify is you can [setup a Build Hook](https://www.netlify.com/docs/webhooks/#incoming-webhooks) to rebuild the site when you want to "refresh" the list of feeds. I'm using [IFTTT](https://ifttt.com) to trigger rebuilds once an hour, which is a perfectly sane rate to consume information at. You could do the same, or use another service like Zapier, EasyCron, setup a cronjob on your server or even setup a cronjob to run locally on your machine and ping the hook as often as you wish.
## How to use
- Clone this repository
- Find RSS feeds and add them to `src/feeds.json`
- Go to Netlify and deploy site from GitHub.
- That's it!
You'll probably want it to update regularly though.
### Instructiosn for IFTTT
### Instructions for Zapier
### Instructions for EasyCron
### Instructions for cronjob (local or otherwise)
## Sponsor
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 affiliates links to setup a $5 instance on [Linode](https://www.linode.com/?r=8729957ab02b50a695dcea12a5ca55570979d8b9) or [Digital Ocean](https://m.do.co/c/31f58d367777).

4
netlify.toml Normal file
View File

@ -0,0 +1,4 @@
[build]
command = "npm run build"
publish = "output/"

30
output/style.css Normal file
View File

@ -0,0 +1,30 @@
body {
font-family:system-ui;
font-size: 18px;
}
details:focus,
details:focus-within,
details:hover {
/* background:#ffeb3b; */
/* outline:2px #000 solid; */
}
details ul li {
}
summary {
cursor:pointer;
}
summary:hover {
opacity:.75;
}
.feed-url {
color:#aaa;
}

2015
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,14 @@
"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", "main": "src/index.js",
"scripts": { "scripts": {
"build": "node src/index.js > output/index.html",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"puppeteer": "^1.17.0", "node-fetch": "^2.6.0",
"nunjucks": "^3.2.0",
"rss-parser": "^3.6.3" "rss-parser": "^3.6.3"
} }
} }

14
src/feeds.json Normal file
View File

@ -0,0 +1,14 @@
{
"Blogs": [
"https://george.mand.is/feed.xml"
],
"GitHub": [
"https://github.com/georgemandis.atom?tab=repositories",
"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"
]
}

87
src/index.js Normal file
View File

@ -0,0 +1,87 @@
/**
* 🦉 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;
});
// 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);
})();

43
src/template.html Normal file
View File

@ -0,0 +1,43 @@
<!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 %}
</body>
</html>