A single-user RSS/Atom feed reader built with Python and Django. Work-in-progress.
Live demo: feed-reader-9jin.onrender.com (read-only; first load may be slow — free-tier cold start)
- Reading — Unified "All Items" view, per-feed views, and a clean article reader with sanitized HTML and images
- Organization — Articles grouped by date ("Today", "Yesterday"), with favicons and per-feed grouping in the sidebar
- Feed management — Currently admin-only. Add feeds by URL, autodiscover feeds from a site URL, organize feeds into folders, rename and delete
- Read/unread tracking — Toggle read state, mark all as read, and unread counts per feed
- Search — Filter the current view by keyword
- Live interaction — Read/unread toggles and on-demand refresh without full page reloads, via
htmx - Background refresh — Scheduled fetching of new articles
This is a deliberately Python-centric stack — no separate JS framework, no Node.js build step.
| Layer | Technology |
|---|---|
| Language | Python 3.13 |
| Framework | Django 6.0 |
| Env / packages | uv (env, dependencies, lockfile, Python version) |
| Feed parsing | feedparser (RSS + Atom) |
| HTML sanitizing | nh3 |
| Styling | Tailwind CSS v4 + typography plugin (django-tailwind-cli, standalone — no Node.js) |
| Database | SQLite |
| Production server | gunicorn |
| Static files | WhiteNoise |
| Deployment | Render (free tier) |
| Testing | Django TestCase |
| Dev tooling | Ruff (lint/format), django-debug-toolbar |
| Layer | Technology | Purpose |
|---|---|---|
| Feed discovery | requests + BeautifulSoup |
Autodiscover feeds from a site URL |
| Interactivity | htmx + django-htmx |
Partial page updates without full reloads |
| Background refresh | Django Tasks framework or cron | Scheduled feed fetching |
Prerequisite: uv installed — it manages the Python version, virtual environment, and dependencies.
uv sync # create the venv and install dependencies
uv run python manage.py migrate # create the local SQLite database
uv run python manage.py tailwind runserver # build CSS and start the dev server
uv run python manage.py fetch_feeds # custom command: refresh all feeds
uv run python manage.py seed_demo # custom command: populate demo feeds and fetch their articlesThen open http://127.0.0.1:8000.
Feeds and articles are managed through the Django admin. Create an account, then sign in at
http://127.0.0.1:8000/admin/:
uv run python manage.py createsuperusererDiagram
Folder ||--o{ Feed: contains Feed ||--o{ Article : has
Folder {
string name
}
Feed {
string title
string feed_url
string site_url
datetime last_fetched
}
Article {
string title
string guid
datetime published
bool is_read
}
flowchart LR
A["fetch_feeds / seed_demo"] --> B["fetch_feed(feed)"]
B --> C["feedparser<br/>(conditional GET)"]
C -->|" 304 Not Modified "| D["skip — nothing new"]
C -->|" new entries "| E["nh3 sanitize<br/>+ build excerpt"]
E --> F["get_or_create<br/>(dedupe by guid)"]
F --> G[("SQLite")]
flowchart TD
Browser(["Browser"]) -->|" HTTP request "| RootURLs["config/urls.py"]
RootURLs -->|" include('feeds.urls') "| AppURLs["feeds/urls.py"]
AppURLs --> View["View<br/>all_items · feed_detail · article_detail"]
View -->|" ORM query "| Models["Models<br/>Folder · Feed · Article"]
Models --> DB[("SQLite")]
View -->|" render(template, context) "| Template["Page template<br/>(extends base.html)"]
ContextProc["context processor<br/>sidebar_feeds()"] -.->|" sidebar data,<br/>every request "| Template
Template -->|" {% include %} "| Partial["partials/article_list.html<br/>(list pages)"]
Template --> Response(["Rendered HTML"])
Response --> Browser
The UI for this project is loosely based on the iOS app Reeder Classic and Google Reader.
Stretch goals: full-text search, cached favicons, OPML import/export, and multi-user accounts.

