Kitten

This tutorial is the first of the two Fetchiverse tutorials where you work with data from the fediverse (specifically, from Aral’s fediverse instance).

Topics covered

Fetchiverse

Since Kitten uses Node.js as its runtime, you can install, import, and use Node modules in your project just like in any other Node.js project.

💡 Kitten installs the latest long-term support (LTS) version of Node.js as its runtime.

You don’t need to install Node or npm yourself to make sites and apps with Kitten.

If you need to use npm, Kitten provides a handy alias to its version of npm with the kitten-npm command. Similarly, you can access Kitten’s version of node using the kitten-node command.

Note that Kitten is now using Node 20 even though it’s not LTS yet to avoid a performance regression introduced in Node 18 that affects OCSP stapling.

That said, Kitten also has commonly-used global APIs you can use without installing or importing them.

You’ve already seen one of those, the JavaScript Database (JSDB), which is available via the global db reference.

Similarly, the Fetch API is available for use as fetch.

Here’s an example of how to use the Fetch API to get the list of public posts from a Mastodon instance.

Welcome to the fediverse

This is the instance we’ll be using: https://mastodon.ar.al

And this is the JSON endpoint with the public timeline data: https://mastodon.ar.al/api/v1/timelines/public

Take a look at both to understand what we’re working with before creating a new folder called fetchiverse with a file called index.page.js in it.

Now add the following code to that file:

export default async function route () {
  const postsResponse = await fetch('https://mastodon.ar.al/api/v1/timelines/public')
  const posts = await postsResponse.json()

  return kitten.html`
    <h1>Aral’s Public Fediverse Timeline</h1>
    <ul>
      ${posts.map(post => (
        kitten.html`
          <li>
            <a class='avatar-link' href='${post.account.url}'>
              <img class='avatar' src='${post.account.avatar}' alt='${post.account.username}’s avatar'>
            </a>
            <div class='content'>
              ${kitten.safelyAddHtml(post.content)}
              ${post.media_attachments.map(media => (
                media.type === 'image' ? kitten.html`<img class='image' src='${media.url}' alt='${media.description}'>` : ''
              ))}
            </div>
          </li>
        `
      ))}
    </ul>
    <style>
      body { font-family: sans-serif; font-size: 1.25em; padding-left: 1.5em; padding-right: 1.5em; }
      h1 { font-size: 2.5em; text-align: center; }
      p:first-of-type { margin-top: 0; }
      p { line-height: 1.5; }
      a:not(.avatar-link) {
        text-decoration: none; background-color: rgb(139, 218, 255);
        border-radius: 0.25em; padding: 0.25em; color: black;
      }
      ul { padding: 0; }
      li {
        display: flex; align-items: flex-start; column-gap: 1em; padding: 1em;
        margin-bottom: 1em; background-color: #ccc; border-radius: 1em;
      }
      .avatar { width: 8em; border-radius: 1em; }
      .content { flex: 1; }
      .image { max-width: 100%; }
    </style>
  `
}

Run Kitten and hit https://localhost to see the latest public timeline from Aral’s mastodon instance.

🔒 Notice the call to kitten.safelyAddHtml() when rendering the post’s content.

This is content that we don’t necessarily trust so we have to be careful with how we add it to our page.

(OK, in this case, we know we’re using HTTPS for the connection to Aral’s Mastodon server and we can be reasonably sure that Aral, his host (toot.io *waves at Jan*), and Mastodon’s source code are trustworthy but still… who knows, maybe Aral got hacked and his instance is sending carefully-crafted <script> tags in the content to exfiltrate your secrets.)

To see what happens if we forget to add this global function call, remove it and run the example again. Ah, you see the escaped content. So Kitten tries to be as safe by possible by default and will escape any strings you attempt to interpolate into your page. However, in this case, we do want to display HTML. We just want to display a subset of safe HTML.

For content like this, Kitten provides the convenient global kitten.safelyAddHtml() method that uses the sanitize-html module.

💡 In this example you saw how to set attributes on HTML tags for the first time.

Here, we’re only setting string values and they work as you would expect. However, there are also HTML boolean attributes that work differently in HTML than, well, pretty much anywhere else:

If a boolean HTML attribute is present, it is “true”. If it is absent, it is “false.”

An example of such a boolean HTML attribute is disabled.

To make matters even more confusing, you cannot set an HTML boolean attribute to false. If you do, e.g., if you set disabled=false, your element will end up disabled.

To specify an HTML boolean attribute in Kitten, you can use the following idiom:

let disabled = true
kitten.html`
  <button ${disabled && 'disabled'}>Press me!</button>
`

The above code outputs <button disabled>…</button>.

💾 This example is available in the examples/fetchiverse folder of the Kitten source code.

Our Fetchiverse example is working great but could we refactor it – in other words, improve the code quality without changing its functionality – to make it even better?

Yes we can! And in the process, we will also learn how to use components and fragments in Kitten.

Next tutorial: Components and fragments