Creating a Spaceflight News Blog with HTMX & JSON API

The other day, I was casually browsing the web and stumbled upon the need for an API for my new little project. Instead of starting from scratch, I thought it would be great if I could find an existing API to build upon. That's when I came across this repository consisting of public APIs.

As I was browsing through the list of public APIs, I couldn't help but think how cool it would be to create a tiny website using some of these APIs. It just seemed like such a fun project that could be quickly put together and shown to others.

But guess what's even more exciting? Building this website using the available APIs with HTMX instead of our typical Single-Page Application (SPA) frameworks!

💡
You can explore the final demo here with the source code on GitHub.

Why

Well, hype aside, I find this exciting because of the challenge it presents. It’s no secret that HTMX can work with both hypermedia and JSON responses. Although, HTMX is primarily designed for hypermedia. However, I want to prove that it's completely possible (and not even that difficult) to use HTMX with JSON API as well.

Also, this seems to be a common real-life scenario that many encounter.

Just imagine someone saying, "Hey, I want to migrate to HTMX but I already have a bunch of JSON APIs. Do I have to change my entire server-side implementation? What a bummer!".

So, today, I’m going to write about how I came to create a very simple website powered by HTMX, Nunjucks, and Spaceflight News API (SNAPI). But really you can replicate this with any JSON API of your choice.

Static Sites

I love static websites. They are fast, cheap, and super easy to host. The best part? They hardly require any maintenance!

I really like the idea of serving dynamic content from a static site with just a single HTML file. When you combine it with HTMX, it takes away all the hassle of dealing with messy JavaScript code in your HTML files.

Prerequisites

  • Basic knowledge of HTML and JavaScript
  • A code editor with a live server for testing (e.g., Visual Studio Code with the Live Server extension) or any code playground like JSFiddle

Goals

By the end of this, you will have built a Spaceflight News blog page using HTMX with the following features:

  1. Displaying blog posts with titles, authors, publication dates, and summaries
  2. Pagination with a "Load More" button
  3. Search functionality

The final outcome would somewhat be akin to this ’Click to Load’ example, but built with an actual JSON API (refer to SNAPI documentation).

Implementation

Step 1: Basic HTML Structure

Let's get started with building our web page. The first step is to set up the basic HTML structure. It's super easy!

Just copy the provided HTML code into a new HTML file. You can name it something like index.html. Oh, and don't forget to include the necessary script in the <head> section:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Spaceflight News Blogs</title>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script src="https://unpkg.com/htmx.org"></script>
        <script src="https://unpkg.com/htmx.org/dist/ext/client-side-templates.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/browser/nunjucks.min.js"></script>
        <link
            rel="stylesheet"
            href="https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/dark.css"
        />
    </head>
    <body>
        <h1>Spaceflight News Blog</h1>
        <p>
            Demo of how the <b>load more</b> UX pattern works on htmx with JSON
            data APIs! You can find the source code on
            <a
                href="https://github.com/ngshiheng/spaceflight-news/blob/main/blogs.html"
                >GitHub</a
            >. Go take a look!
        </p>
    </body>
</html>

jsfiddle.net/jerrynsh/w89bvmk4/

Now, before we move on, I want to share some libraries that we'll be using:

💡
Did I mention that I absolutely love classless CSS? Check out these collections of classless CSS themes here!

API Schema

Here, we’ll be using the /v4/blogs endpoint. According to the SNAPI docs, we've got an example response that we can work with:

{
  "count": 123,
  "next": "http://api.example.org/accounts/?offset=400&limit=100",
  "previous": "http://api.example.org/accounts/?offset=200&limit=100",
  "results": [
    {
      "id": 0,
      "title": "string",
      "url": "string",
      "image_url": "string",
      "news_site": "string",
      "summary": "string",
      "published_at": "2023-09-12T23:51:31.675Z",
      "updated_at": "2023-09-12T23:51:31.675Z",
      "featured": true,
      "launches": [
        {
          "launch_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "provider": "string"
        }
      ],
      "events": [
        {
          "event_id": 2147483647,
          "provider": "string"
        }
      ]
    }
  ]
}

NOTE: We won’t be using all of the fields in this JSON response

If we take a look at the JSON response, it looks like we can make good use of the title, url, summary, and published_at fields to create our blog page.

The first 10 characters of an ISO 8601 date string format are in the format YYYY-MM-DD. So we’ll use the first 10 characters of published_at as our published date later.

The next and previous keys will come in handy for pagination.

Step 2: Client Side Templates

Next, we'll delve into actually calling the API and displaying the available blog posts on SNAPI.

<!DOCTYPE html>
<html lang="en">
    <head>
        <!-- omitted for brevity -->
    </head>
    <body>
        <h1>Spaceflight News Blog</h1>
        <!-- omitted for brevity -->
        <div hx-ext="client-side-templates">
            <div
                hx-get="https://api.spaceflightnewsapi.net/v4/blogs/"
                hx-trigger="load"
                nunjucks-template="blogs-template"
            >
                <template id="blogs-template">
                    
                <!-- count -->
                {% if not previous %}
                <i> Found {{ count }} articles.</i>
                {% endif %}

                <!-- results -->
                {% for blog in results %}
                <div>
                    <hr />
                    <h2>
                        <a href="{{ blog.url }}" target="_blank"
                            >{{ blog.title }}</a
                        >
                    </h2>
                    <h4>
                        Published by {{ blog.news_site }}, {{
                        blog.published_at | truncate(10, true, "")}}
                    </h4>
                    <p>{{ blog.summary }}</p>
                </div>
                {% endfor %}
                    
                </template>
              <div id="result"></div>
            </div>
        </div>
    </body>
</html>

jsfiddle.net/jerrynsh/w87Lc4v5/3/

Now, let me break it down a bit:

  1. We use the hx-ext="client-side-templates" attribute to activate the client-side templates extension for HTMX. This little beauty allows us to employ a templating engine (e.g. Nunjucks) to transform the JSON response from the API into HTML before it shows up on the page.
  2. The nunjucks-template attribute specifies the name of the Nunjucks template (blogs-template) we'll be using to convert the JSON response from the API into HTML. This template comprises the following elements:
  3. A count of the total number of results
  4. A list of the results, each featuring a title, news site, published date, and summary
  5. When the hx-get event is triggered on load, the https://api.spaceflightnewsapi.net/v4/blogs/ endpoint is called and the response is parsed as JSON. The Nunjucks template is then used to render the JSON response into HTML. The rendered HTML is then swapped into the DOM, replacing the div element with the ID blogs-template.

Now, let's take a peek at all this in action on our browser... Huh, it should have worked smoothly, right? Why is the blog section of our page completely blank?! Where are the blog posts?!?!

Debugging: CORS Workaround

Let’s check this out with some good old browser inspection. Checking out the Console tab, we see an error message stating:

Access to XMLHttpRequest at 'https://api.spaceflightnewsapi.net/v4/blogs/' from origin 'https://fiddle.jshell.net' has been blocked by CORS policy: Request header field hx-request is not allowed by Access-Control-Allow-Headers in preflight response.

Error on the Console tab

Ugh! It seems to be a CORS error. A bit of Googling led me to a helpful GitHub issue. After a bit of reading, I found out that there's an event listener for htmx:configRequest that we could use.

We need to work around this issue because the Spaceflight News API doesn't allow cross-origin requests by default.

Here's the script to add to the <head> section to implement the CORS workaround when making requests to external APIs with HTMX. It essentially just removes any custom headers:

<!DOCTYPE html>
<html lang="en">
    <head>
        <!-- omitted for brevity -->
        <script>
            // CORS workaround
            document.addEventListener("htmx:configRequest", (evt) => {
                evt.detail.headers = [];
            });
        </script>
    </head>
    <body>
       <!-- omitted for brevity -->
    </body>
</html>

jsfiddle.net/jerrynsh/jszekt4u/4/

We can now see a list of blog posts being rendered on our HTML page! Yay!

However, we are only seeing the results from the first page. Our next step is to work on the pagination part: add a "Load More" button at the bottom of the page.

Step 3: Pagination

The "Load More" button is implemented as a <button> element:

<!DOCTYPE html>
<html lang="en">
    <head>
        <!-- omitted for brevity -->
    </head>
    <body>
        <h1>Spaceflight News Blog</h1>
        <!-- omitted for brevity -->
        <div hx-ext="client-side-templates">
            <div
                hx-get="https://api.spaceflightnewsapi.net/v4/blogs/"
                hx-trigger="load"
                nunjucks-template="blogs-template"
            >
                <template id="blogs-template">
                    
                  <!-- omitted count for brevity -->
  
                  <!-- omitted results for brevity -->
  
                  <!-- load more -->
                  {% if next %}
                  <button
                      hx-get="{{ next }}"
                      hx-swap="outerHTML"
                      hx-trigger="click"
                      nunjucks-template="blogs-template"
                  >
                      Load More...
                  </button>
                  {% endif %}
          
                </template>
                <div id="result"></div>
            </div>
        </div>
    </body>
</html>

jsfiddle.net/jerrynsh/dyf0x18n/3/. Scroll to the bottom of the page to see the button!

When the “Load More” button is clicked, here’s what happens:

  1. The hx-get event is triggered. The next variable is used to specify the URL of the next page of blogs. The HTML that gets swapped into the DOM is the same as the one for the blogs-template element but with some fancy new blogs added to the end.
  2. The hx-swap attribute basically tells us how we should swap the new HTML into the DOM. In our case, we’re using the outerHTML swap mode. This means that the new HTML will replace the entire existing HTML for the blogs-template element.
  3. Lastly, the hx-trigger attribute tells us which event will trigger the hx-get event. Here, we're using the good ol' click event. So, whenever you click on that "Load More" button, hx-get event gets triggered and our pagination game begins!

That’s it! We’ve now got a “Load More” pagination implemented for our blog.

We're not done yet! We need to find the blog post about the Chandrayaan-3 lunar exploration mission. So, let's keep clicking that "Load More" button...and more...and more...

Step 4: Search Input

Just kidding! Who would want to keep clicking more to look for specific stuff? Thankfully, the /v4/blog of SNAPI supports search queries out of the box. So, we can easily search for the article about the Chandrayaan-3 lunar exploration mission.

Let's work on modifying the original div to a search input. So, you can easily type in your search keywords and find the articles you're interested in:

<!DOCTYPE html>
<html lang="en">

  <head>
    <!-- omitted for brevity -->
  </head>

  <body>
    <h1>Spaceflight News Blog</h1>
    <!-- omitted for brevity -->
    <div hx-ext="client-side-templates">
      <!-- search -->
       <input
             id="search-input"
             autofocus
             hx-get="https://api.spaceflightnewsapi.net/v4/blogs/"
             hx-indicator=".htmx-indicator"
             hx-target="#result"
             hx-trigger="load delay:100ms, keyup changed delay:500ms, search"
             name="search"
             nunjucks-template="blogs-template"
             placeholder="Search blogs..."
             type="search"
      />


      <template id="blogs-template">

        <!-- omitted for brevity -->

      </template>
      <div id="result"></div>

    </div>
  </body>

</html>

jsfiddle.net/jerrynsh/axg84cfL/9/. Note that the name attribute has to match the API's query string, i.e. /v4/blogs/?search

So, the <input> element acts as a search bar that uses the hx-get event to load a list of blogs from the API when you type in a search query.

The hx-get event is triggered by the load, keyup and search events (reference). The search results are then rendered in the #result element using the blogs-template Nunjucks template.

Finally, I can search for “Chandrayaan” related blog posts. No more endless clicking ‘Load More’!

Conclusion

We have successfully created a Spaceflight News blog page with live search functionality, dynamic content loading, and client-side templates using HTMX and JSON API.

So, what’s next? You could give the blog page a makeover by incorporating infinite scrolling, just like this. Or you could add a next-previous button instead.

For a better search experience, you could try to update the search input to sync with the search query of the URL so that you can do something like this.

Thanks for reading!

References

Making this wouldn’t be possible without these awesome resources:

Hosted on Digital Ocean.