Skip to content

Simple, dynamic pagination with HTMX

In our earlier application, we rendered the entries directly in the feed.html. Now let's create an endpoint that will render one page of entries using our partial.

Warning

When we get an RSS feed and parse it using feedparser, that gives us all the entries that are present in the feed. In some cases, it could be dozens or even hundreds of entries.

For pagination to be most effective, you'd only load (from your database) the entries for the current page. Since we don't have a database and we're parsing the feeds every time, we don't have the ability to do this. We're "faking" the need for pagination at the moment, but it'll show you how it works.

This is what the new endpoint looks like:

__init__.py
import feedparser
import jinja_partials
from flask import Flask, abort, render_template, request

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)


    @app.route("/entries/<path:feed_url>")
    def render_feed_entries(feed_url: str):
        try:
            feed = feeds[feed_url]
        except KeyError:
            abort(400)
        page = int(request.args.get("page", 0))

        # Below we're paginating the entries even though
        # in this application it's not necessary, just to
        # show what it might look like if it were.
        return render_template(
            "partials/entry_page.html",
            entries=list(feed["entries"].values())[page*5:page*5+5],
            href=feed["href"],
            page=page,
            max_page=len(feed["entries"])//5
        )


    return app

We'll modify the entry page partial to include a button to load the next page of entries. Having the button as part of the page may seem weird, but it's a nice way to ensure that all the data a page needs is colocated. Since a page knows its own page number, it's easy to make a button to get the next page (just add 1).

If we stored state outside of the page, then when we render the page, we'd have to keep track of the state elsewhere.

templates/partials/entry_page.html
1
2
3
4
5
6
7
8
9
{% 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 %}
{% if page < max_page - 1 %}
  <button hx-get="{{ url_for('render_feed_entries', feed_url=href, page=page+1) }}"
          hx-swap="outerHTML"
          hx-target="this">Load more</button>
{% endif %}

Finally, we can call our new endpoint from feed.html instead of rendering the partial there. Using HTMX, it can call the endpoint and replace the endpoint's response into the element that makes the request:

templates/feed.html
{% 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">
      <div class="px-4 sm:px-6 lg:px-8"
           id="entries"
           hx-get="{{ url_for('render_feed_entries', feed_url=feed.href, page=0) }}"
           hx-swap="afterbegin"
           hx-target="this"
           hx-trigger="load"></div>
    </main>
  </div>
{% endblock content %}