Part 3: Templating HTML with Python, Jinja2 and serverless WebAssembly
Tags: programming, python, serverless, frontend
Published: 2024-01-04 16:37:25

We’re up to part three of a series on building a bookmarker app with serverless Python. In the first part we covered the basics of using [Spin](https://developer.fermyon.com/spin/v2/index) to create a WebAssembly app out of Python code. Part 2 was where we scaffolded out a lot of the logic for saving and retrieving bookmarks.

In Part 3, we’ll turn our attention to the front end. We’ll start with some plain old HTML. Then we’ll mix in the famous Jinja2 template library (used in Django and other projects). Jinja2 is very easy to use on its own. By the end of this part we will have a working bookmarker app. Then we’ll kick it to the next level in Part 4 and Part 5, where we sprinkle some AI on it to automatically generate summaries of the pages we bookmark.

It’s time to turn our attention back to the `index()` function. This time, we’re going to refactor it so we can serve out some actual HTML. And before we even get going on our Python code, let’s create an actual `index.html` file with a form for submitting new URLs.

Next to `app.py`, we’ll create a file named `index.html`. Here’s some basic HTML content that creates two text fields and a submit button.

```html
<!DOCTYPE html>
<html>

<head>
    <title>Bookmarker: A Spin-powered bookmarking tool</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>

<body>
    <div class="container">
        <h1 class="title is-1">Bookmarker</h1>
        <h2 class="subtitle">Enter a new bookmark</h2>
        <form action="/add" method="post">
            <div class="field">
                <label class="label">Title</label>
                <div class="control">
                    <input name="title" class="input" type="text" placeholder="Page title">
                </div>
            </div>
            <div class="field">
                <label class="label">URL</label>
                <div class="control">
                    <input name="url" class="input" type="text" placeholder="http://">
                </div>
            </div>
            <div class="field is-grouped">
                <div class="control">
                    <input type="submit" class="button is-link">
                </div>
            </div>
        </form>
    </div>
</body>

</html>
```

This is a pretty basic HTML that contains a form with a couple of form fields and a submit button. To make styling a little easier, I opted to use [Bulma](https://bulma.io/). But that’s entirely optional, and does nothing to impact our app.

There is one thing to pay close attention to, though. The most important line in this file is this one, which tells the form where to submit form data:

```html
<form action="/add" method="post">
```

When the `Submit` button is pressed, this form control will send a `POST` request to `/add` with all of the form data. And this is exactly what we expected when we wrote the `add_url()` function above.

Now we have a static HTML file. But we have no way of delivering it to the client. One way would be to add the Spin static fileserver (`spin add static-fileserver`). Another way would be load and serve the file from within our Python app, and that’s what we’re going to do.

<aside>
💡 We are opting for this route because in a bit we are going to wire up Jinja templates, and that will require having access to the file in code.

</aside>

We’ll make some small modifications to our `index()` function, having it read the static `index.html` file from disk and then serve it out to the browser.

```python
# This is our index handler
@router.route("/", "/index.html")
def index(request):
    # Buffer the file into one string.
    buf = ""
    with open("index.html", "r") as f:
        buf += f.read()

    return Response(
        200,
        {"content-type": "text/html"},
        bytes(buf, "utf-8"),
    )
```

All we’re doing above is reading the contents of `index.html` into a buffer (`buf`) and then sending that back. Note also that we changed the `content-type` from `text/plain` to `text/html`.

By default, Spin does not allow an app to access the filesystem. This is part of Spin’s security profile. We need to make one quick change in our `spin.toml` to allow our running app to load the `index.html` file. Here’s just the relevant section from `spin.toml`:

```toml
[component.bookmarker]
source = "app.wasm"
key_value_stores = ["default"]
files = ["index.html"]         # <--- This is what we added
[component.bookmarker.build]
command = "spin py2wasm app -o app.wasm"
watch = ["app.py", "Pipfile"]
```

Details on the `files` directive can be found in Spin’s [configuration documentation](https://developer.fermyon.com/spin/v2/manifest-reference#the-component-table). It’s best to rerun `spin build --up` or `spin watch` any time you make a change to `spin.toml`

Now if we point our browser to http://localhost:3000, we’ll see this:


![Initial view of bookmarker app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bavsuvdprr3keu5ha2pe.png)


At this point, you should be able to enter a title and URL into the form and add a new bookmark to your app. Next, let’s modify the HTML to display our bookmarks.

## Adding Templating to Display Links

So far, we’ve worked with a single static HTML page. But we want to dynamically list all of the bookmarks we’ve collected. Python has an excellent template library called [Jinja2](https://jinja.palletsprojects.com/en/3.1.x/) that makes it really easy to dynamically render HTML. So we’ll use that.

To install Jinja2, we need to add a new line to our `Pipfile`:

```toml
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
http-router = "*"
Jinja2 = "*"   # ADDED THIS

[dev-packages]

[requires]
python_version = "3.10"
```

And once we’ve added the `Jinja2` package here, we need to re-run `pipenv update` to install the package. With that, we can start coding.

### A minor change to the HTML to make it a template

The first thing we’ll do is make a tiny change to our `index.html` file to make it a template. We’ll substitute the hard-coded page heading with a template variable:

```html
<!DOCTYPE html>
<html>

<head>
    <title>Bookmarker: A Spin-powered bookmarking tool</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>

<body>
    <div class="container">
        <h1 class="title is-1">{{title}}</h1>
        <h2 class="subtitle">Enter a new bookmark</h2>
        <form action="/add" method="post">
<!-- the rest is unchanged -->
```

All we did above is change our old `h1` content to `{{title}}`, which is a template substitution. Now, if we were to run our app as it is right now, we’d see the title set to `{{title}}` because all we are currently doing is sending `index.html` straight to the client. So we need to modify our `index()` function to use Jinja2.

### Setting up the template engine

To use Jinja2, we need to import a few libraries, set up a template rendering environment, and then send `index.html` through the template renderer. Here’s what that looks like:

```python
import json
from http_router import Router
from jinja2 import Environment, FileSystemLoader, select_autoescape # ADDED THIS
from spin_http import Response
from spin_key_value import kv_open_default
from urllib.parse import urlparse, parse_qs

# Create the new router
router = Router(trim_last_slash=True)

# Omitting a bunch of unchanged code here

# This is our index handler
@router.route("/", "/index.html")
def index(request):
    # Set up Jinja2 to load anything we put in "/".
    # This only has access to the files mapped in spin.toml.
    env = Environment(loader=FileSystemLoader("/"), autoescape=select_autoescape())

    # Render the template
    template = env.get_template("index.html")
    buf = template.render(title="Bookmarker")

    return Response(
        200,
        {"content-type": "text/html"},
        bytes(buf, "utf-8"),
    )
```

I trimmed down the code above to just show the changes. We import several tools from `jinja2` at the top. Then inside of our `index()` function, we replaced the file reading code we had before with the new template rendering code.

First, we set up an `Environment`, which is Jinja2’s way of describing a collection of templates and the associated configuration. The `FileSystemLoader` tells Jinja2 to load templates straight off of the filesystem, looking inside of the directory `/`. Understanding this requires understanding a little more about Spin and its security model.

A Spin app doesn’t actually get access to your filesystem. Instead, it gets access to a pseudo-filesystem that Spin sets up for it. And the only files that will show up there are the ones that `spin.toml` grants access to.

Previously, we added this line to `spin.toml`:

```toml
files = ["index.html"]
```

The syntax of this line is [described in the Spin docs](https://developer.fermyon.com/spin/v2/manifest-reference#the-component-table). Our specific line above is the most succinct way of telling Spin that the file `index.html` should be copied into the root of the pseudo-filesystem. So that means if our app looks in the `/` directory, it will see one, and only one, file: `index.html`. And this makes Spin apps very secure because they cannot get from the pseudo-filesystem to any of the real files on your real filesystem unless you explicitly grant it permission.

Back to our code, now that we have an `Environment` built in `env`, we can load and render `index.html` as a Jinja2 template:

```python
# Render the template
template = env.get_template("index.html")
buf = template.render(title="Bookmarker")
```

The `template.render()` function takes any number of named variables, which are passed into the template. So `title=“Bookmarker”` means that in the template, the statement `{{title}}` will get evaluated to `Bookmarker` before being sent to the client.

Once the app is rebuilt, we can test this out:


![Bookmarker screenshot with rendered templates](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xwsbtahu7ziqjpqf59tn.png)

With that done, we can take the next step and print out a list of links. This will require a little more template work. But first, we need to edit `index()`to fetch the bookmarks from Key Value Store and pass them into the template engine.

### Fetching and displaying bookmarks

Earlier we saw how to save bookmarks. As you may recall, we stored our bookmarks as a single JSON file in Key Value Store. So now all we need to do is fetch those, parse the JSON into a Python list, and then send that list into the template engine.

To do this, we need to do one more pass at our `index()` function.

```python
# This is our index handler
@router.route("/", "/index.html")
def index(request):
    # Set up Jinja2 to load anything we put in "/".
    # This only has access to the files mapped in spin.toml.
    env = Environment(loader=FileSystemLoader("/"), autoescape=select_autoescape())

    # Get our bookmarks out of KV store and parse the JSON
    store = kv_open_default()
    bookmarks = json.loads(store.get("bookmarks") or b"[]")

    # Render the template
    template = env.get_template("index.html")
    buf = template.render(title="Bookmarker", bookmarks=bookmarks)

    return Response(
        200,
        {"content-type": "text/html"},
        bytes(buf, "utf-8"),
    )
```

We’ve added a section to open the default Key Value Store (just like we did before) and then fetch the record named `”bookmarks”`. We parse the JSON into a Python object with `json.loads()`. And with that, we’ve got a record of all the bookmarks we’ve saved so far.

Next, we just need to pass those bookmarks into the template renderer:

```python
buf = template.render(title="Bookmarker", bookmarks=bookmarks)
```

When the template is rendered, now two different variables will be present: `title` and `bookmarks`. But while `title` is just a string, `bookmarks` will be a list of bookmarks that looks something like this:

```json
[
    {
        "title": "SOME TITLE",
        "url": "SOME URL"
    },
    {
        "title": "SOME OTHER TITLE",
        "url": "SOME OTHER URL"
    }
]
```

Next up, we can go back to our HTML file and add some template directives to loop through the `bookmarks` list and print our bookmarks.

Here’s what that looks like:

```html
    <div class="content">
        <h2 class="title is-2">Your Bookmarks</h2>
        <ul>
            {% for bookmark in bookmarks %}
            <li><a href="{{bookmark.url}}">{{bookmark.title}}</a></li>
            {% endfor %}
        </ul>
    </div>
```

In Jinja2, it’s easy to loop over a list using the `for… in` syntax. Essentially, that means that it will go through every entry in `bookmarks`, assigning that entry to the `bookmark` variable each time through the loop. And that means we can access each item’s `title` and `url` property using `{{bookmark.title}}` and `{{bookmark.url}}`.

Adding the above HTML at the bottom of the `index.html` file (just before the `</body>`) we can see the result in a web browser.


![Screenshot with one bookmark](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jlq9ehqghh9ae69ivl93.png)

And if we add another bookmark, we’ll see our new entry.


![Screenshot with two bookmarks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/avlqnxzxi3a3l2s602tb.png)



And there we have it! We’ve built a new bookmarking tool with Python, Spin, the `http_router` , Jinja2, and Key Value Store. Form here, we could [deploy it to Fermyon Cloud with `spin deploy](https://developer.fermyon.com/spin/v2/quickstart#deploy-the-application)` or keep adding new features.