A Website without Reloading
One of the first things you can add with HTMX is boosted links. These links fetch the target link, and swap the returned HTML by the current page HTML.
With these, the page content changes without a page reload, which can make the page seem a bit snappier since there is no blank page in between link clicks.
First, let's create our Flask app. Since we don't have a database at the moment, we'll use a dictionary of feed URLs to feeds. Each feed has some attributes, as well as a dictionary of entries (mapping entry URL to entry body).
When we load the page, we'll go through our feeds and parse their RSS. This is very inefficient. You'd normally want a database, and parse the RSS periodically. It'll do the job for showing you how this all works.
import feedparser
import jinja_partials
from flask import Flask, render_template
feeds = {
"https://blog.teclado.com/rss/": {"title": "The Teclado Blog", "href": "https://blog.teclado.com/rss/", "show_images": True, "entries": {}},
"https://www.joshwcomeau.com/rss.xml": {"title": "Josh W. Comeau", "href": "https://www.joshwcomeau.com/rss.xml", "show_images": False, "entries": {}},
}
def create_app():
app = Flask(__name__)
jinja_partials.register_extensions(app)
@app.route("/feed/")
@app.route("/feed/<path:feed_url>")
def render_feed(feed_url: str = None):
for url, feed_ in feeds.items():
parsed_feed = feedparser.parse(url)
for entry in parsed_feed.entries:
if entry.link not in feed_["entries"]:
feed_["entries"][entry.link] = entry
if feed_url is None:
feed = list(feeds.values())[0]
else:
feed = feeds[feed_url]
return render_template("feed.html", feed=feed, feeds=feeds)
return app
Note
The way of updating the entries of a feed is over-complicated, but we need this logic for later on. At the moment, you could just do this:
But we'll be adding some custom data to entries later, so we don't want to overwrite all the entries each time. That's why we're only adding new entries, and not those that already exist.
Setting up the base template
In the base template we'll want to include our dependencies: TailwindCSS and HTMX.
We'll also set up the content
block where other templates can put their content.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed reader</title>
<script src="https://cdn.tailwindcss.com?plugins=typography,aspect-ratio"></script>
<script src="https://unpkg.com/htmx.org@1.9.9"></script>
</head>
<body>
{% block content %}
{% endblock content %}
</body>
</html>
Our partials for entry and entry page
One entry will represent one article in the RSS feed we're reading:
{#
A rendered summarized entry
- title
- published
- summary
- media_content (post image)
- author
- link
#}
<article class="grid grid-cols-[300px_600px] mb-4">
{% if media_content %}
<img class="aspect-video rounded-md shadow-md"
src="{{ media_content[0]['url'] }}" />
{% endif %}
<div class="ml-8 pt-4 {{ 'col-start-2' if media_content else 'col-start-1 col-span-2' }}">
<h2 class="font-bold text-2xl">{{ title }}</h2>
<p>
Published by <span class="font-medium">{{ author }}</span> on <span class="font-medium">{{ published
}}</span>
</p>
<p class="leading-6 mb-2">{{ summary | safe }}</p>
<a href="{{ link }}" class="underline" target="_blank">Read article</a>
</div>
</article>
Entries can have images, and they expect a few properties which are named after the feedparser
properties for an entry. That way when we parse a feed, we can just pass the entries directly to this template.
An entry page is a collection of entries. We don't need to have this as its own partial yet, but it will come in handy when we add pagination.
{% for item in entries %}
{{ render_partial("partials/entry.html", title=item.title, published=item.published, summary=item.summary,
media_content=item.media_content, author=item.author, link=item.link) }}
{% endfor %}
The main page
Now that we've got our partials and base template, we can put them together in the main feed page.
This has a list of feeds on the left, and the list of entries on the right.
{% extends "base.html" %}
{% block content %}
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-3">
{% for feed_url, feed_ in feeds.items() %}
<li>
<a href="{{ url_for('render_feed', feed_url=feed_.href) }}"
hx-boost
class="{{ 'bg-green-700 text-white' if feed.href == feed_['href'] else '' }} group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
{{ feed_['title'] }}
</a>
</li>
{% endfor %}
</ul>
</nav>
</div>
</div>
<div class="pl-72">
<main class="py-10">
{% for item in feed["entries"].values() %}
{{ render_partial("partials/entry.html", title=item.title, published=item.published, summary=item.summary,
media_content=item.media_content, author=item.author, link=item.link) }}
{% endfor %}
{# {{ render_partial("partials/entry_page.html", entries=entries) }} #}
</main>
</div>
{% endblock content %}
With this, we're ready to launch of site and test it out!