Skip to content
joshg253Public

About

Self-Hosted RSS Reader

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Lectio

CI Ruff Python FastAPI WebSub Webhooks GReader API Fever API Miniflux API Last commit

Work in progress. This README covers features and design intent. For setup, see docs/deployment.md.

Lectio is a self-hosted feed reader focused on fast reading triage, rich content handling, and automation. It runs well on a personal VPS with full multi-user support, and is built to keep feed reading fast, keyboard-friendly, and workflow-oriented.


What it is

A self-hosted RSS reader with a triage-first interface that adapts from a three-pane desktop layout to narrower tablet and phone workflows. Built on Python + FastAPI + the reader library, with a plain-HTML/JS frontend — no build step, no bundler, no framework.

The design priority is speed of triage: quickly marking things read, surfacing what matters, and staying out of the way.


Screenshots

Dark mode Light mode
Dark mode Light mode
Read Mode (e-ink) Saved articles
Read Mode Saved

More shots — the phone layout, Save Article, settings, automation, feed properties, tags, history and admin — are on the Screenshots wiki page. All of them are generated from synthetic demo data, never a real library.


What it does

The full tour — every feature, organized by area — is on the Features wiki page. The short version:

  • Triage first. Three-pane reader, keyboard nav everywhere, context menus, bulk mark-as-read/unread that updates in place, per-view remembered sort, and a layout that collapses to two panes on a tablet and one on a phone. Checkbox multi-select on the post list drives bulk tag/star/playlist/move actions — see the Features wiki page for the detail.
  • Picks up where you left off. Close the tab, press Back once too often, or swipe the app away — reopening Lectio returns you to the same article, scrolled to the same place. It also installs to your home screen as a standalone app.
  • Built for a phone, not just shrunk onto one. One pane at a time, with Back walking the view stack — article → feed → folder — and then toggling the folder drawer instead of closing the tab out from under you.
  • Tagging a post keeps it forever. A tag triggers a full offline capture — page, readability text, every image, and any files the post links to — so a kept article survives the site going down. Stars are the to-do pile; tags are the keep pile.
  • Read-it-later built in. Save any page (menu, bookmarklet, /api/save, or the browser extension) with no feed needed, then read it in Read Mode, an e-ink-friendly reading app at /read that works offline — including Archive, Delete and mark-read, which queue and drain when you reconnect.
  • Content that actually renders. Feed sanitization is Lectio's own, so embeds survive: sandboxed players from a curated host allowlist, inline SVG and MathML, recovered YouTube embeds, podcast audio (even when it lives in a separate host feed), inline LaTeX math, and images kept at the author's layout and a sane size.
  • Filter this view. Narrow the open list by text (zelda "tears of" !sponsored), video duration, or tag chips: tag:games, tag:!games, tag:gam*; tagged: matches tags you applied. Press Enter to pull in matches from further down the view as you scroll; it pairs with Select All for quick bulk triage.
  • Automation. Highlight, mark-as-read, tag-filter, deduplicate, and send-to-destination rules (Instapaper, Pinterest, Reddit, Quire, email, YouTube playlists, webhooks) at any scope, with dry-run and run history. An optional auto-read age keeps a quiet site's first update from flooding the unread list. See Features details.
  • Feed management that expects the real web. Resilient auto-discovery, Page Feeds for feedless sites, dev.to and DeviantArt adapters (adding an artist Watches them; on a Watch-feed byline, the name filters to that artist and the ✕ Unwatches), conditional GET, per-feed and per-domain backoff, GUID-churn suppression, feed compare, duplicate scanning, unsubscribe that keeps your curation, and an optional outbound proxy with FlareSolverr/last-resort-backend escalation for feeds behind an anti-bot challenge. A Utilities tab groups the clean-up tools, including one that removes duplicate and unwatchable videos from your YouTube playlists.
  • Fix a post in place. Edit its date, title or URL; clean up its body with an Aardvark-style element remover; re-fetch its content — undoably, with an Internet Archive fallback — one post, a multi-selection, or a whole feed or folder at a time. Teaser-only feeds can fetch the full article automatically as new posts arrive, and feeds whose RSS lacks real tags can pick up the publisher's own article topics as tag suggestions — each switched on per folder or per feed.
  • Yours to run. Isolated per-user databases with shared content caches, Google Reader / Fever / Miniflux v1 APIs, WebSub push, Takeout-style export/import, and no build step: plain HTML and JS, no bundler, no framework.

Technical overview

Layer What it does
main.py FastAPI routes, Jinja2 templates, all request handling
services/ Feed refresh, lead images, email, starred archive, YouTube, reader API wrapper
reader library Feed fetching, parsing, storage, ETag/conditional requests
Per-user SQLite DBs Feed/entry store, app state (prefs, rules, read history), starred archive; layout in tenancy

Dates, saved-article recovery, per-feed file capture, suggested tags, comic episode expansion, transparent-image handling, the refresh watchdog and the page-weight work are all described on the Features wiki page — they are behaviour, not architecture, and this file is meant to stay short. Design rationale for any of it lives in ARCHITECTURE.md.


Stack

  • Backend: Python 3.15, FastAPI, uvicorn
  • Feed library: reader (handles HTTP, parsing, ETags, scheduling)
  • Frontend: Vanilla JS, Jinja2 templates, no build step
  • Database: SQLite (WAL mode), per user
  • Deployment: Docker + docker-compose, Traefik reverse proxy

Development

Enable the lint hook once per clone:

git config core.hooksPath .githooks

It runs scripts/lint_changed.py over the lines you staged (ruff, ~40ms) so a lint problem is fixed in the commit rather than after a CI round-trip. CI runs the same script across the whole PR. Both are scoped to changed lines rather than whole files, because the repo carries a backlog of existing findings — new code is held to the rules while the backlog shrinks file by file. git commit --no-verify bypasses it.

  • Tests — make test (or uv run pytest).
  • Rebuilding the container — make rebuild; the image bakes the source in, so a rebuild is what makes a commit live.

CI, dependency audit, CodeQL, scratch cleanup and backups: docs/development.md.


Status

Active personal use. Not yet documented for general deployment. The codebase moves fast — APIs, DB schema, and config format may change without notice.

Issues and PRs welcome, but this is primarily a personal project.

About

Self-Hosted RSS Reader

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages