A web development kit that’s small, purrs, and loves you.
 
 
 
Go to file
Aral Balkan 4d7b0d2420
Update component name
2023-11-27 10:55:01 +00:00
bin Closes #52: Stop relying on the --experimental-specifier-resolution flag 2022-11-11 13:54:55 +00:00
build-templates Add Node inspector support to kitten command 2023-11-01 11:54:45 +00:00
examples Update package-lock files 2023-11-26 18:54:10 +00:00
images Add kitten SVGs into source directory 2023-03-12 18:40:26 +00:00
src Display Kitten version information on Settings page 2023-11-26 19:06:30 +00:00
tests Standardise Kitten emoji as U+1F431 (Cat Face/Kitten) and use everywhere 2023-06-28 11:17:54 +01:00
web Update component name 2023-11-27 10:55:01 +00:00
.gitignore Add package script and git ignore generated packages directory 2023-10-06 18:19:39 +02:00
.nvmrc Update Node version to latest 2023-09-20 12:03:03 +01:00
.unimportedrc.json Add unimported as dev tool 2023-06-13 12:56:48 +03:00
.woodpecker.yml Add Woodpecker continuous integration (CI) configuration 2022-09-19 13:54:30 +01:00
CONTRIBUTORS.md Update automatically-generated contributors file 2023-11-09 19:34:35 +00:00
LICENSE Add AGPL v3 license text 2022-09-19 10:32:47 +01:00
README.md Add link to example; improve flow of evergreen web section 2023-11-25 18:03:56 +00:00
build Generate version file as build artifact and in JSON format 2023-09-13 11:16:17 +01:00
contributors.js Fix crash if contributors property in package.json is not an array 2023-05-26 23:18:43 +02:00
deploy Add installer to uploads during deployment 2023-11-13 18:39:35 +00:00
featured-contributors.md Fix Kitten sixel regression; inline supports-sixel and replied modules 2023-07-02 19:39:28 +01:00
install Installer can now also install specific API version package 2023-11-17 16:54:00 +00:00
kitten-day.sixel Add sixel 2023-03-31 12:31:24 +01:00
package Install before calculating package name 2023-11-07 12:30:09 +00:00
package-lock.json Update package-lock files 2023-11-26 18:54:10 +00:00
package.json Upgrade @small-web/kitten to version 2.10.2 2023-11-26 18:53:52 +00:00
suppress-experimental.cjs Import assertions now trigger new experimental warning in Node; suppress 2023-05-02 12:17:02 +01:00
update-version.js Make month in version stamp 1-based not 0-based 2023-11-07 10:09:21 +00:00

README.md

🐱 Kitten

A 💕 Small Web development kit.

Kitten is a little kit (“kitten”, get it?) optimised for people (not corporations) who make the new everyday things for themselves and other people not because they want to become billionaires but because they want to contribute to the common good by helping nurture the Small Web while adhering to the Small Technology Principles.

Meow!

Kitten features intuitive file system-based routing, HTML and accessibility validation, a built-in object database, WebSockets, zero-code authentication, public-key cryptography, Markdown support, syntax highlighting via highlight.js, semantic styles via Water CSS, and more.

🍼 Warning: Kittens still a baby.

Kitten is neither feature complete nor ready for production use.

Sections marked 🚧 represent unimplemented features.

Please feel free to have a play but proceed at your own risk. Here be dragons kittens.

System requirements

  • Linux, macOS, and Windows (WSL 2).
  • Bash version 5.x+.
  • Common developer tools and system utilities (git, tar, tee, and xz).

💡 macOS comes with an ancient version of Bash. To upgrade it to a modern one, run brew install bash using Homebrew.

💡For production servers, only Linux with systemd is supported.

Getting started

Install Kitten

Linux and macOS

Either:

😇 Clone this repository and run the ./install script, or

😈 Copy and paste one of the following commands into your terminal to pipe the installation script into your shell and run it:

🤓 You can download and view the install script source to verify its safe to pipe to your shell: https://kittens.small-web.org/install. And also compare it to: https://codeberg.org/kitten/app/raw/branch/main/install

Using wget:
wget -qO- https://kittens.small-web.org/install | bash
Using curl:
curl -sL https://kittens.small-web.org/install | bash

💡If youre running an “immutable” Linux distribution like Fedora Silverblue, please install Kitten from your host account (instead of from within a container) at least once so Kitten can set unprivileged ports to start from 80 (so it can run without elevated privileges).

💡If you want to install not the latest version of Kitten but the latest version for a given API version, modify your download command to tack the api version to the URL and to pass it as an argument to the bash script. For example, if you wanted to install the latest Kitten package for API version 1 using CURL, your command would look like this:

curl -sL https://kittens.small-web.org/install/1/ | bash -s -- 1

Windows

Kitten runs on Windows 10 and 11 under WSL 2.

The installation process, however, is not as seamless as it is on Linux and macOS. You must first install WSL 2 and you must manually add Kittens local development-time certificate authority to the trust stores of your Windows browsers to avoid certificate errors.

For step-by-step instructions, please see:

Install Kitten on Windows under WSL 2.

Like this? Fund us!

Small Technology Foundation is a tiny, independent not-for-profit.

We exist in part thanks to patronage by people like you. If you share our vision and want to support our work, please become a patron or donate to us today and help us continue to exist.

Tutorials

Hello, world!

Lets quickly create and test your first “Hello, world!” Kitten site.

  1. Create a directory for the example and enter it:

    mkdir kitten-playground
    cd kitten-playground
    
  2. Create a file called index.html and add the following content to it:

    <h1>Kitten</h1>
    <p>🐱️</p>
    
  3. Run Kitten using the following syntax:

    kitten
    

Once Kitten is running, hit https://localhost, and you should see your new site.

💡 When you run kitten without any arguments, it defaults to serving the current directory. You can also specify a path as the first argument (kitten [path to serve]) to serve a different directory than the one youre currently in.

💡 Notice that you didnt get a certificate error. This is because Kitten automatically creates a development-time TLS certificate for your local machine signed by a local certificate authority that it adds to your systems trust store (and to Firefox, if you have it installed) using Auto Encrypt Localhost.

(If youre running on Windows under WSL 2 and you do get a certificate error, please remember to manually install the local development certificates in your browsers.)

💡 Youre not limited to accessing your local machine via https://localhost. You can also use any local IP address that its accessible from (see Accessing your local machine from other devices on your local area network for more information). You can also use the IP aliases 127.0.0.2 - 127.0.0.4 and localhost aliases (subdomains) place1.localhost - place4.localhost without certificate errors. The latter, especially, are useful when testing the peer-to-peer features of Small Web apps.

💡 Small Web places made with Kitten are meant to be owned and used by one person. Until and identity and secret has been generated for this person, Kitten will display a link to the special page that will generate them. (You secret on a Small Web place never hits the server.)

💡 By default, Kitten will be as quiet as possible and only surface warnings and errors. If you want more extensive logging, start it with the VERBOSE environment variable set:

VERBOSE=true kitten [path to serve]

Similarly, if you want to see performance statistics, set PROFILE=true.

Note that the first time Kitten is run, it will create a TLS certificate authority and add it to your system trust stores as well as to Firefox and Chrome so you can run your development environment from https://localhost to match the deployment environment as closely as possible. Your operating system will ask your permission before allowing this.

So weve seen how Kitten happily serves any HTML you throw at it just like any good web server should.

But you can render HTML using any old web server…

Lets do something no other web server can do, shall we?

Counting kittens.

You know whats better than one kitten? Many kittens!

Rename your index.html file to index.page.js and update its contents to match the following:

let count = 1

export default () => kitten.html`
  <h1>Kitten count</h1>
  <p>${'🐱️'.repeat(count++)}</p>
`

Now run kitten, go to https://localhost, and refresh the page to see the number of kittens increase each time you do.

Ooh, magic!

How does it work?

A page in Kitten is written in plain old JavaScript (and hence the page.js extension).

Kitten uses file system routing. So, in this example, your /index.page.js file is mapped to the root route (/) of your site.

💡 Since Kitten pages and components, etc., as youll see later are just JavaScript files, you dont need any extra tooling to make things with Kitten. Your editor of choice will likely already know how to work with JavaScript (and HTML and CSS) files or can be easily configured to do so.

A route in Kitten is just an exported default function inside of a JavaScript module. Any HTML you return from your route is sent to the browser.

You write your HTML inside of kitten.html tagged template literals. These are just standard JavaScript template literals (template strings) that get passed through a special global function called kitten.html.

The example above contains the minimum code required to achieve our goal. However, heres a more explicit version in case youre not familiar with JavaScript arrow functions. In the version below, you can explicitly see everything thats happening (to underscore no pun intended that theres really no magic involved).

let count = 1

export default function route (_request, _response) {
  return kitten.html`
    <h1>Kitten count</h1>
    <p>${'🐱️'.repeat(count++)}</p>
  `
}

With the above version of the code, its clear that what youre exporting from the .page.js file is a route and that it gets passed the HTTP request and response objects.

💡 Weve added underscores to the names of the request and response parameters to make it clear that we know were not using them in the body of the function. We could also just have left the parameters out but its always good to be as explicit as possible about your intent when coding.

Demystifying things

Take a look at the title of the tab when you run the example. Do you see that it reads “Kitten count”?

Thats odd, you didnt tell Kitten to set the title of the page but if you look at the source code of the page, youll see that its set in the <head> of your page as:

<title>Kitten count</title>

Howd that happen?

Well, Kitten tries to do the right thing by default. So, if you dont set a title for your page, it tries to derive one for you by looking to see if there is a <h1> tag on your page and using that if there is. It might not be perfect but its better than nothing if all youre doing is coding up a quick example.

But what if you want to set your own title?

Enter, the page tag.

The <page> tag

Kitten has a special tag called <page> that you can use to set commonly found elements in the heads of your pages (lang, title, charset, icon, and viewport) as well as to include the libraries that Kitten has first-class support for (HTMX, HTMX WebSocket, Alpine.js, and Water.css).

To use it, you simple include the tag anywhere in a page (or fragment or component):

let count = 1

export default () => kitten.html`
  <page title='Kitten count example'>

  <h1>Kitten count</h1>
  <p>${'🐱️'.repeat(count++)}</p>
`

If you run the example now, youll see that the title you supplied is shown instead of the default one derived from the <h1> element.

💡 In addition to the <html> tags lang attribute, the regular <head> settings, and supported libraries, you can also use the syntax-highlighting-theme attribute to specify the highlight.js theme to be applied to code blocks rendered from <markdown>…</markdown> sections in your code.

💡 Youll see later that you can set anything you want in the head of your pages using the special HEAD slot (<content for='HEAD'>…</content>) but the <page> tag should let you set the most common things you need when making Small Web sites and apps with Kitten.

A little less magic, a little more type

A design goal of Kitten is to be easy to play with. Want to spin up a quick experiment or teach someone the basics of web development? Kitten should make that simple to do. Having magic globals like the kitten.html tagged template you saw earlier help with that.

However, for larger or longer-term projects where maintainability becomes an important consideration, you might want to implement type checking. Contrary to popular belief, you dont have to use TypeScript or have a build process for this.

You can simply document your types using JSDoc and turn on type checking by adding a // @ts-check comment to the top of your JavaScript files and any modern development environment that supports language intelligence for JavaScript via the TypeScript Language Server (e.g., Helix Editor, VSCodium, etc.) will pick them up and use them to provide you with static type checking, auto-completion, etc.

The problem is, what do you do about Kittens magic globals?

(If you have type checking on, you will get errors like Cannot find name 'html' if you use them.)

The answer is, you can install and use the type-safe Kitten namespace package (@small-web/kitten) in your project.

Heres how youd update the Kitten Count example to do this:

  1. Create a miminal package.json file:

    {
        "name": "kitten-count-typed",
        "type": "module"
    }
    
  2. Install the package:

    npm install @small-web/kitten
    
  3. Import the strongly-typed kitten object (its the default export):

    // @ts-check
    import kitten from '@small-web/kitten'
    
    let /** number */ count = 1
    
    export default () => kitten.html`
      <h1>Kitten count</h1>
      <p>${'🐱️'.repeat(count++)}</p>
    `
    

All the objects you find under the kitten namespace are also exported separately so, if you want to, you can also import just the objects you want. The following listing is thus equivalent to the one above:

// @ts-check
import { html } from '@small-web/kitten'

let /** number */ count = 1

export default () => html`
  <h1>Kitten count</h1>
  <p>${'🐱️'.repeat(count++)}</p>
`

💡 You can find the source code for this and other examples in Kittens source code repository, under the examples/ folder. Source code links are given below for your reference. Keep reading here for tutorials that take you through creating each one of them in turn:

💡 If a project contains node module dependencies, Kitten will automatically call npm install for you before serving it.

HTML is so 1991 (introducing Markdown)

HTML is great but did you know you can also use Markdown? (Well, now you do.)

Heres how the Kitten Count example looks in Markdown.

let count = 1

export default () => kitten.markdown`
  # Kitten count
  ${'🐱️'.repeat(count++)}
`

Pretty neat!

(Granted, its not a great use of Markdown but you get the idea.)

Kittens Markdown support comes from markdown-it.

Kitten supports a very rich set of Markdown features including automatic anchors for headings, syntax highlighting, figure (and figure caption) support, footnotes, typographical niceties (like converting typewriter quotes "" into curly quotes “”, insert, mark, subscript, superscript, etc.) and even table of contents creation for Markdown sections.

Well look at Markdown in more depth in its own example later. For now, know that you can add Markdown to your pages by using kitten.markdown tagged template strings and/or by enclosing Markdown regions within your HTML between <markdown>…</markdown> tags.

🚧 The Markdown example is yet to be written but you can see almost all the features demonstrated in examples/components and examples/markdown for the time being.

💡 Unlike HTML, Markdown is whitespace-sensitive so make sure you indent your code properly or it will get parsed incorrectly.

Persistence is the secret to success (or something)

So counting kittens is great fun but what happens if you restart the server?

All your kittens are lost! (This is a tragedy.)

So lets fix that.

(Brace yourself, youre about to use drumroll a SCARY database! Oooh!)

👻 Using JavaScript Database (JSDB) a (not so) scary database

Update your code to match this:

if (kitten.db.kittens === undefined) kitten.db.kittens = { count: 1 }

export default () => kitten.html`
  <h1>Kitten count<h1>
  <p>${'🐱️'.repeat(kitten.db.kittens.count++)}</p>
`

Your page should automatically reload in the browser with the new count.

Now refresh the page a few times, manually stop the server, restart it, and load the page again…

Wait, what?

Thats it?

Seriously?

Yep, thats the magic of the integrated JavaScript Database (JSDB) in Kitten.

If you dont believe me, restart the server and note that all your kittens are still there.

If you still dont believe me (wow, what a cynic), look in the following folder:

~/.local/share/small-tech.org/kitten/databases 

You should see another folder in it that mirrors the path of your project along with the domain and port you ran it at.

For example, if your project is in:

/var/home/aral/kitten-playground

And you ran it locally at the default domain and port (which is localhost and 443), then the folder will be named:

var.home.aral.kitten-playground.localhost.443

Inside this folder (your projects database folder), you should see a kittens.js file.

Open it up in a text editor and take a look at it.

It should look something like this:

export const _ = { 'count': 40 };
_['count'] = 41;
_['count'] = 42;

Thats what a table looks like in JavaScript Database (JSDB), the database thats integrated into Kitten and available from all your routes via the global db reference.

Kitten db command

Kitten provides you with a simple way to get information about and see a live view of your tables.

To get general information about the database, including where the database is located and which tables it has, use the db command from your projects folder:

kitten db

If you want to see a live view of the contents of a table, add its name to the db command.

So for the Persisted Kitten Count example, if you wanted to see the kittens table, you would enter:

kitten db kittens

💡 You are not limited to storing plain objects in your database. You can also store custom objects (instances of custom classes) and get them back with the correct type when you read them. Thats a more advanced feature, however, and you will need to implement a database app module to do it.

Check out the database app module in the Domain project to see an example of advanced database use in a larger real-world app.

💡 Theres so much more to JSDB and you can learn all about in the JSDB documentation.

Initialising things

If initialising your database table in your route feels a little yucky, thats because it is. What if more than one route needed to use that table? If we werent absolutely certain that the routes would be called in a given order, wed have to repeat the conditional initialisation in every route just to be safe.

You can see how this could become a maintenance nightmare.

Fear not, Kitten to the rescue!

If you create a special script called main.script.js in the root of your project folder and export a default function from it, Kitten will import that function and run it at start up.

This is a great place to carry out global initialisation for your app. This function also gets passed a reference to the Polka app instance so you can perform advanced tasks like adding custom middleware, etc., if you need to.

For our purposes, we could move the conditional initialisation of our database table to main.script.js like this:

export default function (_app) {
 if (kitten.db.kittens === undefined) kitten.db.kittens = { count: 1 }
}

Which would leave our route looking rather pristine:

export default () => kitten.html`
  <h1>Kitten count<h1>
  <p>${'🐱️'.repeat(kitten.db.kittens.count++)}</p>
`

💡 There is another way to achieve this thats specifically designed for the purpose of initialising databases (and adding strong typing to them) using a special App Module. You havent seen App Modules yet but, to get a feel for it, take a look at how its used in Domain: https://codeberg.org/domain/app/src/branch/main/app_modules/database

How many kittens are too many kittens?

So kittens are great but maybe after a certain number we should truncate the kitten emojis and display the exact count to save the person from having to count them, as fun as that might be.

Enter, conditionals.

Conditionals

Lets add a conditional so once there are more than twenty kittens, we show a message that states the additional number instead of showing yet more kitten emojis:

if (kitten.db.kittens === undefined) kitten.db.kittens = { count: 1 }

export default () => {
  kitten.db.kittens.count++

  return kitten.html`
    <h1>Kitten count</h1>
    <p>${'🐱️'.repeat(kitten.db.kittens.count > 20 ? 20 : kitten.db.kittens.count)}</p>

    <if ${kitten.db.kittens.count > 20}>
      <p>(and ${kitten.db.kittens.count - 20} more.)</p>
    </if>
  `
}

Ooh, whats that? An <if> tag? Thats not HTML.

No, its not. Its a little extension to HTML thats unique to Kitten.

What you see above is actually a convenient shorthand form of the <if> tag that comes with an important limitation: it can only be used to display one tag.

In the above example, that means that we can only include that one <p> tag after the <if>. The <p> tag itself can have any number of children but you cannot add any sibling tags.

To understand why, lets look at the full syntax if the <if> tag:

kitten.html`
  <if ${condition}>
    <then>
      <h1>The condition is true :)</h1>
      <img src='woot.gif' alt='Geeks partying'>
      <p>And there was much rejoicing!</p>
    </then>
    <else>
      <h1>The condition is false :(</h1>
      <p>Sad trombone.</p>
    </else>
  </if>
`

The <then> and <else> tags, apart from being optional (as you saw in the shorthand version, where theyre omitted), are also optionally-closed.

So the above example can also be written as shown below without losing the ability to include any number of siblings:

<if ${condition}>
  <then>
    <h1>The condition is true :)</h1>
    <img src='woot.gif' alt='Geeks partying'>
    <p>And there was much rejoicing!</p>
  <else>
    <h1>The condition is false :(</h1>
    <p>Sad trombone.</p>
</if>

In fact, most times, this last form is the one you should favour unless you only have a tiny snippet of code (with a single root node) that you want to add conditionally to your page, in which case you can use the first form you saw in the initial example.

The one downside of this canonical form is that youre left with a dangling indent right before the closing tag (</if>). If that gets on your nerves or makes it more difficult to ensure that your markup is valid at a glance, please feel free to use the full form. It really is up to you.

💡 Since the <then> and <else> tags are optional, the way the parser works is that it takes the first child of the <if> tag as the true condition and the second child as the false condition.

🪤 The <if> conditional does not short circuit so if you have values in your branches that might be null or undefined, use conditional chaining to ensure you dont encounter runtime errors. Either that, or use JavaScript conditionals in your templates instead.

Different kittens for different folks

Weve just seen how we can keep a persisted count of kittens.

The count, however, will be the same at any given time for anyone who visits the site.

So, if I visit the site from my computer and see 10 kittens and you visit the site right afterwards from your phone, youll see 11 kittens (because my visit raised the count by one).

This is because were persisting a single kitten count.

But what if we wanted every person who visits the site to have their own kitten count, as special and unique as they are?

Enter, sessions.

Sessions

Kitten has built-in session support to make implementing sessions trivial in your sites.

Since the protocol of the web (HTTP) is stateless, we need to use a database if we want to persist information. But what if we want to persist different information for each person that visits our site?

Or, more precisely, for each browser that visits our site.

Clearly, we need a way for our server to know that the request that just came in belongs to the same browser that sent that other request a few minutes ago. While there are different ways to do this, the method that Kitten uses is based on cookies.

💡Wait a minute… arent cookies evil things that Big Tech uses to violate your privacy?

Yes, and no.

You see, there are two types of cookies: first-party and third-party. The latter are whats used to track you. The former enable sites to implement basic functionality like sessions.

To make it easier to remember, think of first-party cookies as the ones your mom bakes for your birthday and third-party cookies as the ones a wicked witch makes to lure little kids into her hut.

The great thing about having built-in support for sessions is that you dont have to worry about any of this. Whenever a request comes into one of your routes, you can simply access the session object for it using:

request.session

So what it that object?

Its simply an object thats persisted in your sites database.

You can see the session objects that are generated by your site by viewing the sessions table in your database. For example, on my machine, I can view the sessions table for the project my in kitten-playground by running:

tail --follow ~/.local/share/small-tech.org/kitten/databases/var.home.aral.kitten-playground.localhost.443/sessions.js

(Look at Kittens console output to get the path for your machine/operating system.)

Kitten Count (with sessions)

So lets update our original persisted Kitten example to keep a separate count of Kittens for each browser that accesses it:

export default function (request, _response) {
  if (!request.session.kittens) {
    request.session.kittens = { count: 1 }
  }

  return kitten.html`
    <h1>Kitten count</h1>
    <p>${'🐱️'.repeat(request.session.kittens.count++)}</p>
  `
}

Now run the example using the kitten command and hit it from different browsers. You should see that each has a different count of kittens.

To see the cookie that Kitten sets, open up the Storage tab of your browsers developer tools and look under the Cookies section.

Delete the cookie and see what happens.

You can find this example at examples/kitten-count-sessions. There is also a more complicated session-based example at examples/trivia that uses the session to store a persons game state.

Database App Module

💡 This is an advanced feature. Please feel free to skip this section and return to it later if you find it confusing.

You are not limited to using the default, untyped database Kitten sets up for you.

The default database, like many of the beautiful defaults in Kitten, exists to make it easy to get started with Kitten and use it to build quick Small Web places, teaching programming, etc.

If, however, youre designing a Small Web place that you will need to maintain for a long period of time and/or that you are going to collaborate with others on, it would make sense to create your own database and to make it type safe so you get both errors and code completion during development time.

You can create your own custom database using a special type of app module called the database app module.

💡 Learn more about App Modules.

Like any other app module, your database app module goes in the special app_modules directory in the root of your project. Whats special is that the app module must be called database.

Start a new project and create your database app module by adding the following files in the app_modules/database/ folder:

package.json

{
  "name": "@app/database",
  "type": "module",
  "main": "database.js",
  "dependencies": {
    "@small-tech/jsdb": "^4.0.0",
    "@small-web/kitten": "^2.1.0"
  }
}

Since app modules are local node modules, they must all contain a package.json file. Here, were declaring the JavaScript Database (JSDB) and the @small-web/kitten ­library (which contains a strongly-typed kitten global instead of the untyped default one) are our two dependencies.

JSDB is the database Kitten uses for databases and Kitten expects your database app modules to use it also. No other databases are supported.

Kitten automatically installs the dependencies of app modules and checks for updated dependencies based on the package-lock.json file and installs them if necessary. As long as you make proper use of the package-lock.json file in your Small Web projects, Kitten should keep them updated for you during both development and production.

Next, lets create the database.js file that weve denoted as the main entry point into our module:

database.js

//@ts-check

import path from 'node:path'
import JSDB from '@small-tech/jsdb'

export class Kitten {
  name = ''
  age = 0

  constructor (name, age) {
    this.name = name
    this.age = age
  }

  toString () {
    return `${this.name} (${this.age} year${this.age === 1 ? '' : 's'} old)`
  }
}

/**
  @typedef {object} DatabaseSchema

  @property {Database} database
  @property {Array<Kitten>} kittens
*/

class Database {
  initialised = false
}

// When the database is being opened by the db commands, we dont compact it.
const compactOnLoad = process.env.jsdbCompactOnLoad === 'false' ? false : true

export const db = /** @type {DatabaseSchema} */ (
  JSDB.open(
    path.join(globalThis.kitten.databaseDirectory, 'db'),
    {
      compactOnLoad, 
      classes: [
        Kitten
      ]
    }
  )
)

export async function initialise () {
  if (!db.database) {
    db.database = new Database() 
  }

  if (!db.database.initialised) {
    db.kittens = [
      new Kitten('Fluffy', 1),
      new Kitten('Ms. Meow', 3),
      new Kitten('Whiskers', 7)
    ]
    db.database.initialised = true
    console.info(`\n  • Database initialised.`)
  }

  return db
}

export default db

The most important thing to note is the async initialise function that were exporting from the module.

Kitten will import and run this before starting the server when a database app module is found in your project. Furthermore, it will take the database reference returned from this function as set it as the default database (globalThis.kitten.db or, simply, kitten.db).

Notice a few other things:

We are using @ts-check to have our editor use the TypeScript language server to check for type errors and everything, including the database schema, is strongly typed.

This makes authoring easier as you will get type completions as well as errors when working with database objects.

Also, note that we set JSDBs compactOnLoad option based on the value of the process.env.jsdbCompactOnLoad environment variable set by Kitten. This is to enable Kittens database commands to run without compacting (and thereby altering) your database tables while your app might be running (e.g., when youre debugging your app by running the kitten db tail command in a separate Terminal window/pane.)

Finally, note that we are storing instances of the Kitten class in the database and, when opening the JSDB database, we are specifying the Kitten class in the classes property of the options object that we pass as the second argument.

Next, in order for our types to be recognised when the module is imported, we need to add a very simple TypeScript type definition file:

index.d.ts

// Export database instance as default export.
import db from './database.js'
export default db

// Also export all other exports.
export * from './database.js'

The database is ready so now comes the fun part: using it.

Lets add a page to display the kittens in the Kitten database, along with their ages:

index.page.js

// @ts-check
import _ from '@small-web/kitten'

/** @type {import('./app_modules/database/database.js').DatabaseSchema} */
const db = _.db

export default () => _.html`
  <h1>Kittens</h1>

  <ul>
    ${db.kittens.map(kitten => _.html`
      <li>${kitten}</li>
    `)}
  </ul>
`

💡 An app about kittens is an edge case so we import the kitten global as _ (underscore) to avoid confusing the library with actual (well, virtual) kittens.

The great advantage of using database app modules is that your database, like the rest of your project, can be strongly typed.

View the type of the kitten variable in the html template, for example, to confirm that it is, indeed, a Kitten instance.

Also note that because the kittens are instances of the Kitten class, we can simply refer to them in the template and their description will be printed out for us as their overriden toString() methods are called behind the scenes.

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 dont 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 Kittens version of node using the kitten-node command.

Note that Kitten is now using Node 20 even though its 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.

Youve 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.

Heres 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 well 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 were 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 (_request, _response) {
  const postsResponse = await fetch('https://mastodon.ar.al/api/v1/timelines/public')
  const posts = await postsResponse.json()

  return kitten.html`
    <h1>Arals 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 Arals mastodon instance.

🔒 Notice the call to kitten.safelyAddHtml() when rendering the posts content.

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

(OK, in this case, we know were using HTTPS for the connection to Arals Mastodon server and we can be reasonably sure that Aral, his host (toot.io *waves at Jan*), and Mastodons 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, were 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 examples/fetchiverse.

💾 Theres also version of this example that implements a streaming timeline using a WebSocket in examples/streamiverse.

Components and fragments

The above example is only about 50 lines of code in a single file. While thats fine for something so simple, in larger projects, it would help us to maintain our code if we break it up into smaller components and fragments.

Lets start by examining the layout of our list.

We have two major elements in each list item: the authors avatar and the post content itself. These would be prime candidates to make into separate fragments or components.

So lets do that:

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

  return kitten.html`
    <h1>Arals Public Fediverse Timeline</h1>
    <ul>
      ${posts.map(post => kitten.html`
        <li>
          <${Avatar} post=${post} />
          <${Content} post=${post} />
        </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; }
      ul { padding: 0; }
      li {
        display: flex; align-items: flex-start; column-gap: 1em; padding: 1em;
        margin-bottom: 1em; background-color: #ccc; border-radius: 1em;
      }
    </style>
  `
}

const Avatar = kitten.html`
  <a class='Avatar' href='${post.account.url}'>
    <img src='${post.account.avatar}' alt='${post.account.username}s avatar' />
  </a>
  <style>
    .Avatar img {
      width: 8em;
      border-radius: 1em;
    }
  </style>
`

const Content = ({ post }) => kitten.html`
  <div class='Content'>
    ${kitten.safelyAddHtml(post.content)}
    ${post.media_attachments.map(media => (
      media.type === 'image' && kitten.html`<img src='${media.url}' alt='${media.description}'>`
    ))}
  </div>
  <style>
    .Content { flex: 1; }
    .Content p:first-of-type { margin-top: 0; }
    .Content p { line-height: 1.5; }
    .Content a:not(.Avatar) {
      text-decoration: none; background-color: rgb(139, 218, 255);
      border-radius: 0.25em; padding: 0.25em; color: black;
    }
    .Content img { max-width: 100%; }
    /* Make sure posts dont overflow their containers. */
    .Content a {
      word-break: break-all;
    }
  </style>
`

Notice how we split up the styles also and encapsulated them in the components that they pertain to, scoping them to the component itself using class prefixes.

You might be wondering what happens when more than one copy of a component is included on the page. Do the style tags get replicated? The short answer is no. Kitten is smart enough to deduplicate the style tags in your components before rendering the page. In fact, it gathers all the styles on your page into a single, neat <style> tag in the <head> of your page.

Once youve separated your page into components and fragments, theres no rule that says they must all be in the same file. Since they are just snippets of JavaScript, you can put each one in its own file and import them in.

🐈 In Kitten, we call a custom HTML element that can be included as a custom tag a component and any other snippet of HTML, CSS, or JavaScript a fragment. They go in .component.js and .fragment.js files, respectively.

💡 A good rule of thumb is that if an element will be reused in multiple places on a page and/or in multiple pages, its a component. If it is a part of page that might be rendered separately from it (or makes sense to separate for organisational reasons to make maintenance easier), its a fragment.

💡 If youre using a component for layout, you can also use a .layout.js extension. Like every other Kitten-specific extension, these are just JavaScript files but Kitten knows that these are server-side routes and should not be served as client-side JavaScript.

So heres one way we could organise the code:

index.page.js

import Avatar from './Avatar.component.js'
import Content from './Content.component.js'

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

  return kitten.html`
    <h1>Arals Public Fediverse Timeline</h1>
    <ul>
      ${posts.map(post => (
        kitten.html`
          <li>
            <${Avatar} post=${post} />
            <${Content} post=${post} />
          </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; }
      ul { padding: 0; }
      li {
        display: flex; align-items: flex-start; column-gap: 1em; padding: 1em;
        margin-bottom: 1em; background-color: #ccc; border-radius: 1em;
      }
    </style>
  `
}

Avatar.component.js

export default ({ post }) => kitten.html`
  <a class='Avatar' href='${post.account.url}'>
    <img src='${post.account.avatar}' alt='${post.account.username}s avatar' />
  </a>
  <style>
    .Avatar img {
      width: 8em;
      border-radius: 1em;
    }
  </style>
`

Content.component.js

export default ({ post }) => kitten.html`
  <div class='Content'>
    ${kitten.safelyAddHtml(post.content)}
    ${post.media_attachments.map(media => (
      media.type === 'image' && kitten.html`<img src='${media.url}' alt='${media.description}'>`
    ))}
  </div>
  <style>
    .Content { flex: 1; }
    .Content p:first-of-type { margin-top: 0; }
    .Content p { line-height: 1.5; }
    .Content a:not(.Avatar) {
      text-decoration: none; background-color: rgb(139, 218, 255);
      border-radius: 0.25em; padding: 0.25em; color: black;
    }
    .Content img { max-width: 100%; }
    /* Make sure posts dont overflow their containers. */
    .Content a {
      word-break: break-all;
    }
  </style>
`

💡 Separating your pages into components and fragments should make your sites easier to maintain but this does come at the expense of locality of behaviour.

Locality of behaviour is about keeping related functionality together so you can easily read through the code in a linear fashion.

In this example, I probably would have kept everything in a single file since its so little code and since I dont need to include the Avatar or Content components from any other routes. Dont be afraid to experiment with how you organise your own projects. Soon, youll develop a knack for knowing when youve hit the sweet spot.

Component idiom

The component idiom in Kitten is for your components to have a single root element and for that element to have a class name thats used to scope classes to it.

🪤 You can have a component with multiple root elements but this will cause issues if you return just that component in an Ajax request to htmx. If you absolutely want to do this (not recommended), you must flatten the array returned by the component before sending it over the wire or else youll get the following error:

🞫 Error ERR_INVALID_ARG_TYPE: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of Array.

HTML attributes

In addition to the custom properties (or “props”) you can define and set on your components, you can also pass all other props (HTML attributes) to an element of your choosing within your component.

This is very useful as it means that you can use regular HTML attributes to listen for events and even use frameworks like htmx and Alpine.js with your components.

Take this simple example of a custom button. Notice how were passing the rest of the props obtained using the spread operator to the button element. This allows us to listen for the onclick event and display an alert dialogue in response to it.

MyButton.component.js

export default function ({label = 'Missing label prop (label="…")', ...props}) {
  return kitten.html`
    <div class='MyButton'>
      <button class='MyButton' ...${props}>${label}</button>
      <style>
        .MyButton button {
          border: 2px solid darkviolet;
          color: darkviolet;
          border-radius: 0.25em;
          padding: 0.25em 0.5em;
          background: transparent;
          font-size: 2em;
        }
      </style>
    </div>
  `
}

index.page.js

import MyButton from './MyButton.component.js'

export default () => kitten.html`
  <h1>My button</h1>

  <${MyButton} label='Press me!' onclick='alert("Thank you for pressing me!")' />
`

If, instead you wanted to use Alpine.js, this is how your index page would look:

import MyButton from './MyButton.component.js'

export default () => kitten.html`
  <page alpinejs>

  <h1>My button</h1>
  <${MyButton}
    label='Press me!'
    x-data
    @click='alert("Thank you for pressing me!")'
  />
`

💡For more advanced components, you can, of course, use htmx and Alpine.js inside your component and expose just your custom interface.

Slots

You can also pass content into components using a special attribute called a slot.

For example, if you have the following component:

Box.component.js

export default ({lineStyle = 'solid', SLOT}) => kitten.html`
  <div class='Box'>
    ${SLOT}
  </div>

  <style>
    .Box {
      padding: 1em;
      border: 1px ${lineStyle} CornflowerBlue;
    }
    .Box em { color: DarkViolet; }
  </style>
`

You can slot content into it like this from your page:

index.page.js

import Box from './Box.component.js'

export default () => kitten.html`
  <h1>Page</h1>
  <p>This is the page.</p>

  <${Box} lineStyle=dashed>
    <h2>Slotted content</h2>
    <p class='override'>This is <em>slotted content</em> from the page.</p>
  </>

  <p>This is the page again.</p>

  <style>
    body { font-family: sans-serif; }

    /* Example of using the cascade to override styles in components for slotted content. */
    .override em { color: DeepPink; }
  </style>
`

Layout components

Without layout components

Slots become especially useful for layout.

Imagine you have a basic site with three pages: the home page, an about page, and a contact page. For consistency, the pages should share the same header, navigation, and footer.

First, lets take a look at how wed achieve this without using layout components and slots:

pages.script.js
// The data model for our site.
export const [ HOME, ABOUT, CONTACT_ME ] = ['home', 'about', 'contact-me']
export const pages = {
  [HOME]: { title: 'Home', link: '/' },
  [ABOUT]: { title: 'About', link: '/about' },
  [CONTACT_ME]: { title: 'Contact me', link: '/contact-me' }
}
Header.component.js
import { pages } from './pages.script.js'
import Navigation from './Navigation.component.js'

export default ({ pageId }) => kitten.html`
  <header class='Header'>
    <${Navigation} pageId=${pageId} class='navigation'/>
    <h1>${pages[pageId].title}</h1>
  </header>

  <style>
    .Header { border-bottom: 1px solid gray; }
    .Header .navigation {
      color: white;
      padding: 1em;
      background-color: cadetblue;
    }
  </style>
`
Navigation.component.js
import { pages } from './pages.script.js'

export default ({ pageId, CLASS }) => kitten.html`
  <nav class='Navigation ${CLASS}'>
    <ul>
      ${Object.entries(pages).map(([__pageId, page]) => kitten.html`
        <li>
          <if ${ __pageId === pageId }>
            <span class='currentPage'>${page.title}</a>
          <else>
            <a href='${page.link}'>${page.title}</a>
          </if>
        </li>
      `)}
    </ul>
  </nav>

  <style>
    .Navigation { background-color: red; }
    .Navigation ul { list-style-type: none; display: flex; padding: 0; }
    .Navigation li:not(:first-of-type) { margin-left: 1em; }
    .Navigation a { color: white; }
  </style>
`
index.page.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
import { HOME } from './pages.script.js'

export default () => kitten.html`
  <${Header} pageId=${HOME} />
  <main>
    <markdown>
      ## Welcome to my home page!
      There are many home pages but this one is mine.
      I hope you enjoy it.
    </markdown>
  </main>
  <${Footer} />
`
about.page.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
import { ABOUT } from './pages.script.js'

export default () => kitten.html`
  <${Header} pageId=${ABOUT} />
  <main>
    <markdown>
      ## Hey, look, its me!
      Information about me.
    </markdown>
  </main>
  <${Footer} />
`
contact-me.page.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
import { CONTACT_ME } from './pages.script.js'

export default () => kitten.html`
  <${Header} pageId=${CONTACT_ME} />
  <main>
    <markdown>
      ## Get in touch!
      Normally, thered be a form here. Look in the guestbook and markdown examples if you want to see examples of such forms.
    </markdown>
  </main>
  <${Footer} />
`

So while this works, you probably see a couple of problem areas:

  1. While weve encapsulated the navigation component in the header component, we still have to manually add the header and footer components to every page. Thats error-prone redundancy that we should refactor out.
  2. Where would we put styles that we want to affect all our pages? We dont really have a place for that.

Enter layout components.

With layout components

Lets start our refactor by pulling out the common page structure into a layout component:

Site.layout.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'

export default ({ pageId, SLOT }) => kitten.html`
  <${Header} pageId=${pageId} />
  <main>
    ${SLOT}
  </main>
  <${Footer} />
`

💡Kitten supports a separate extension, layout.js, which is just an alias for component.js, if you want to make it clear that youre using a component for layout.

Now that we have a layout component with a slot for our content, lets refactor the pages to use it:

index.page.js
import Site from './Site.layout.js'
import { HOME } from './pages.script.js'

export default () => kitten.html`
  <${Site} pageId=${HOME}>
    <markdown>
      ## Welcome to my home page!
      There are many home pages but this one is mine.
      I hope you enjoy it.
    </markdown>
  </>
`
about.page.js
import Site from './Site.layout.js'
import { ABOUT } from './pages.script.js'

export default () => kitten.html`
  <${Site} pageId=${ABOUT}>
    <markdown>
      ## Hey, look, its me!
      Information about me.
    </markdown>
  </>
`

contact-me.page.js

import Site from './Site.layout.js'
import { CONTACT_ME } from './pages.script.js'

export default () => kitten.html`
  <${Site} pageId=${CONTACT_ME}>
    <markdown>
      ## Get in touch!
      Normally, thered be a form here. Look in the guestbook and markdown examples if you want to see examples of such forms.
    </markdown>
  </>
`

Well, thats a bit nicer.

💡 ${SLOT} is shorthand for ${SLOT.default}. As youll see later, slots also support named slots and SLOT.default is a special slot that stacks all slotted content that isnt addressed to a specific named slot.

Now, if we want to add global styles, the layout component becomes a natural place to do it:

import Header from './Header.component.js'
import Footer from './Footer.component.js'

export default ({ pageId, SLOT }) => kitten.html`
  <${Header} pageId=${pageId} />
  <main>
    ${SLOT}
  </main>
  <${Footer} />

  <style>
    body {
      font-family: system-ui, sans-serif;
      padding: 1em;
    }
  </style>
`

But what if we wanted to style the header or footer component?

Passing CSS class lists to components

In Kitten, you can declare a class (or class list) on a custom component just like you can on any other HTML element, using the class attribute.

So if we want to change all the text in the header so that it displays in small caps and add a bit of top margin between the footer and the rest of our page, we can do that like this:

import Header from './Header.component.js'
import Footer from './Footer.component.js'

export default ({ pageId, SLOT }) => kitten.html`
  <${Header} pageId=${pageId} class='header' />
  <main>
    ${SLOT}
  </main>
  <${Footer} class='footer' />

  <style>
    body {
      font-family: system-ui, sans-serif;
      padding: 1em;
    }
    .header {
      font-variant: small-caps;
    }
    .footer {
      margin-top: 2em;
    }
  </style>
`

However, to make it work, we also have to modify the header and footer components to add the class weve specified to the class attribute of their root element.

Kitten passes the class attribute as a special CLASS property to your component. All magic Kitten properties are in UPPERCASE to differentiate them from the properties you declare and use while authoring and to make them stand out.

Here are the relevant parts of the header and footer components after the changes have been made:

header.component.js

export default ({ pageId, CLASS }) => kitten.html`
  <header class='Header ${CLASS}'>
    <${Navigation} pageId=${pageId} class='navigation'/>
    <h1>${pages[pageId].title}</h1>
  </header>
`
export default ({ pageId, CLASS }) => kitten.html`
  <header class='Header ${CLASS}'>
    <${Navigation} pageId=${pageId} class='navigation'/>
    <h1>${pages[pageId].title}</h1>
  </header>
`

🐈 The idiom in Kitten is to pass CSS classes to components and have them apply to the components root element. Having a root element for components is also a Kitten idiom. However, you can have components without a root element (with multiple sibling elements) and, if you really want to, you can apply the passed class list to any element or to multiple elements. It just might get a bit harder to follow whats happening and to maintain your code if you do.

💡 You can see this project in the layout folder under examples.

To round out the layout example, lets see how youre not limited to just the one default slot.

Using named slots, you can have as many as you like.

Named slots

Say we wanted pages to be able to add or change elements in the heading and the footer.

We can do that using named slots.

Site.layout.js

import Header from './Header.component.js'
import Footer from './Footer.component.js'

export default ({ pageId, SLOT }) => kitten.html`
  <${Header} pageId=${pageId} class='header'>
    ${SLOT.header}
  </>
  <main>
    ${SLOT}
  </main>
  <${Footer} class='footer'>
    ${SLOT.footer}
  </>

  <style>
    body {
      font-family: system-ui, sans-serif;
      padding: 1em;
    }
    .header {
      font-variant: small-caps;
    }
    .footer {
      margin-top: 2em;
    }
  </style>
`

Note that weve added two named slots, header and footer and referenced them via the SLOT property as SLOT.header and SLOT.footer.

Also note that were actually placing these as default slots in the header and footer components.

The Site layouts default slot remains unchanged.

To make this work, lets implement slot support in the header and footer components.

Header.component.js

import { pages } from './pages.script.js'
import Navigation from './Navigation.component.js'

export default ({ pageId, SLOT, CLASS }) => kitten.html`
  <header class='Header ${CLASS}'>
    <${Navigation} pageId=${pageId} class='navigation'/>
    <h1>${pages[pageId].title}</h1>
    ${SLOT}
  </header>

  <style>
    .Header {
      border-bottom: 1px solid gray;
    }

    .Header .navigation {
      color: white;
      padding: 1em;
      background-color: cadetblue;
    }
  </style>
`
export default ({ SLOT, CLASS }) => kitten.html`
  <footer class='Footer ${CLASS}'>
    ${SLOT}
    <markdown>
      Copyright (c) 2023-present, Me.

      The content on this site is released under [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/).

      The source code of this site is released under [GNU AGPL 3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).

      Powered with love by Kitten. 🐱 💕
    </markdown>
  </footer>

  <style>
    .Footer {
      border-top: 1px solid gray;
      padding-top: 1em;
      text-align: center;
      font-size: small;
    }
    .Footer p { margin: 0.25em; }
  </style>
`

Finally, lets create some content to go in these named slots.

On the About page, well add a little shout-out to our funding page. And on the Contact Me page, lets add a notice to the header that were on holiday so no one tries to contact us.

about.page.js

import Site from './Site.layout.js'
import { ABOUT } from './pages.script.js'

export default () => kitten.html`
  <${Site} pageId=${ABOUT}>
    <markdown>
      ## Hey, look, its me!
      Information about me.
    </markdown>
    <content for='footer'>
      <div class='funding'>
        <p>If you want to fund my work, you can donate to <a href='https://small-tech.org/fund-us'>Small Technology Foundation</a></p>
      </div>
    </content>
  </>
  <style>
    .funding {
      padding: 1em;
      margin-bottom: 1em;
      background-color: aquamarine;
      border-radius: 1em;
    }
    .funding a {
      color: white;
    }
  </style>
`

contact-me.page

import Site from './Site.layout.js'
import { CONTACT_ME } from './pages.script.js'

export default () => kitten.html`
  <${Site} pageId=${CONTACT_ME}>
    <markdown>
      ## Get in touch!
      Normally, thered be a form here. Look in the guestbook and markdown examples if you want to see examples of such forms.
    </markdown>
    <content for='header'>
      <p class='awayMessage'>Im away on holiday at the moment</p>
    </content>
  </>
  <style>
    .awayMessage {
      text-align: center;
      font-weight: bold;
      font-size: 1.5em;
      background-color: yellow;
    }
  </style>
`

Notice that weve kept the navigation component in the header component which means that when we override the headers styles in the Site layout component to make the title display in in small caps, the navigations font changes also.

What if we wanted to be able to style the navigation separately in our layout template?

Well, we could create a new named slot in the header component that inserts content about the heading and use that to pass in the navigation component. Then, we could override the style in the layout component.

Lets do that as our final refactor:

Header.component.js

import { pages } from './pages.script.js'

export default ({ pageId, SLOT, CLASS }) => kitten.html`
  <header class='Header ${CLASS}'>
    ${SLOT.aboveHeading}
    <h1>${pages[pageId].title}</h1>
    ${SLOT}
  </header>

  <style>
    .Header {
      border-bottom: 1px solid gray;
    }
  </style>
`

Site.layout.js

import Header from './Header.component.js'
import Navigation from './Navigation.component.js'
import Footer from './Footer.component.js'

export default ({ pageId, SLOT }) => kitten.html`
  <${Header} pageId=${pageId} class='header'>
    <content for='aboveHeading'>
      <${Navigation} pageId=${pageId} class='navigation'/>
    </content>
    ${SLOT.header}
  </>
  <main>
    ${SLOT}
  </main>
  <${Footer} class='footer'>
    ${SLOT.footer}
  </>

  <style>
    body {
      font-family: system-ui, sans-serif;
      padding: 1em;
    }
    .header {
      font-variant: small-caps;
    }
    .navigation {
      font-variant: none;
      color: white;
      padding: 1em;
      background-color: cadetblue;
    }
    .footer {
      margin-top: 2em;
    }
  </style>
`

Note that if you wanted the content slotted into the header to go above the title instead of below it, you could have moved ${SLOT.header} inside the <content for='aboveHeading'>…</content> tag.

You can basically nest named and default slots any way you like that makes sense for the design of your site or app and best expresses your intent.

💡 You can even have multiple content areas target the same named slot and, just like the default slot, they will be stacked in the order in which they were encountered in your component or page.

Special page slots

In addition to the slots you can define and use while authoring Kitten apps, there are a handful of special preset slots you can use to send content into places you wouldnt otherwise be able to reach from your pages as they lie outside where your page is rendered in Kittens internal base layout component.

🐈 The internal base layout component is fully customisable.

The list of special page slots supported by kitten are HTML, HEAD, START_OF_BODY, BEFORE_LIBRARIES, AFTER_LIBRARIES, and END_OF_BODY.

💡 The HTML slot is unique in that you are expected to provide just the opening <html> tag, not the full document. Its there so you can specify a different language (e.g., <html lang=uk'> for Ukrainian). Do not return anything else in the content for this slot or the sky will fall on your head. 🙀

Heres an example, showing you most of the special page slots in use:

index.page.js

export default () => kitten.html`
  <page htmx alpinejs>

  <content for='HEAD'>
    <title>Special page slots</title>
    <link rel='icon' href='/favicon.ico'>
  </content>

  <h1>Special page slots</h1>

  <h2>This is just regular content on the page.</h2>

  <content for='START_OF_BODY'>
    <p>This is the <strong>start of the body</strong>.</p>
  </content>

  <content for='BEFORE_LIBRARIES'>
    <p>This is right <strong>before Kitten libraries</strong> (i.e., htmx and Alpine.js) are declared.</p>
  </content>

  <content for='AFTER_LIBRARIES'>
    <p>This is right <strong>after Kitten libraries</strong> are declared. Its a good place to put scripts if you want to make sure the Kitten libraries are loaded and can be used.</p>
  </content>

  <content for='END_OF_BODY'>
    <p>This is the <strong>end of the body.</strong></p>
  </content>

  <style>
    body { font-family: system-ui, sans-serif; }
    p { padding: 1em; border: 0.25em solid deeppink; border-radius: 0.5em; }
  </style>
`

Notice how you can use the HEAD slot to set the title and specify an icon. Kitten has intelligent defaults for these that it uses if you forget (it tries to get the title from your page if you have a h1 tag and it sets the favicon to an empty one so you dont get load errors in your developer console, etc.)

🐈 Just like regular slots, you can specify more than one of the special page slots and any content you send there will stack.

You can also specify certain <head> properties in the <page> tag.

These are the title, charset, viewport, and icon properties. For anything else you want to add to the head of your web page, please use the special HEAD page slot.

💡 When a HEAD slot exists, it has priority over properties defined in <page> tags.

For example, you could also have written the above example as follows:

export default () => kitten.html`
  <page htmx alpinejs title='Special page slots' icon='/favicon.ico'>

  <content for='HEAD'>
    <title>Special page slots</title>
    <link rel='icon' href='/favicon.ico'>
  </content>

  <h1>Special page slots</h1>
`

Streamiverse

While the fetching a fediverse timeline is fun and all, wouldnt it be cooler if you could stream it? Lets do just that using a WebSocket on the server and htmx to enhance base fetchiverse example.

💡 Kitten, via its first-class support for htmx, encourages a Hypermedia-Driven Application architecture for web applications where application state is represented in hypermedia. Basically, this means we send HTML between the client and server instead of using data formats like JSON and state is managed on the server.

💡 If you want to learn htmx (and about hypermedia in general), there is now a book called Hypermedia Systems by the folks who authored and maintain htmx.

The only exception to this is when it comes to protecting the identity and privacy of the person who owns a Kitten site/app. Identity and authentication, as we will see later, are handled entirely in the client (in the browser) via public-key cryptography as we expect that the client runs on a device (a computer, phone, etc.) that is entirely within the control of the person.

index.page.js

Lets start with the code for our new index page:

import Post from './Post.component.js'

export default async function route (_request, _response) {
  const response = await fetch('https://mastodon.ar.al/api/v1/timelines/public')
  const posts = await response.json()
  
  return kitten.html`
    <page htmx htmx-websocket>

    <h1>Arals Public Fediverse Timeline</h1>
    <ul id='posts' hx-ext='ws' ws-connect='/updates.socket'>
      ${posts.map(post => kitten.html`<${Post} post=${post} />`)}
    </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; }
      ul { padding: 0; }
    </style>
  `
}

Pay special attention to the unordered list tags attributes:

<ul id='posts' hx-ext='ws' ws-connect='/updates.socket'>
  ${posts.map(post => kitten.html`<${Post} post=${post} />`)}
</ul>

Specifically:

  • It has an id. We will be using this id from our socket code to tell client where to add new posts as they stream in.

  • It uses the htmx WebSockets extension (hx-ext='ws') and tells it to connect to a WebSocket route at the path /updates.socket (ws-connect='/updates.socket').

Finally, we have to tell Kitten, explicitly, that we are using htmx and its WebSocket extension on the page so it knows to include script tags in the head of the rendered page to load in those libraries:

<page htmx htmx-websocket>

💡 Kitten has built-in support for htmx, the htmx WebSockets extension, and Alpine.js. It exposes the HTMX, HTMX_WEBSOCKET, and ALPINEJS constants for you globally to use when returning a libraries array from your page routes.

Since htmx is a progressive enhancement on HTML, if you forget to include it, your page will render and display without any errors, it just wont have any client-side interactivity.

Believe it or not, thats all the code you need on the client to set up and manage a WebSocket connection.

😻 When I said Kitten loves you, I meant Kitten loves you.

Post.component

Notice how weve refactored the fetchiverse example so that we now have a Post component. Weve also added the simple Avatar and Content components to the same file in the name of locality of behaviour.

export default function Post ({ post }) {
  return kitten.html`
    <li class='Post component'>
      <${Avatar} post=${post} />
      <${Content} post=${post} />
    </li>
    <style>
      .Post {
        display: flex; align-items: flex-start; column-gap: 1em; padding: 1em;
        margin-bottom: 1em; background-color: #ccc; border-radius: 1em;
      }
    </style>
  `
}

// Private components (can only be used by Post).

const Avatar = ({ post }) => kitten.html`
  <a class='Avatar component' href='${post.account.url}'>
    <img src='${post.account.avatar}' alt='${post.account.username}s avatar' />
  </a>
  <style>
    .Avatar img {
      width: 8em;
      border-radius: 1em;
    }
  </style>
`

const Content = ({ post }) => kitten.html`
  <div class='Content component'>
    ${kitten.safelyAddHtml(post.content)}
    ${post.media_attachments.map(media => (
      media.type === 'image' && kitten.html`<img src='${media.url}' alt='${media.description}'>`
    ))}
  </div>
  <style>
    .Content { flex: 1; }
    .Content p:first-of-type { margin-top: 0; }
    .Content p { line-height: 1.5; }
    .Content a:not(.Avatar) {
      text-decoration: none; background-color: rgb(139, 218, 255);
      border-radius: 0.25em; padding: 0.25em; color: black;
    }
    .Content img { max-width: 100%; }
    /* Make sure posts dont overflow their containers. */
    .Content a {
      word-break: break-all;
    }
  </style>
`

A post is a natural unit for a component in our example as we receive individual post updates from the Mastodon API. Since we send HTML over the wire, our web socket will have to create Post instances. And we also have to create Post instances in our original GET route in the index page that sends over the initial timeline. By having Post as a component in its own module, we can import and use it from both places.

💡 None of the code in the Post, Avatar, or Content components has otherwise changed from the fetchiverse example.

Finally, lets look at the big new thing that makes this version stream: the socket route.

updates.socket

🐈 In Kitten, you declare WebSocket routes in .socket.js files.

🪤 You will have noticed that Kitten usually strips the extensions from your routes. If you have a page called /hello.page, for example, you can access it from https://localhost/hello. The exception is WebSocket routes, which keep their extensions. So the path to the updates.socket route is https://localhost/updates.socket.

💡 The main reason for this is due to the built-in redirection Kitten performs to forward URIs that dont contain a trailing slash to ones that do (e.g., https://localhost/hello will get a 308 forward to https://localhost/hello/). This works for HTTP routes but is not guaranteed to work for WebSocket routes (because clients are not obligated to follow redirects during the handshake/protocol upgrade stage… dont ask me why not.) So, to avoid the situation where you could have to refer to your WebSocket route as, for example, '/chat/' and where it would not work if you forgot the trailing slash which would be very easy to do Kitten decrees that the file extension is kept for socket routes, thereby bypassing the issue altogether (while also improving semantics and further differentiating WebSocket routes from HTTP routes at a glance).

import Post from './Post.component.js'

let stream = null

export default function socket (socket, _request) {
  // Lazily start listening for timeline updates on first request.
  if (stream === null) {
    console.log('  🐘 Listening for Mastodon updates…')
    stream = new kitten.WebSocket('wss://mastodon.ar.al/api/v1/streaming?stream=public')

    stream.addEventListener('message', event => {
      const message = JSON.parse(event.data)
      if (message.event === 'update') {
        const post = JSON.parse(message.payload)

        console.log(`  🐘 Got an update from ${post.account.username}!`)

        const update = kitten.html`
          <div hx-swap-oob="afterbegin:#posts">
            <${Post} post=${post} />
          </div>
        `

        socket.all(update)
      }
    })
  }
}

The WebSocket route itself creates a WebSocket connection to consume Arals public Mastodon feed from Arals Mastodon server. It also adds a message listener that gets called each time theres a message from the Mastodon server. Since we only care about new posts in this example, we only handle update messages.

💡 As you can see here, WebSocket connections do not have to be client-to-server, they can be server-to-server also. To help you create these sorts of connections, Kitten exposes a reference to the WebSocket module it uses internally (ws) at kitten.WebSocket. This is what were using in the code above.

The messages sent by Mastodon are in JavaScript Object Notation (JSON) format so the first thing we do is to parse them into a plain JavaScript object. And in that object we find the payload property that contains the post itself.

Now comes the the htmx magic:

const update = kitten.html`
  <div hx-swap-oob="afterbegin:#posts">
    <${Post} post=${post} />
  </div>
`

The hx-swap-oob attribute (which stands for swap out-of-band) will signal to htmx in the browser that it should add this post to the top of the list of posts on the page (remember that our posts list had the id of posts).

Finally, after weve created our Post snippet, we send it to all connected WebSocket clients using the special .all() method on the socket object:

socket.all(update)

And thats all there is to it!

Run the example using kitten and visit https://localhost to see Arals public fediverse timeline streaming from his Mastodon server.

💡 Notice that were sending HTML over the wire using the WebSocket. This is how htmx works. It makes it possible for us to create dynamic functionality like a streaming fediverse timeline without writing any custom client-side JavaScript.

WebSocket Echos

In the Streamiverse example, we used a WebSocket to stream in post updates but we could just as well have used Server-Sent Events (SSE) given that the communication was one-way.

💡 Kitten does not currently implement support for HTTP/2 so if you use SSE, people will be limited to 6 browser connections to your app.

🚧 Support for Server-Sent Events has not been implemented yet in Kitten.

Whats unique about WebSockets is that you can carry out asynchronous two-way communication.

So lets take advantage of that by creating a very simple WebSocket echo server that simply returns what we send it.

First, lets create the page:

index.page.js

export default () => kitten.html`
  <page htmx htmx-websocket water>

  <main
    hx-ext='ws'
    ws-connect='/echo.socket'
  >
    <h1>WebSocket Echo</h1>
    <form id='message-form' ws-send>
      <label for='message'>Message</label>
      <input id='message' name='message' type='text' required> 
      <button type='submit'>Send</button>
    </form>

    <ul id='echos' hx-swap-oob='beforeend'></ul>
  </main>
`

The only new thing here is that we have a form with the ws-send

And now, lets add the WebSocket route:

echo.socket.js

export default function (socket, request) {
  console.log('Echo socket: new client connection.', request.session)

  socket.addEventListener('message', event => {
    const message = JSON.parse(event.data).message

    socket.send(`
      <ul id='echos' hx-swap-oob='beforeend'>
        <li><strong>“${message}”</strong> received on server.</li>
      </ul>
    `)
  })
}

And thats it. Now run the example and you should see your messages being echoed back to you.

Enhancing usability with Alpine.js

The most visible and annoying usability issue right now is that when you send a message, the message box is not cleared. So lets fix that using Kittens built-in support for Alpine.js.

First, update the <page> tag so Kitten knows to include Alpine.js on the page:

<page htmx htmx-websocket water alpinejs>

Then, update the <input> element so it clears itself after the message has been sent:

<form id='message-form' ws-send><input 
    id='message' name='message' type='text' required
    x-data
    @htmx:ws-after-send.window='$el.value = ""'
  >
</form>

So whats happening here is that were using Alpines @ syntax to specify an inline handler for htmxs ws-after-send event.

💡 Remember that we can only use Alpine.js within Alpine.js components and the way you designate an Alpine.js component is to declare an x-data attribute on it. In this case, we have no actual data the component has to manage so we just use an empty x-data tag.

While htmx supports both camelCase and kebap-case for event names, Alpine.js can only work with kebap case since attribute names are case insensitive in HTML5. So while you will see events in the former style in the htmx documentation, remember to use kebap case when listening to htmx events using Alpine.js.

💡 The @ syntax for defining event handlers is a shortcut for Alpines more verbose x-on attribute so we could also have written that line as:

<input  x-on:htmx:ws-after-send.window='$el.value = ""'>

That said, given htmx uses a namespace (htmx:) that also contains a colon, you might find the shorthand @ syntax easier to author and read.

We also specify that we want to listen to the event on the window as it is dispatched from the parent <form> node so it would otherwise not reach the child <input> node (events in JavaScript bubble up, not down).

💡 To find out more about how Alpine.js works, make sure you read the Alpine.js documentation. Its got a tiny API and you can likely work through the whole documentation in less than an hour.

Send a few more messages and notice that sent messages are now cleared from the input box.

Ah, thats better! 😻

Kitten chat

You can do a lot by using a WebSocket server to communicate with your application. You can use it, for example, in place of POST requests to send remote commands to the server and get asynchronous results back. Thats really powerful in creating responsive applications. But youre not limited to sending messages to just one page. You can also broadcast messages to all connected pages.

To see how that works, lets create a simple chat example, starting with the index page:

index.page.js

import styles from './index.styles.js'

// The page template is static so we render it outside
// the route handler for efficiency (so it only gets rendered
// once when this file first loads instead of on every request).
export default () => kitten.html`
  <page htmx htmx-websocket>

  <main>
    <h1>🐱 <a href='https://codeberg.org/kitten/app'>Kitten</a> Chat</h1>

    <div
      id='chat'
      hx-ext='ws'
      ws-connect='/chat.socket'
      x-data
    >
      <ul id='messages'>
         <!-- Received messages will go here. -->
      </ul>

      <form id='message-form' ws-send>
        <label for='nickname'>Nickname:</label>
        <input
          id='nickname' name='nickname' type='text' required
          @htmx:ws-after-send.window='$el.value = ""'
        >
        <label for='text'>Message:</label>
        <input id='text' name='text' type='text' required />
        <button id='sendButton' type='submit'>Send</button>
      </form>
    </div>
  </main>

  ${[styles]}
`

Also, create an index.styles.js file and add the following CSS rules to it so our interface fills up the whole browser window, with the majority of the space reserved for the chat messages.

🐈 Weve wrapped our entire interface a <main> tag. This is because we are going to add styles that wouldnt work when applied to the <body> tag that Kitten renders your page onto. You might have seen mainstream web frameworks render pages into a special div. Kitten tries to keep special cases to a minimum and doesnt do that.

💡 We could have just inlined the styles into the HTML block but since theyre quite verbose, we decided to put them into their own file. Note that the syntax were using to include (interpolate) the styles into the HTML. By wrapping them in an array, we are telling HTMX to bypass any sanitisation it may otherwise perform on interpolated values. Needless to say, only do this with trusted content and never with data you obtain from an API call, etc. In those instances, use the global kitten.safelyAddHtml() function.

index.styles.js

export default kitten.css`
  * { box-sizing: border-box; }

  /* Make interface fill full browser canvas. */
  main {
    display: flex;
    font-family: sans-serif;
    height: calc(var(--vh, 1vh) * 100 - 1em);
    flex-direction: column;
    flex-wrap: nowrap;
    justify-content: flex-start;
    align-content: stretch;
    align-items: flex-start;
    padding: 1em;
  }

  h1 {
    margin-top: 0;
    margin-bottom: 0;
  }

  p {
    margin-top: 0;
    margin-bottom: 0;
  }

  a {
    color: #334b4c;
  }

  form {
    background: #eee;
    display: grid;
    grid-template-columns: [labels] auto [controls] 1fr;
    align-items: center;
    grid-row-gap: 0.5em;
    grid-column-gap: 0.5em;
    padding: 0.75em;
    width: 100%;
  }

  form > label { grid-column: labels; }

  form > input, form > button {
    grid-column: controls;
    min-width: 6em;
    max-width: 300px;
    padding: 0.5em;
    font-size: 1em;
  }

  button {
    text-align: center;
    cursor: pointer;
    font-size:16px;
    color: white;
    border-radius: 4px;
    background-color:#466B6A;
    border: none;
    padding: 0.75em;
    padding-top: 0.25em;
    padding-bottom: 0.25em;
    transition: color 0.5s;
    transition: background-color 0.5s;
  }

  button:hover {
    color: black;
    background-color: #92AAA4;
  }

  button:disabled {
    color: #999;
    background-color: #ccc;
  }

  /* The chat div should not affect the layout. This is actually a smell.
   It is purely being used to declaratively create a WebSocket connection.
   Need to think about this.
  */
  #chat {
    display: contents;
  }

  #messages {
    list-style: none;
    width: 100%;
    flex: 100 1 auto;
    align-self: stretch;
    overflow-y: scroll;
    background-color: #eee;
    padding: 0.75em;
  }
`

OK, and now, finally, lets create our socket route to handle the passing of messages between people.

chat.socket route

export default function (socket, request) {
  socket.addEventListener('message', event => {
    // A new message has been received: broadcast it to all clients
    // in the same room after performing basic validation.
    const message = JSON.parse(event.data)

    if (!isValidMessage(message)) {
      console.warn(`Message is invalid; not broadcasting.`)
      return
    }

    const numberOfRecipients = socket.all(
      kitten.html`
        <div hx-swap-oob="beforeend:#messages">
          <li><strong>${message.nickname}</strong> ${message.text}</li>
        </div>
      `
    )

    // Log the number of recipients message was sent to
    // and make sure we pluralise the log message properly.
    console.log(`  🫧 Kitten ${request.originalUrl} message from ${message.nickname} broadcast to `
      + `${numberOfRecipients} recipient`
      + `${numberOfRecipients === 1 ? '' : 's'}.`)
  })
}

// Some basic validation.

// Is the passed object a valid string?
function isValidString(s) {
  return Boolean(s)                // Isnt null, undefined, '', or 0
    && typeof s === 'string'       // and is the correct type
    && s.replace(/\s/g, '') !== '' // and is not just whitespace.
}

// Is the passed message object valid?
function isValidMessage(m) {
  return isValidString(m.nickname) && isValidString(m.text)
}

Now, if you run kitten and visit https://localhost you should see the chat interface and be able to send messages. Open another browser window to see the messages appear.

💡 When you send a message, we are not optimistically copying it to the messages list on the client. In fact, our app currently has no custom client-side functionality at all. Weve declared all dynamic functionally as htmx attributes in the HTML. So thats why we use the sockets all() method to send received messages to all clients, including the one that originally sent the message. (Or else the person sending the message would not see it in the message list.) This also means that you can know that the socket is working even if you dont open another browser tab or window to see the sent messages appear as long as theyre appearing for you in your own window after being sent.

If we were optimistically updating the messages list with client-side logic, we would use the broadcast() method on the socket instead. This method ensures that a message is sent to all clients apart from the one that originally sent it.

Also, due to the way htmxs WebSocket extension functions, the outer <div> (or any other outer element you specify in your response) is stripped by htmx before updating the document. So if you view source on your page, youll see that only the list items are present in the list.

So weve just written a very basic chat app without writing any custom client-side logic at all. Thats pretty cool. But our chat app does have a number of usability issues that we could improve by sprinkling some more custom logic on the client using Alpine.js.

Lets start with the first-launch experience…

When the page initially loads, the message list is empty. If youve used the app before, then youll know that thats where the messages go but its not overly friendly. So lets show a placeholder message there when there are no messages and hide it when the first message arrives.

Modify your index.page.js to add a placeholder list item to the #messages list:

<div
  id='chat'
  hx-ext='ws'
  ws-connect='/chat.socket'
  x-data='{ showPlaceholder: true }'
>
  <ul id='messages' @htmx:load='showPlaceholder = false'>
    <li id='placeholder' x-show='showPlaceholder'>
      No messages yet, why not send one?
    </li>
  </ul></div>

Run the app and verify that the placeholder gets hidden when the first message arrives in the message list.

The unordered list receives htmx:load events whenever new data (a list item) is loaded. When it hears this, it sets the showPlaceholder flag to false. The placeholder list item is shown or hidden based on the state of this flag using the x-show attribute.

The flag itself is declared in the parent <div> using an x-data attribute.

So that was simple.

But how about this: reduce the height of your chat window so that there is only space for two or three to messages to display. Now send yourself some messages and notice what happens when the fourth or fifth message comes in. Theyre added to the list but they arent shown on screen as theyre added to the bottom of the chat section.

Lets fix that by adding a bit more Alpine code to make the chat section scroll to the bottom whenever a new message is received:

<div
  id='chat'
  hx-ext='ws'
  ws-connect='/chat.socket'
  x-data='{ showPlaceholder: true }'
>
  <ul id='messages' @htmx:load='
    showPlaceholder = false
    $el.scrollTop = $el.scrollHeight
  '>
    <li id='placeholder' x-show='showPlaceholder'>
      No messages yet, why not send one?
    </li>
  </ul></div>

Getting syntax highlighting for Alpine.js

It might be that your editor supports syntax highlighting for embedded Alpine.js code by default. If so, thats great and you can skip this section.

If it doesnt, theres a little trick you can use to get it in any editor that supports syntax highlighting for tagged template literals. Kitten comes with a simple one called kitten.js that simply passes through anything its passed. If you use it, however, you will get syntax highlighting for embedded code in editors like Helix Editor. It does add complexity to the code, however, and gives you yet another quote mark that you might have to escape if it appears in your code. For larger pieces of code, it is best to use a standard JavaScript file and include it at runtime in your page.

All that said, this is what the above code would look like if you used it:

<div 
  id='chat'
  hx-ext='ws'
  ws-connect='/chat.socket'
  x-data='${kitten.js`{ showPlaceholder: true }`}'
>
  <ul id='messages' @htmx:load='${kitten.js`
    showPlaceholder = false
    $el.scrollTop = $el.scrollHeight
  `}'>
    <li id='placeholder' x-show='showPlaceholder'>
      No messages yet, why not send one?
    </li>
  </ul></div>

Adding a status indicator

Since a WebSocket is a persistent connection, it would be good to know when we get disconnected. htmxs WebSocket extension does a good job of queuing messages when this happens but it would help if we knew that we were offline (either because our Internet connection is disrupted or because the server has died).

So lets add a status indicator component that uses Alpine.js to achieve this:

StatusIndicator.component.js

export default () => kitten.html`
  <p>Status: <span 
    id='status'
    x-data='{ status: "Initialising…" }'
    @htmx:ws-connecting.window='status = "Connecting…"'
    @htmx:ws-open.window='status = "Online"'
    @htmx:ws-close.window='status = "Offline"'
    x-text='status'
    :class='
      status === "Online" ? "online" : status === "Offline" ? "offline" : ""
    '>
    Initialising…
  </span></p>

  <style>
    .online {color: green}
    .offline {color: red}
  </style>
`

Finally, lets add the StatusIndicator component to our index.page.js:

import styles from './index.styles.js'
import StatusIndicator from './StatusIndicator.component.js'

export default () => kitten.html`
  <h1>🐱 <a href='https://codeberg.org/kitten/app'>Kitten</a> Chat</h1>

  <${StatusIndicator} />

  <div
    id='chat'…
`

Now, when you run Kitten Chat, you should see the indicator turn green when youre online.

Stop Kitten and note that the indicator turns red and shows you that youre offline.

Restart Kitten and note the indicator turns green again once the app reconnects.

🚧 The htmx WebSocket extension implements an exponential backoff algorithm that keeps trying to connect to the server after exponentially longer waiting periods after getting disconnected. There is an issue with this implementation where this interval does not reset even if you close the browser tab. You actually have to restart the browser for the interval to reset. Im going to look into filing a bug about this and hopefully contribute a fix upstream once I get a chance.

Debugging htmx

If some of the htmx stuff seams rather opaque and magical to you, thats because, to a degree, it is. Making what htmx is doing visible by logging it might help you to debug your site or app when things go awry.

To that end, lets implement an htmx logger.

While Alpine.js lets you do commonly done things easily, its sometimes easiest just to pop out into plain old JavaScript for more convoluted things.

Since this is just a regular web page, you can add any number of <script> tags to it and load in external JavaScript by specifying its path.

So lets add a client-side .js file and implement our htmx logger in that.

💡 Note that .js files are just regular client-side JavaScript files. They are served from your server like any other static file. Make sure you dont put anything sensitive (like API keys, etc.) in them.

index.js

function onLoad() {
  // For debugging: log out htmx events to the console.
  htmx.logger = function(elt, event, data) {
    if(console) {
      console.log(event, elt, data)
    }
  }
}

window.addEventListener('load', onLoad)

💡 The htmx global is available when htmx is included on the page so we wait for the documents load event before attempting to attach the debugger.

Of course, just creating our script file isnt enough, we also need to load it from the page. We could just add it to the end of our page (maybe after the closing <main> tag) but then wed have a problem: we couldnt guarantee that it gets loaded after htmx itself does. (And our logger function relies on htmx having loaded so that the htmx global is available.)

What we really want is to add our script to the page after libraries like htmx have loaded. We can do this using the special page slots and the special Kitten tag called <content> which we saw earlier in the Layout Components section:

</main>
  
  <content for='AFTER_LIBRARIES'>
    <script src='./index.js' />
  </content>

  ${[styles]}

💡 If we wanted to use ES6 Modules in our script, wed have to add type='module' to our script tag.

Adding final touches for mobile devices using custom JavaScript

Now that we have a place to put arbitrary client-side JavaScript, lets use a trick also known as “a hacky workaround” (oh, hello, welcome to web development) to ensure our interface displays correctly even when the address bar is visible in mobile browsers.

Update your index.js file to match the following:

index.js

function fixInterfaceSizeOnMobile () {
  function resetHeight() {
    let vh = window.innerHeight * 0.01;
    document.querySelector(':root').style.setProperty('--vh', `${vh}px`)
  }

  resetHeight()

  window.onresize = function (_event) {
    resetHeight()
  }
}

function onLoad () {
  fixInterfaceSizeOnMobile()

  // For debugging: log out htmx events to the console.
  htmx.logger = function(elt, event, data) {
    if(console) {
      console.log(event, elt, data)
    }
  }
}

window.addEventListener('load', onLoad)

While this is an issue that only surfaces on mobile devices, running the fix does not have a negative effect on desktop browsers so we just always run it when the page first loads by listening for the load event on the window.

Focus is hard sometimes

Finally, lets make one last usability improvement. Wouldnt it be nice if we could keep chatting after sending a message without having to constantly click in the chat box? (In other words, if the chat box kept focus after sending a message, even if we click the Send button, for example.)

This is very easy to do using Alpine.js. Modify #text input in the #message-form as shown below:

<form id='message-form' ws-send><input 
    id='text' name='text' type='text' required
    @htmx:ws-after-send.window='
      $el.value = ""
      $el.focus()
    '
  /></form>

💡 Notice that were listening for the event a little differently here. Instead of listening for the @htmx:ws-after-send event on the <input> element itself, were using the .window modifier to listen for it on the window. Thats because the event is dispatched from the <form> element that contains the ws-send attribute. And that element is our parent so events dispatched from there will not bubble down to us (events in JavaScript bubble up). But the event will bubble up from there to the window so thats where we listen for it.

Now run the example on a desktop computer and notice that the message box keeps its focus even if you press the Send button.

Thats nice!

But now run the example on a mobile phone with a virtual keyboard. Ah. By keeping focus on the message field, were stopping the keyboard from hiding. And that means we cant see the message we just sent. Thats less than ideal. So lets implement another little hack by defining a function that tries to detect if the person is on mobile (remember that none of these hacks are ideal) and then lets see how we can call that JavaScript from our Alpine code.

First, add a function called isMobile() to index.js:

globalThis.isMobile = () => {
  return (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
  || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4)))
}

💡 Notice how we declared the function in global scope so we can call it from Alpine.js.

Now, modify your index.page.js one last time to only focus the field if the person is not on a mobile device:

<form id='message-form' ws-send><input id='text' name='text' type='text' required
    @htmx:ws-after-send.window='
      $el.value = ""

      // On mobile we dont refocus the input box
      // in case there is a virtual keyboard so
      // the person can see the message they just sent.
      if (!isMobile()) { $el.focus() }
    '
  ></form>

💡 Take note that this approach is brittle. We are using the user agent string to make a best guess effort whether the person is on a mobile device. User agent strings can be spoofed. Beyond that, just because a person is on a mobile device, it doesnt mean that theyre using a virtual keyboard. They could have a physical keyboard attached. Unfortunately, there isnt a built-in way of detecting from a web page whether someone is using a virtual keyboard. Although there are other hacks you might want to try.

You can find the final version of this example is the examples/kitten-chat folder.

Persisted Kitten Chat

While we were able to improve the usability of the Kitten Chat example by sprinkling a little Alpine.js here, a little JavaScript there on the client, there is one limitation that we need to implement more server-side functionality to overcome: the messages are not persisted.

If two people are having a chat and someone else enters the room, they dont see the messages that have already been sent.

JavaScript Database (JSDB) to the rescue once again!

What we need to do is to persist messages when they arrive in our chat socket and display the messages that are already in our database while rendering our index page. Since both the socket and page route now need to create messages, lets start by creating a Message component that can be used by both of them:

Message.component.js

export default function Message ({ message }) {
  return kitten.html`
    <li>
      <strong>${message.nickname}</strong> ${message.text}
    </li>
  `
}

This component simply takes a message object and render a list item that shows the persons nickname and the text of their message.

Now, lets refactor our WebSocket route (chat.socket.js) to:

  • Create our messages table (if it doesnt exist),
  • Persist received messages to the messages table,
  • Render messages using the Message component and send them to all connected clients.

chat.socket.js

import Message from './Message.component'

// Ensure the messages table exists in the database before using it.
if (kitten.db.messages === undefined) kitten.db.messages = []

// Kitten Chat example back-end.
export default function (socket, request) {
  socket.addEventListener('message', event => {
    const message = JSON.parse(event.data)

    if (!isValidMessage(message)) {
      console.warn(`Message is invalid; not broadcasting.`)
      return
    }

    // We dont need to use the message HEADERS, delete them so
    // they dont take unnecessary space when we persist the message.
    delete message.HEADERS

    // Persist the message in the messages table.
    kitten.db.messages.push(message)

    const numberOfRecipients = socket.all(
      kitten.html`
        <div hx-swap-oob="beforeend:#messages">
          <${Message} message='${message}' />
        </div>
      `
    )
    // …
  }
}
// …

💡 Messages sent by the htmx WebSocket extension include a htmx-specific HEADERS object.

For example:

{
  nickname: 'Aral',
  text: 'Hello, everyone!',
  HEADERS: {
    'HX-Request': 'true',
    'HX-Trigger': 'message-form',
    'HX-Trigger-Name': null,
    'HX-Target': 'chat',
    'HX-Current-URL': 'https://localhost/'
  }
}

All we want to store in the database is the nickname and text so we delete the HEADERS object before persisting so were left with an array of objects like the following in our database:

{
  nickname: 'Aral',
  text: 'Hello, everyone!'
}

Finally, lets modify our index page to both use our new Message component and, if there are any messages in the database, to render them in the page:

index.page.js

import styles from './index.styles.js'
import Message from './Message.component.js'
import StatusIndicator from './StatusIndicator.component.js'

// Ensure the messages table exists in the database before using it.
if (kitten.db.messages === undefined) kitten.db.messages = []

export default () => kitten.html`
  <div 
    id='chat'
    hx-ext='ws'
    ws-connect='/chat.socket'
    x-data='{ showPlaceholder: true }'
  >
    <ul id='messages' @htmx:load='
      showPlaceholder = false
      $el.scrollTop = $el.scrollHeight
    '>
      <if ${kitten.db.messages.length === 0}>
        <then>
          <li id='placeholder' x-show='showPlaceholder'>
            No messages yet, why not send one?
          </li>
        <else>
          ${kitten.db.messages.map(message => kitten.html`<${Message} message=${message} />`)}
      </if>
    </ul>
  </div>
`

💡 Since the conditional logic in our template is somewhat verbose, I chose to use Kittens <if> conditional. The thing to be aware of here is that, due to JavaScripts language limitations, the <if> conditional cannot short circuit (so every branch of the conditional is evaluated, even if it is false, even if only the the true branch is displayed). This means that if you try to access a property on an object that is null or undefined in the falsey branch, your app will return an error. In this case, it is not a problem but, if it way, you would either use chained optionals in all your branches or one of the following JavaScript-only conditional methods.

The first one, which resembles Kittens <if> syntax the most, is to use an immediately-invoked closure:

${(messages => {
  if (messages.length === 0) {
    return kitten.html`
      <li id='placeholder' x-show='showPlaceholder'>
        No messages yet, why not send one?
      </li>
    `
  } else {
    return messages.map(message => kitten.html`<${Message} message=${message} />`)
  }
})(kitten.db.messages)}

If thats confusing to read, you can also write it as a regular immediately-invoked function expression (IIFE):

${(function (messages) {
  if (messages.length === 0) {
    return kitten.html`
      <li id='placeholder' x-show='showPlaceholder'>
        No messages yet, why not send one?
      </li>
    `
  } else {
    return messages.map(message => kitten.html`<${Message} message=${message} />`)
  }
})(kitten.db.messages)}

Or, if thats still confusing, you can always use a conditional (ternary) operator. Notice how you refer to kitten.db.messages directly if you do this.

${
  kitten.db.messages.length === 0 ?
    kitten.html`
      <li id='placeholder' _='on htmx:load from #chat hide me'>
        No messages yet, why not send one?
      </li>
    `
  : kitten.db.messages.map(message => kitten.html`<${Message} message=${message} />`)
}

All three of these approaches are equivalent and, since they use native JavaScript statements, all of them short circuit. Feel free to use the one that reads best for you.

And thats it: now when you run the app and load it in your browser, you will see any messages that were sent previously when the page first loads.

I guess persistence really does pay off.

(Im here all week. 🐱)

Project-specific secret

Kitten automatically generates a cryptographically-secure secret for each project.

For the cryptographers among you, this is a base256-encoded ed25519 private key. For everyone else, its a lovely string of 32 emoji that looks something like this:

🌻🦝🍰🐨🍮🌼🍙🦃🦚🦈🪴🍆🙉🐘🌮🌍🧇🐊🦁🐂🐧🐨🐁🐙🐧🥜🐵🐮🥔🦘🍓🐻

This secret is shown to you only the very first time you run Kitten on a given project (folder).

If youre wondering how in the world you are going to type that in, dont worry: youre not supposed to be able to by design.

Instead, please add this secret to your password manager of choice.

If you implement authenticated routes in your application, you can use your password manager to enter your secret for you.

For technical details, please see the Cryptographical Properties section.

Authenticated Routes

To signal to Kitten that a route is only available when the person is authenticated, you add 🔒 to the end of the route name (thats a lock emoji).

Kitten itself has just a route thats available to all apps/sites created with Kitten at:

settings🔒/

If you hit the /settings route on any Kitten app/site, youll be automatically redirected to the /sign-in route if youre not authenticated and you only see the settings section if you are.

Adding the suffix to a directory, as shown here, ensures that it applies to all routes in that directory.

You can also add it to specific routes (files).

HTTP Routes

Weve seen examples of simple Kitten apps that use pages and WebSockets, but what if you want to POST data from a web form, implement Ajax with fragments of HTML using htmx, or create an Application Programming Interface (API) that returns JSON?

Enter HTTP Routes.

(OK, technically speaking, everything is an HTTP route but thats the terminology we use in Kitten to separate Pages from, well, every other HTTP route except WebSocket routes.)

Similar to how you create pages in .page.js files and WebSocket routes in .socket.js files, HTTP routes are declared using a naming convention based on their filename extension which can be any valid HTTP1/1.1 method in lowercase (e.g., .get.js, .post.js, .patch.js, .head.js, etc.)

HTTP Routes do not carry out any processing on whatever value you return for them.

So you can return a fragment of HTML (e.g., a component) if youre implementing Ajax, or use JSON.stringfy() to return a JSON repsonse, etc.

e.g.,

my-project
  ├ index.page.js
  ├ index.post.js
  ├ about
  │   ╰ index.page.js
  ├ todos
  │   ╰ index.get.js
  ╰ chat
     ╰ index.socket.js

Optionally, to organise larger projects, you can encapsulate your site within a src folder. If a src folder does exist, Kitten will only serve routes from that folder and not from the project root.

e.g.,

my-project
  ├ src
  │  ├ index.page.js
  │  ├ index.post.js
  │  ├ index.socket.js
  │  ╰ about
  │      ╰ index.page.js
  ├ test
  │   ╰ index.js
  ╰ README.md

POST/redirect/GET

One very common HTTP Route is POST, usually used when you want to send data back from a page and persist it.

The Guestbook example (examples/guestbook), demonstrates this pattern in the simplest possible way.

First, lets create a page that will display the form for signing the guestbook and existing guestbook entries:

index.page.js

if (!kitten.db.entries) kitten.db.entries = []

export default () => kitten.html`
  <h1>Guestbook</h1>

  <h2>Sign</h2>

  <form method='POST' action='/sign'>
    <label for='message'>Message</label>
    <textarea id='message' name='message' required></textarea>
    <label for='name'>Name</label>
    <input type='text' id='name' name='name' required />
    <button type='submit'>Sign</button>
  </form>

  <h2>Entries</h2>

  ${kitten.db.entries.length === 0 ?
    kitten.html`<p>Hey, no ones signed yet… be the first?</p>`
  :''}

  <ul>
    ${kitten.db.entries.map(entry => kitten.html`
      <li>
        <p class='message'>${entry.message}</p>
        <p class='nameAndDate'>${entry.name} (${new Date(entry.date).toLocaleString()})</p>
      </li>
    `)}
  </ul>

  <style>
    body { font-family: sans-serif; margin-left: auto; margin-right: auto; max-width: 20em; }
    label { display: block; }
    textarea, input[type='text'] { width: 100%; }
    textarea { height: 10em; }
    button { width: 100%; height: 2em; margin-top: 1em; font-size: 1em; }
    ul { list-style-type: none; padding: 0; }
    li { border-top: 1px dashed #999; }
    .message, .nameAndDate { font-family: cursive; }
    .message { font-size: 1.5em; }
    .nameAndDate { font-size: 1.25em; font-style: italic; text-align: right; }
  </style>
`

This is very straightforward. Notice that we have a form for signing the guestbook and its just plain HTML.

<form method='POST' action='/sign'>
  <label for='message'>Message</label>
  <textarea id='message' name='message' required></textarea>
  <label for='name'>Name</label>
  <input type='text' id='name' name='name' required />
  <button type='submit'>Sign</button>
</form>

Its method is set to POST and its action is /sign. That means that when the submit button is pressed, it will carry out an HTTP POST request to the /sign route on our server.

In that route, we will save the new guestbook entry and then redirect the persons browser back to the index page. This pattern of handling a POST request and then redirecting to a GET route (our pages are all GET routes), is called the POST/redirect/GET pattern.

So lets create our POST route:

sign.post.js

if (!kitten.db.entries) kitten.db.entries = []

export default (request, response) => {
  // Basic validation.
  if (!request.body || !request.body.message || !request.body.name) {
    return response.forbidden()
  }

  kitten.db.entries.push({
    message: request.body.message,
    name: request.body.name,
    date: Date.now()
  })

  response.get('/')
}

And thats it.

💡 Kitten has a number of request and response helpers defined to make your life easier. You just used two of them, above: response.forbidden(), which returns a HTTP 403: Forbidden error and response.get(), which is an alias for response.seeOther(), which returns and HTTP 303: See Other response.

In this case, since were going a Post/Redirect/Get (PRG), using the get() alias makes the intent of our code clearer.

You could also have manually handled the direction like this:

response.statusCode = 303
response.setHeader('Location', '/')
response.end()

(Which is exactly what Kitten does internally when you use the .get() / .seeOther() methods.)

In addition to the methods youve already seen, Kitten also supports the following helpers:

Request

is (array|string): returns true/false based on whether the content-type of the request matches the string or array of strings presented.

This is used internally for Express Busboy compatibility but you might find it useful in your apps too if youre doing low-level request handling.

Response

get (location), seeOther (location): 303 See Other redirect (always uses GET).

redirect (location), temporaryRedirect (location)`: 307 Temporary Redirect (does not change the request method).

permanentRedirect (location): 308 Permanent Redirect (does not change the request method)

badRequest (body?): 400 Bad Request.

unathenticated (body?), unauthorised (body?), unauthorized (body?): 401 Unauthorized (unauthenticated).

forbidden (body?): 403 Forbidden (request is authenticated but lacks sufficient rights i.e., authorisation to access the resource).

notFound (body?): 404 Not Found.

error (body?), internalServerError (body?): 500 Internal Server Error.

Run kitten command on your project folder and visit https://localhost to see your guestbook.

💡 Notice that were doing some very basic validation to make sure that body of the request (which is where the forms data is found) is as we expect it.

In case youre worried about script inject, type <script>alert("Hehe, I just hacked you!")</script> in your message box. Try it out and see what happens. Kittens template engine automatically escapes interpolated string content to avoid such attacks. If you wanted to allow HTML through, Kitten provides a global kitten.safelyAddHtml() function you can call that sanisitises the input before allowing it. While it comes with intelligent defaults, you can also customise exactly what you want to let through or not.

You can test out the basic server-side validation using a basic curl command. First, lets send a bad request and see what we get. In this case, were not sending any data at all:

curl --include --data-urlencode '' https://localhost/sign/

And we see that our validation works:

HTTP/1.1 403 Forbidden
Access-Control-Allow-Origin: *
Set-Cookie: sessionId=LbhwT0YqkVyAzQR0S-2KFGPa; Max-Age=28800000; Path=/; HttpOnly; Secure; SameSite=Strict
Date: Fri, 11 Aug 2023 12:36:23 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

(The --include flag is what tells curl to print out the response header we received.)

Finally, lets send a valid request and sign the guestbook from the command-line like proper nerds:

curl --include --data-urlencode 'message=From curl with love.' --data-urlencode 'name=Curl' https://localhost/sign/

This time, we get a much nicer response:

HTTP/1.1 303 See Other
Access-Control-Allow-Origin: *
Set-Cookie: sessionId=LMw_wcABaqRNWyhYmF7fvHFJ; Max-Age=28800000; Path=/; HttpOnly; Secure; SameSite=Strict
Location: /
Date: Fri, 11 Aug 2023 12:37:35 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

Its telling us that we should see the / route. So, lets. Go back to your browser and refresh the main page and you should see the guestbook entry from curl.

Multipart forms and file uploads

Kitten has high-level support for multi-part form handling and file uploads.

Uploads sent to POST routes via <input type='file'> in your pages are automatically saved in your projects uploads folder. Kitten automatically assigns them unique IDs and serves them from the /uploads/<unique-id> route. The Upload objects are also available to your POST routes in the request.uploads array.

💡 An upload object has the following properties:

.id           // Unique id. Used to look up uploads and calculate resource paths.
.fileName     // Name of the original file that was uploaded.
.filePath     // Absolute path to uploaded file on server.

.resourcePath // Relative URL resource path the upload can be downloaded from. 

.mimetype     // MIME type of file.
.field        // Name of file upload field in form that file was uploaded from.
.encoding     // Encoding of file.
.truncated    // Whether file was truncated or not (boolean).
.done         // Whether upload was successfully completed or not (boolean).

And the following method:

.delete()     // Deletes the upload.

A common idiom is to save the uploads unique ID (e.g., request.uploads[0].id), along with any other data in your form (e.g., the alt-text of an image upload), in your own database tables. Then, when you want to, say, render an uploaded image on a page, you can use the global kitten.upoads object to reference the upload you need and access its resource path.

e.g.,

kitten.html`
  <img src='${kitten.uploads.get(uploadId).resourcePath}' alt='…'>
`

💡 The kitten.uploads collection has the following methods:

.get(id)    // Returns Upload object with given ID (or undefined, if it doesnt exist).
.all()      // Returns array of all Upload objects.
.allIds()   // Returns array of strings of all Upload object IDs.
.delete(id) // Deletes object with given id (or fails silently if it doesnt exist).

Kitten can handle multiple file uploads as well as single ones.

💡Note that you must set the enctype='multipart/form-data' attribute on your forms for file uploads to work correctly.

The following basic example shows just how easy it is to handle file uploads in Kitten. In it, you can upload one image at a time along with its alt-text and displays them in a grid at the top of the page:

index.post.js

export default function (request, response) {
 request.upoads.forEach(upload => {
   kitten.db.images.push({
     path: upload.resourcePath,
     altText: request.body.altText ? request.body.altText : upload.fileName
   })
 })
 response.get('/')
}

index.page.js

if (!kitten.db.images) kitten.db.images = []

export default () => kitten.html`
<h2>Uploaded images</h2>

<if ${kitten.db.images.length === 0}><p>None yet.</p></if>

<ul>
  ${kitten.db.images.map(image => kitten.html`
    <img src=${image.path} alt=${image.altText}>
  `)}
</ul>

<h2>Upload an image</h2>

<form method='post' enctype='multipart/form-data'>
  <label for='image'>Image</label>
  <input type='file' name='image' accept='image/*'>
  <label for='alt-text'>Alt text</label>
  <input type='text' id='alt-text' name='altText'>
  <button type='submit'>Upload</button>
</form>

<style>
  body { max-width: 640px; margin: 0 auto; padding: 1em; font-family: sans-serif; }
  ul { padding: 0; display: grid; grid-template-columns: 1fr 1fr; }
  img { max-height: 30vh; margin: 1em; }
  input { width: 100%; margin: 1em 0; }
  button { padding: 0.25em 1em; display: block; margin: 0 auto; }
</style>
`

You can find the code for the above example in examples/file-uploads.

End-to-end encrypted Kitten Chat

So now youve learned how to use WebSockets, authenticated routes, HTTP routes, and how to carry out global tasks using a main.script.js file in Kitten. How about we put it all together and sprinkle some of Kittens built-in cryptography support to create an end-to-end encrypted version of the Kitten Chat example.

💡 This is just a basic example of implementing end-to-end encryption. It is not meant to be used in production or in real world situations for private communication.

🚧 Some of the elements you see in this example (like the remote message emitter and the means of retrieving the public keys of remote servers and delivering messages to them) will be implemented in a production-ready manner in Kitten itself soon. Once this happens, Ill either add a separate example that uses the built-in APIs or update this example to use them based on which I find more useful at the time.

Encryption and threat models

Nothing is entirely secure. Some things are secure enough.

In many situations, the best we can do is to raise the cost of surveillance to ensure that it is only used in specific cases (as opposed to mass surveillance).

End-to-end encryption is one of the means we have open to us for raising the cost of surveillance.

💡 Its important to understand your threat model.

Kittens security model does not protect you against targetted surveillance by determined adversaries that could compromise your server to install and run their own compromised version of your Kitten app.

Barring any potential vulnerabilities in Kitten, if you are hosting your app using a commercial web host, this means that they will have had to infiltrate your web host and install an app that can steal your secret. This would normally require either a determined person working at the web host or a state-level actor.

If youre hosting your app on your own hardware at a physical location you control, it would require the compromise of that location.

What end-to-end encryption mainly protects against is the opportunistic person at your web host being able to read your messages even if they compromise your machine.

Messages are already protected in transit via TLS. Their being end-to-end encrypted means that they are also protected at rest in the database on the server.

Since your secret remains on the client (the browser), implementing end-to-end encryption requires the use of JavaScript.

So, in this example, we will be making use of htmx and Alpine.jss event handling to encrypt and decrypt messages in the browser using JavaScript. The server will only ever see the encrypted text (or as we call it in cyptography, the ciphertext) and never the unencrypted text (or plaintext.)

Well start from where we left off in the Persisted Kitten Chat example.

Private vs public routes

In the Persisted Kitten Chat example, we hadnt implemented authentication and all our routes were public. Anyone could join the chat and, if they wanted to, even bypass our web interface and connect directly to our chat socket.

Needless to say, thats not something youd normally implement outside of a simple example for a tutorial.

For our end-to-end encrypted chat example, we need to decide which routes we keep public and which ones must be private.

To begin with, make a copy of the Persisted Kitten Chat example and lets create a directory that will require any route placed in it require authentication:

mkdir private🔒

💡 We saw earlier that we can use the built-in authentication system in Kitten to easily create private routes by appending them with the lock emoji (🔒).

Now, lets copy all the files that were previously public into our new private folder.

If you were to run the example now and hit https://localhost, youd get the 404 Kitten since we no longer have an index.page.js in the root of our project (everything is in the private folder). So lets add a simple index.html file in the root of our project with a link that takes us to the chat. This is a page anyone will be able to access:

index.html

<h1>Public site</h1>

<a href='/private'>End-to-end encrypted Kitten chat</a>

💡 Notice how we didnt have to add the lock emoji in the link. Kitten is clever enough to strip it off when creating its route patterns.

Now, were going to run Kitten a little differently, by explicitly specifying the domain instead of using localhost:

kitten --domain=place1.localhost

Were doing this because to test the end-to-end encrypted Kitten chat, we will need to fire up two different instances of the Kitten server so we can use them to chat. Since were testing locally, we need to use a subdomain that is an alias for localhost but will be treated as a different domain by browsers (this is to ensure that cookies are isolated between the instances as cookies are not isolated by port but by domain).

Kitten has built-in support for four localhost aliases to help you test the peer-to-peer features of Small Web apps (place1.localhost to place4.localhost).

💡 Subdomains on localhost Just Work (tm) on Linux but on macOS you have to manually edit your /etc/hosts file to map the subdomains to 127.0.0.1.

Once youve made the changes, your hosts file should resemble the one below:

##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost place1.localhost place2.localhost
255.255.255.255	broadcasthost
::1             localhost

Now, hit https://place1.localhost and you should see the link to the private chat section.

💡 When the server starts, Kitten will generate a new secret for you. Note this down in your password manager now.

Follow the link on the page and you should reach Kittens automatically-generated Sign In page at /sign-in.

Enter the secret you had saved in your password manager to sign in.

💡 Did you forget to note it down in your password manager? Thats OK. If you ever forget the secret for a project youre testing locally, you can follow the “Databases” link that Kitten displays when its run and delete the folder that holds the database for your project. The next time you run Kitten, it will recreate the password. Dont forget to note it down this time :)

You should now see the chat interface from before and the chat should function exactly as the Persisted Kitten Chat example did.

Now, lets change it so our messages are end-to-end encrypted.

private🔒/index.page.js

First, lets update our interface based on how we envision the chat to work when end-to-end encryption is implemented.

Starting with the interface and working inwards from there is whats known as “outside-in design.” It lets us concentrate on how our tool is going to be used first before we get bogged down on the implementation details.

The first change were going to make is to the <div> tag where the htmx socket is defined.

There, were going to have htmx prevent the wsConfigSend event and call our encryptMessage() handler so we can encrypt the message before it is sent. (We will then manually send the message ourselves.)

<div 
  id='chat'
  hx-ext='ws'
  ws-connect='/private/chat.socket'
  x-data='${kitten.js`{ showPlaceholder: true }`}'
  @htmx:ws-config-send.prevent='encryptMessage'
>

The only other change were going to make is to change the names and labels of the form elements.

Since our app will now enable people at different domains to chat to each other securely, we need to specify which domain we are sending our message to. And, finally, we rename the message itself from text to plainText to make it very clear that this is the message before it is encrypted.

<form id='message-form' ws-send >
  <label for='domain'>To:</label>
  <input id='domain' name='domain' type='text' required />

  <label for='plainText'>Message:</label>
  <input id='plainText' name='plainText' type='text' required
    
  />
  
</form>

The rest of the page stays the same.

Well look at how we implement the encryptMessage() function later but first, weve only specified when we should encrypt messages. We havent specified when we should decrypt them.

So lets think about that next.

private🔒/Message.component.js

We mentioned earlier that the server only ever sees the ciphertext and never the plaintext. So we know that both encryption and decryption must take place on the client (in the browser) using client-side JavaScript.

We also know that, given how htmx works, the chat socket sends new messages as HTML snippets to the client.

Finally, we know that if we want to run custom JavaScript, we can use Alpine.js to do so declaratively.

So, lets combine all this and take a look at how we must modify the Message.component so that messages are automatically decrypted as they load in the browser:

export default function Message ({ message }) {
  return kitten.html`
    <li
      x-data='{
        messageText: "${kitten.sanitise(message.cipherText)}"
      }'
      x-init='$nextTick(() => messageText = decryptMessage("${kitten.sanitise(message.cipherText)}", "${kitten.sanitise(message.from)}", "${kitten.sanitise(message.to)}"))'
    >
      <strong>${kitten.sanitise(message.from)}${kitten.sanitise(message.to)}</strong> <span x-text='messageText'>${kitten.sanitise(message.cipherText)}</span>
    </li>
  `
}

What were doing here is using Alpine.jss x-data directive to set up our data model for the list item node. In it, we populate a property called messageText with our messages ciphertext.

Then, we write up the x-init directive so that when the node is initialised, we call a function called decryptMessage(), passing it our ciphertext as well the domains that are the sender and receiver of our message.

💡 Notice that the x-init handler is wrapped in a call for Alpine.jss magic $nextTick property. This makes Alpine.js wait until DOM updates are finished so we dont call the decryption handler before it has had a chance to load.

So whats happening here is that the ciphertext is sent from the server to the client and, before the message is displayed, it is decrypted on the client and the plaintext is shown in the interface.

Now, lets actually add the encryptMessage() and decryptMessage() function implementations to the client-side script imported by our page.

private🔒/index.js

Our functions will make use of the built-in cryptography functions in the Kitten Cryptography API. This is a library Kitten serves at runtime from the route /🐱/library/crypto-1.js:

import { encryptMessageForDomain, decryptMessageFromDomain } from '/🐱/library/crypto-1.js'

globalThis.encryptMessage = async function (event) {
  // Encrypt the plain text and send that in the message to the server.
  const parameters = event.detail.parameters
  const ourPrivateKey = localStorage.getItem('secret')
  const encryptedMessage = await encryptMessageForDomain(parameters.plainText, ourPrivateKey, parameters.domain)
  event.detail.socketWrapper.send(
    JSON.stringify({
      cipherText: encryptedMessage,
      from: window.location.host,
      to: parameters.domain
    }),
    event.detail.elt // elt = DOM element
  )
}

globalThis.decryptMessage = async function (cipherText, fromDomain, toDomain) {
  if (fromDomain === window.location.host) {
    // This is a message we sent. Since the shared secret is commutative (g^jk === g^kj, or, in other words,
    // can either be our private key and their public key or vice-versa), we just flip the from/to domains :)
    fromDomain = toDomain
  }
  const ourPrivateKey = localStorage.getItem('secret')
  return await decryptMessageFromDomain(cipherText, ourPrivateKey, fromDomain)
}

Encryption

Remember that the encryptMessage() function is called by htmx before a message is sent over the socket to the server. (Specifically, when the htmx:wsConfigSend event fires.)

Based on the htmx documentation, we know that the event argument will contain a detail property, which, itself, will contain a parameters object with the form data, an elt property that holds a reference to the DOM node that holds the socket, and a socketWrapper that has a send() method we can use to manually send the message after weve encrypted it.

So we:

  1. Retrieve our secret key from local storage (this was automatically saved there for us by Kitten when we signed in.)

  2. Call the encryptMessageForDomain() function we imported from Kittens cryptography library and pass it the plainText and domain from the form as well as our secret (or private key as its known in cryptography).

  3. Finally, manually create a JSON message that contains the encrypted text (cipherText) as well as the from and to properties that address the sender (our domain, which we get from window.location.host so it includes our port number, which is important when testing locally) and the receiver (the remote domain, which we manually enter into the Domain: textbox in the interface).

Decryption

Similarly, remember that the decryptMessage() function is called by Alpine.jss x-init directive when a new Message component is received from the server via the WebSocket. And we saw in the Message.component that we pass in the ciphertext, sender, and receiver as arguments.

So all we do here is to retrieve our secret (private key) from local storage again, check to see if were the sender and, if so, take advantage of the associativity property of Diffie-Hellman shared secrets to swap the receiver and sender domains thereby eventually resulting in the decryption of the message using our own public key.

To understand that last bit more fully, lets also take a look at the encryptMessageForDomain() and decryptMessageFromDomain() functions in the Kitten Cryptography API.

Kitten Cryptography API

The Kitten Cryptography API (which you can find in /src/lib/crypto.js) contains, among other things, high-level functions for encrypting and decrypting messages sent between Small Web domains:

const textDecoder = new TextDecoder() // default: 'utf-8'

export async function encryptMessageForDomain (message, ourPrivateKey, domain) {
  const sharedSecret = await sharedSecretForDomain(domain, ourPrivateKey)
  const encryptedMessageBytes = await encrypt(sharedSecret, message)
  const encryptedMessage = bytesToHex(encryptedMessageBytes)
  return encryptedMessage
}

export async function decryptMessageFromDomain (encryptedMessage, ourPrivateKey, domain) {
  const sharedSecret = await sharedSecretForDomain(domain, ourPrivateKey)
  const decryptedMessageUInt8Array = await decrypt(sharedSecret, hexToBytes(encryptedMessage))
  const decryptedMessageUtf8String = textDecoder.decode(decryptedMessageUInt8Array)
  return decryptedMessageUtf8String
}

Both these methods are fairly spartan and rely on the sharedSecretForDomain() function to carry out the heavy lifting:

// Cache shared secrets for different domains as theyre
// expensive to calculate.
const sharedSecrets = {}

async function sharedSecretForDomain (domain, ourPrivateKey) {
  if (sharedSecrets[domain] === undefined) {
    // We dont have a shared secret yet. Attempt to calculate one
    // by getting the other domains ed25519 public key.
    const domainToContact = `https://${domain}/💕/id`
    const theirPublicKeyResponse = await fetch(domainToContact)
    const theirPublicKeyHex = await theirPublicKeyResponse.text()
    const ourPrivateKeyHex = bytesToHex(emojiStringToSecret(ourPrivateKey))
    sharedSecrets[domain] = await getSharedSecret(ourPrivateKeyHex, theirPublicKeyHex)
  }
  return sharedSecrets[domain]
}

The sharedSecretForDomain() function is where the Diffie-Hellman key exchange happens and the shared secret thats used to encrypt and decrypt messages between a pair of Small Web domains is calculated.

As part of the Small Web Protocol, every Small Web site serves its ed25519 public key at the /💕/id route.

💡 The Small Web Protocol requires Small Web routes to be namespaced under the 💕 path. This also happens to be the Small Web logo.

So the first thing we do is get this for the domain we want to send the message to.

Then we combine that with out private key to calculate the shared secret.

The actual calculation of the shared secret uses the getSharedSecret() function from Paul Millers noble-ed25519 library.

💡 The Kitten Cryptography API also makes extensive use of Pauls ed25519-keygen library.

Once we have the shared secret, the actual encryption and decryption are handled by the encrypt() and decrypt() functions from Pauls micro-aes-gcm library, which itself uses low-level cryptographic primitives from the Web Crypto API.

💡 AES-GCM stands for Advanced Encryption Standard Galois/Counter Mode but all you really need to know is that Paul and the folks who implemented the Web Crypto API in your browser have done the hard work of implementing the cryptography and all you need to do is to use the high-level encryptMessageForDomain() and decryptMessageFromDomain() functions exposed by the Kitten Cryptography API when creating end-to-end-encrypted messages to send between peer-to-peer Small Web places.

Also note that while the Kitten Cryptography API is available on both the server and the client, the encryption and decryption functions are only meant to be run in the browser. In fact, under the current Kitten runtime (Node version 18 LTS), they will cause a runtime error as the global crypto object that exposes the Web Cryptography API is not present. It can be easily polyfilled but isnt on purpose as you shouldnt be using it. When Kitten moves onto a runtime thats version 19+, the calls wont fail but you still shouldnt be using them on the server.

At this point, we have made all the changes we need to on the client side but we still need to handle messages differently on the server. So, next, lets take a look at the server-side code.

/private🔒/chat.socket.js

import Message from './Message.component.js'

if (kitten.db.messages === undefined) kitten.db.messages = [];

function messageHtml (message) {
  // Wrap the same Message component we use in the page
  // with a node instructing htmx to add this node to
  // the end of the messages list on the page.
  return kitten.html`
    <div hx-swap-oob="beforeend:#messages">
      <${Message} message=${message} />
    </div>
  `
}

// Kitten Chat example back-end.
export default function (socket, request) {
  const saveAndBroadcastMessage = message => {
    // Persist the message in the messages table.
    kitten.db.messages.push(message)

    // Since we are not optimistically showing messages
    // as sent on the client, we send to all() local clients, including
    // the one that sent the message. If we were optimistically
    // updating the messages list, we would use the broadcast()
    // method on the socket instead.
    const numberOfRecipients = socket.all(messageHtml(message))

    // Log the number of recipients message was sent to
    // and make sure we pluralise the log message properly.
    console.log(`🫧 Kitten ${request.originalUrl} message from ${message.from} to ${message.to} broadcast to `
    + `${numberOfRecipients} recipient`
    + `${numberOfRecipients === 1 ? '' : 's'}.`)
  }

  // Handle remote messages.
  const remoteMessageHandler = message => {
    if (!isValidMessage(message)) {
      console.warn(`Message from remote place is invalid; not saving or broadcasting to local place.`)
      return
    }
    saveAndBroadcastMessage(message)
  }
  kitten.events.on('message', remoteMessageHandler)

  socket.addEventListener('close', () => {
    // Housekeeping: stop listening for remote message events
    // when the socket is closed so we dont end up processing
    // them multiple times when the client disconnects/reconnects.
    kitten.events.off('message', remoteMessageHandler)
  })

  socket.addEventListener('message', async event => {
    // A new message has been received from a client connected to
    // our own Small Web place: broadcast it to all clients
    // in the same room and deliver it to the remote Small Web place
    // it is being sent to after performing basic validation.

    const message = JSON.parse(event.data)

    if (!isValidMessage(message)) {
      console.warn(`Message from local place is invalid; not saving or delivering to remote place.`)
      return
    }

    // We dont need to use the message HEADERS, delete them so
    // they dont take unnecessary space when we persist the message.
    delete message.HEADERS

    saveAndBroadcastMessage(message)

    // Deliver message to remote places inbox.
    // Note: we are not doing any error handling here.
    // In a real-world application, we would be and wed be
    // alerting the person if their message couldnt be 
    // delivered, etc., and giving them the option to retry.

    const body = JSON.stringify(message)
    const requestOptions = {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body 
    }

    const inbox = `https://${message.to}/inbox`
    try {
      await fetch(inbox, requestOptions)
    } catch (error) {
      console.error(`Could not deliver message to ${message.to}`, error)
    }
  })
}

// Some basic validation.
// …

There are two major changes here so lets examine them separately.

  1. When we receive a message from a local client, we must not only save it in the database but also deliver it to the remote node.

  2. We must also have a way of knowing when a message has been delivered to us so we can both save it in our database and broadcast it to all connected local clients.

We handle the first requirement by making a POST call to the remote servers /inbox route:

const body = JSON.stringify(message)
const requestOptions = {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body 
}

const inbox = `https://${message.to}/inbox`
try {
  await fetch(inbox, requestOptions)
} catch (error) {
  console.error(`Could not deliver message to ${message.to}`, error)
}

We will see what the /inbox route looks like when we implement it next.

But for now, lets also look at how we statisfy the second requirement:

// Handle remote messages.
const remoteMessageHandler = message => {
  if (!isValidMessage(message)) {
    console.warn(`Message from remote place is invalid; not saving or broadcasting to local place.`)
    return
  }
  saveAndBroadcastMessage(message)
}

kitten.events.on('message', remoteMessageHandler)

socket.addEventListener('close', () => {
  // Housekeeping: stop listening for remote message events
  // when the socket is closed so we dont end up processing
  // them multiple times when the client disconnects/reconnects.
  kitten.events.off('message', remoteMessageHandler)
})

Here we listen tomessage events on a global Kitten object called Events that we havent seen before.

Events is just global instance of Nodes EventEmitter thats made available to you for ease of authoring.

When we hear that a message has been received, we save it before broadcasting it to all local clients. And we make sure we stop listening for the event when the socket is closed so that we dont handle messages multiple times in case clients disconnect and reconnect.

At this point, we have just one thing left to create: the /inbox route where other places can send us messages.

So lets build that now.

/inbox.post.js

The /inbox route, as we saw earlier in the code that sends messages to it, is a POST route. Also notice that it is not in our private directory. Any other Small Web place should be able to send a message to our inbox so it must be public.

The code for it couldnt be simpler:

export default function (request, response) {
  const message = request.body
  console.log('📥 /inbox received:', message)
  kitten.events.emit('message', message)
  response.end('ok')
}

All were doing is getting the message from the body of the request (which Kitten automatically parses from JSON for us) and then using Kittens convenient global EventEmitter instance, Events, to dispatch a message event.

💡 Again, this is a basic example. In a real-world scenario, you would carry out further validation on received message.

Testing peer-to-peer Small Web features

To test the end-to-end encrypted Kitten chat example, you have to run more than one instance of it. So open two Terminal windows (or tabs or panes within your Terminal app) and run two instances of the example:

Terminal 1

kitten --domain=place1.localhost

Terminal 2

kitten --domain=place2.localhost --port=444

💡 Notice that you have to use not just a different domain but, when testing locally, also a different port.

Now, open https://place1.localhost/private in one browser window and https://place2.localhost:444/private in another.

In each one, enter the address of the other in the Domain: field (without the https:// prefix) and send a few messages between them.

Remember that when running in deployment from a domain name, these places would be owned by different people and reside on different computers around the world.

💡 If you want a little respite from all the cryptography stuff and give yourself a little reward, check out the Animated End-to-End Encrypted Kitten Chat project in the examples folder ;)

Deploying a Kitten application

😻 Kitten periodically and automatically checks for updates to your project on a daily basis and makes sure your deployment is up-to-date. (This is especially important if youre deploying someone elses app to your own server. It means you dont have to worry about maintaining it.) If you do want to immediately update a specific deployment, 🚧 you can do so from the /💕/update route. Also, if you want to update a specific deployment whenever you deploy to your git repository, 🚧 you can set up a special git hook to do so. Finally, you can also update Kitten itself from the CLI by running the kitten update command.

To deploy your app as a service, you use the deploy command:

kitten deploy <httpsGitCloneURL>

For example, the following will create and run a production server of the Persisted Kitten Chat example, as hosted on my personal Codeberg account at my hostname.

kitten deploy https://codeberg.org/aral/persisted-kitten-chat

kitten.json

Before you deploy, you must make sure your app has a kitten.json file in its root directory and that in that file you specify the Kitten API version that your app supports and has been tested with by specifying it via the apiVersion key.

For example:

{
  "apiVersion": 1
}

Kitten productions servers periodically and automatically check for updates to both Kitten itself and to the Kitten app that is being served. Whenever there is a breaking change to the Kitten API (e.g., an existing method is removed or how its used is changed in such a manner that it could break existing code), the change is documented in the change log and the Kitten API version, which is a monotonically increasing integer, is incremented by one.

To see how it works, imagine that you deploy a new server running a Kitten version that has its API version set to 1. You specify this API version in your kitten.json file as shown above. Whenever Kitten checks for Kitten updates, it ensures that the API version is still 1 before updating Kitten. Say that after three Kitten updates, Kitten notices that the latest version of Kitten has an API version of 2 but that your app (which Kitten is also periodically checking for updates for from its git repository) is still at API version 1. At that point, Kitten does not install the latest version of Kitten but rather waits for you to test your app with Kitten API version 2 and specify that it is supported by updating your kitten.json file accordingly.

If there are any other Kitten packages released that support Kitten API version 1 (e.g., bug/security fixes that are backported from the latest versions of Kitten), Kitten will continue to update itself to those versions.

Once Kitten sees that youve updated your app to Kitten API version 2, it will go ahead and update itself to the latest package supported by that version.

So, all this to say that Kitten is smart about ensuring that its auto-updates system does not break your servers.

🪤 If you are using a database in your app, please remember that you are responsible for ensuring that schema changes do not break your app between versions. In other words, you are responsible for implementing migrations. To make this easier on yourself, you might want to ensure that the collections and individual data items you persist are custom classes that abstract away the actual objects that are persisted. That way, you can implement granular migrations in your model classes. Otherwise, you can, of course, also implement global, one-shot migrations in the traditional manner.

💡 Production servers require systemd.

💡 Kitten will not automatically create an alias for www for you, pass --aliases=www.<hostname> (or the shorthand, --aliases=www) if you want that. You can, of course, also list any other subdomains other that www that you want Kitten to serve your site/app on in addition to the main domain.

😻 If your app uses node modules, Kitten will intelligently call npm install on your project, as well as on any app modules your project might have.

🌲 Evergreen Web (404 → 307)

Kitten has built-in support for the Evergreen Web (404 to 307) technique.

To implement 404 to 307 redirection for your Small Web place in Kitten, simply set domain you want to forward to in your Small Web Settings page which you can reach in your browser at /💕/settings/.

Lets say you are deploying your new Small Web place at your own domain (e.g., https://ar.al) but you already have a personal web site or a blog there. You will already have content that other people might have linked to and/or rely on. It would be a shame for all those links to break when you deploy your new site.

The Evergreen Web (404 to 307) technique is very simple:

On your new site, issue a 307 redirect for any paths that result in a 404 (not found) error to the previous version of your site. That way, if the path existed in the previous version of your site, it will be found there and, if it did not, it will result in a 404 error there.

💡 Of course, youre not limited to one level of indirection. If the previous version of your site replaced an even earlier version, theres no reason why you cannot add a 404 to 307 rule there also and have a chain of redirects going all the way back to the earliest version of your site.

One of the great advantages of this technique is that you can just leave older versions of your site running (perhaps at different subdomains) and they can all continue to exist using whatever technologies they were using at the time. (You dont have to take static exports, etc., of the data.)

By using 404 to 307, you will contribute to an evergreen web by not breaking URLs.

To see it in action, run the evergreen-web example.

For more information, see 4042307.org.

Valid file types

Kitten doesnt force you to put different types of routes into predefined folders. Instead, it uses file extensions to know how to handle different routes and other code and assets.

Here is a list of the main file types Kitten handles and how it handles them:

Extension Type Behaviour
.page.js Kitten page Compiled into HTML and served in response to a HTTP GET request for the specified path.
.post.js, .get.js, .put.js, .head.js, .patch.js, .options.js, .connect.js, .delete.js, .trace.js HTTP route Served in response to an HTTP request for the specified method and path.
.socket.js WebSocket route Served in response to a WebSocket request for the specified path.
.component.js A component file, returns HTML Ignored by router.
.layout.js Layout component, returns HTML Ignored by router.
.fragment.js A fragment file, returns HTML Ignored by router.
.script.js Server-side script file Ignored by router. Useful for including server-side JavaScript modules. File is not accessible from the client.
.styles.js Server-side styles file Ignored by router. Useful for including server-side CSS (in JS). File is not accessible from the client.
Other (.html, .css, .js, .jpg, .gif, etc.) Static files Any other files in your project apart from the ones listed above are served as static files.

HTTP routes

HTTP data routes are served in response to an HTTP request for the specified method and path.

All HTTP request methods are supported.

You create an HTTP route by create a JavaScript file named with the HTTP request method you want to respond to.

For example, to respond to GET requests at /books, you would create a file named books.get.js in the root of your source folder.

The content of HTTP routes is an ESM module that exports a standard Node route request handler that takes http.IncomingMessage and http.ServerResponse arguments.

For example, your books.get.js route might look like this:

export default (request, response) => {
  const books = kitten.db.books.get()
  response.end(books)
}

URL normalisation

Kitten automatically normalises URLs that do not have a trailing slash when they should to add one.

It does so using a permanent 308 redirect that preserves the method of the request.

This means, for example, that a request to https://my.site/hello will be forwarded to https://my.site/hello/.

This is both to help implement canonical paths for your sites/apps and to avoid the unexpected situation of a relative include failing from your page if a slash is not provided in the path. (e.g., if you have an index.js file and an index.html file in a folder called hello and you include the JavaScript file using <script src='./index.js'></script>, that will succeed if the page is hit as /hello/ but fail if it is hit as /hello.)

Database

Kitten has an integrated JSDB database thats available from all your routes as db.

JSDB is a transparent, in-memory, streaming write-on-update JavaScript database for the Small Web that persists to a JavaScript transaction log.

You can find the databases for your projects in the ~/.small-tech.org/kitten/database folder. Each project gets its own folder in there with a name based on the absolute path to your project on your disk (e.g., if a Kitten project is stored in /var/home/aral/projects/my-project, its database will be in a folder named var.home.aral.projects.my-project in the main database folder.)

Tables in JSDB are simply JavaScript objects or arrays and JSDB writes to plain old JavaScript files.

You can get information about the database and specific tables, delete the whole database and specific tables, and tail specific tables using the command-line interface via the db info [tableName] (or just db [tableName], which is a convenience alias), db delete [tableName] and db tail <tableName> commands.

Learn more about JSDB.

Route parameters

You can include route parameters in your route paths by separating them with underscores and surrounding the parameter names in square brackets.

For example:

manage_[token]_[domain].socket.js

Will create a WebSocket endpoint at:

/manage/:token/:domain

You can also intersperse path fragments with parameters:

books_[id]_pages_[page].page.js

Will compile the Kitten page and make it available for HTTP GET requests at:

/books/:id/pages/:page

So you can access the route via, say, https://my.site/books/3/pages/10.

You can also specify the same routes using folder structures. For example, the following directory structure will result in the same route as above:

my-site
  ╰ books
     ╰ [id]
         ╰ pages
             ╰ [page].page.js

Note that you could also have set the name of the page to index[page].page.js_. Using just [page].page.js for a parameterised index page is a shorthand.

You can decide which strategy to follow based on the structure of your app. If, for example, you could access not just the pages but the references and images of a book, it might make sense to use a folder structure:

my-site
  ╰ books
     ╰ [id]
         ├ pages
         │   ╰ [page].page.js
         ├ references
         │   ╰ [reference].page.js
         ╰ images
             ╰ [image].page.js

You may, or may not find that easier to manage than:

my-site
  ├ books_[id]_pages_[page].page.js
  ├ books_[id]_references_[reference].page.js
  ╰ books_[id]_images_[image].page.js

Kitten leaves the decision up to you.

Optional parameters

To create an optional parameter, prefix your parameter name with optional- inside the square brackets.

For example, the following route:

delete
  ╰ index_[subdomain]_[optional-return-page].page.js

Will match both of the following paths:

  • /delete/my-lovely-subdomain/
  • /delete/my-other-subdomain/troubleshooting/

The wildcard parameter

Finally, you can also use the special wildcard parameter to match any string:

my-site
 ╰ books_[any].page.js

The about route will match any path that begins with /books/.

💡 Under the hood, Kitten uses Polka for its routing and translates Kittens file system-based naming syntax to Polkas routing syntax.

App Modules

When working on larger projects, you might end up with pages at varying levels within the site, all of which need to use the same global resource (for example, a layout template or a utility class).

In these situations, your import statements might start to get unruly.

Take the following example:

my-site
  ├ site.layout.js 
  ╰ books
     ╰ the-handmaids-tail
         ╰ pages
             ╰ cover.page.js   

Lets say your cover.page.js uses the site layout template.

Heres what its import statement would look like:

import Site from '../../../site.layout.js'

export default () => kitten.html`
  <${Site}>
  </>
`

Thats both cumbersome to write and error-prone. (Especially when you consider that different pages at different levels of the hierarchy may want to use the same file. You dont want to spend your day counting dots.)

Enter App Modules.

App Modules are special local Node modules that exist only in your app (not on npm). They live in the special app_modules folder in your project and Kitten knows to ignore them when calculating its routes from the file structure of your project.

App Modules, like all Node modules, need a package.json file but they can get away with specifying a subset of the information contained in ones for npm packages. And, like all Node modules, they need to be npm installed into your project (but from a local file path instead of from npmjs.org).

Finally, if you have type checking enabled in your projects, you should add an index.d.ts file to your App Modules so your editor doesnt complain when using the TypeScript Language Server (LSP).

Heres how youd make your site layout into an App Module:

  1. Create the app_modules directory and then a directory for your module:

    mkdir -p app_modules/site-layout
    
  2. Add the files your module will need:

    my-site
      ├ app_modules
      │  ╰ site-layout 
      │     ├ site.layout.js
      │     ├ index.d.ts
      │     ╰ package.json
      ╰ books
         ╰ the-handmaids-tail
             ╰ pages
                 ╰ cover.page.js  
    
  3. In your package.json file, you need to specify three properties: name, type, and main:

    {
      "name": "@app/site-layout",
      "type": "module",
      "main": "site.layout.js"
    }
    
  4. Finally, if you have type checking enabled and you want to avoid type errors in your editor, your index.d.ts file should look like this:

    import SiteLayout from './site.layout.js'
    export default SiteLayout
    

And thats it.

All you need to do then is to install the App Module.

From the root of your project, install your module using its local path:

npm install ./app_modules/site-layout

🪤 You should keep App Modules as simple as possible and they should not have their own dependencies. (Were using them to simplify authoring, not to create a monorepo.) That said, if you do end up having dependencies for your App Modules, remember to npm install them separately from within their own folders as npm will not automagically do that for you.

Finally, from cover.page.js (and any other page, anywhere in your project) you can now import your layout using:

import Site from '@app/site-layout'

Thats much better, isnt it?

For a real world example, see the database app module in Domain that wraps Kittens global JSDB instance with type information and some utility methods.

Static files

You do not have to do anything special to server static files with Kitten. It is, first and foremost, a web server, after all. Any file thats in your project folder that isnt a hidden file or in a hidden folder and isnt a special Kitten file (e.g., .page.js, .socket.js, .post.js, etc.), will be served as a static file.

Reserved routes

Kitten tries to be as minimally invasive into your site or apps own namespace as possible. That said, there are certain routes that it does reserve. Some of these are part of the Small Web protocol and others are Kitten-specific.

Reserved Small Web protocol routes

The following routes are reserved as part of the Small Web protocol, which is namespaced by the Small Web emoji/logo (💕).

They are used to implement public-key authentication and manage the communication between Small Web instances and the Domain hosts that host them:

  • /💕/id

  • /💕/id/ssh

  • /💕/settings/domain (authenticated route)

  • /💕/sign-in

  • /💕/sign-out

Reserved Kitten-specific routes

Any other private Kitten-specific routes are kept in the 🐱 namespace so as not to pollute your app/sites own URI space.

Kitten runtime libraries

Kitten comes with several runtime libraries that you can use in your sites/apps. Some of these have high-level interfaces for including them (see Page Routes) but you can include any of them in your sites/apps manually by knowning their paths:

In /🐱/library/:

  • htmx: htmx-1.js.gz

  • htmx WebSocket: htmx-ws-1.js

  • Alpine.js: hyAlpine.js.js.gz

  • prism (CSS): prism-1.css

  • Water (CSS): water-2.css

The -N notation specifies the major version of the library. Since Kitten sites automatically update, if there is a major version update to a library, it will be adding alongside previous versions so as not to break existing sites. So if htmx 2.x.x comes out, for example, with breaking changes and we start supporting it, it will be added as htmx-2.js. All minor and patch updates will be automatically updated without changing the name of the include.

Command-line interface (CLI)

💡 Use kitten help to access command information from your terminal.

To get detailed help on a specific command, use kitten help <command name>. For example, kitten help serve.

You can also pass the --help or -h option to any command to get more detailed information about it. So kitten serve --help is equivalent to kitten help serve.

Default command

serve

kitten [path to serve] [options]
kitten serve [path to serve] [options]

Serves a Kitten place.

💡 If do not specify a path to serve, the default directory (./) is assumed.

Options
  • --domain : Main domain name to serve site at. Defaults to hostname in production, localhost during development.
  • --aliases : Additional domains to respond to (each gets a TLS cert and 302 redirects to main domain). e.g., www.
  • --port : The port to create the serer on. Defaults to 443.
  • --working-directory: The working directory Kitten was launched from. Defaults to current directory (default .)

💡 When running multiple Kitten instances on different ports to test peer-to-peer functionality during development, remember to use different domains for each instance (for example, localhost for one and 127.0.0.1:444 for the other) to ensure that sessions work correctly. This is because Kitten sessions make sure of cookies and cookies do not provide isolation by port. There is currently an issue open on Auto Encrypt Localhost to add IPs 127.0.0.2 - 127.0.0.4 to generated certificates so you can test up to four peers via the 127.0.0.1 - 127.0.0.4 loopback addresses (five, if you add localhost). If you have a need for more perhaps for some sort of automated testing please open an issue here, explain your use case, and well consider adding more.

Database commands

db (alias for db info)

kitten db [table name]
kitten db info [table name]

Shows database and table information.

db delete

kitten db delete [table name]

Deletes either the whole database or the table with the specified table name after asking you for confirmation.

db tail

kitten db tail <table name>

Shows a summary of the head (start) and tail (end) of your database table (remember that database tables in JSDB are append-only JavaScript logs) and starts to follow additions to it.

Press Ctrl C to exit as you would a regular shell tail command.

Deployment of Kitten applications

deploy

kitten deploy <git HTTPS clone URL> [options]

Creates a Kitten place by deploying a Kitten project from a git repository as a systemd service.

Options
  • --domain : Main domain name to serve site at. Defaults to hostname in production, localhost during development.
  • --aliases : Additional domains to respond to (each gets a TLS cert and 302 redirects to main domain). e.g., www.

Kitten daemon (systemd service) commands

status

kitten status

Shows Kitten systemd service status.

logs

kitten logs [options]

Tails (follows) the Kitten systemd service logs using journald.

Options
  • --since: Time span to show logs for. For example, 6 hours ago, yesterday, etc. (defaults to 1 hour ago).

start

kitten start

Starts the Kitten systemd service.

stop

kitten stop

Stops the Kitten systemd service.

enable

kitten enable

Enables the Kitten systemd service (so it auto starts on boot and if the process exits)

disable

kitten disable

Disables the Kitten systemd service (so it no longer auto starts on boot, etc.)

General commands

version (aliases: --version and -v)

kitten version
kitten --version
kitten -v

Displays the Kitten version, made up of:

  • Date of the build (the Kittens birthday)
  • The git commit hash of the build (as represented by the RGB colour equivalent of the hex value; the Kittens favourite colour)
  • The API version (the major semver component from package.json.)

🚧 Eventually, you will be able to tell Kitten what API version your app is written for and it will only auto-update to the latest release in that API version.

help (aliases: --help and -h)

kitten help
kitten help <command name>
kitten <command name> --help
kitten <command name> -h

Use kitten help to access command information from your terminal.

To get detailed help on a specific command, use kitten help <command name>. For example, kitten help serve.

You can also pass the --help or -h option to any command to get more detailed information about it. So kitten serve --help is equivalent to kitten help serve.

update

kitten update
kitten update <API version>

Use kitten update to update (upgrade or downgrade) your currently installed version of Kitten.

When you run the command without supplying the optional API version argument, Kitten will attempt to update to the more recent publicly-available Kitten package from kittens.small-web.org. If your version of Kitten is more recent than the most recent publicly-available Kitten package, Kitten will prompt you to ask if you want to downgrade your installation.

If you supply the optional API version argument, the update will check for the latest version for the given API version. If such an API version does not exist, you will get an error. If your version of Kitten is already on that API version but is a more recent build, you will be prompted whether you want to downgrade your installation.

💡 The only time the downgrade prompt should show is if youre running a local development build that is more recent that the most recent publicly-available Kitten package. This is useful if you want to quickly test a publicly-available package during development. You can, of course, always install the latest development version by running the ./install script as usual.

uninstall

kitten uninstall

Uninstalls Kittens and removes all Kitten data after asking for confirmation.

Install Kitten on Windows under WSL 2

Kitten runs on Windows under WSL with the following caveats:

  1. Requires WSL 2 (will not work with WSL 1).
  2. You must manually install Kittens local development-time certificate authority in your Windows browsers.

If you havent installed Kitten on Windows or used WSL 2 before, the instructions, below, take you through the whole process.

Instructions

  1. Open up a Windows Powershell tab in Terminal App.

    🪤 Windows comes with a couple of terminal apps and shells. Make sure you use the exact pair mentioned above.

  2. Install WSL 2:

    wsl --install
    

    This will install WSL 2 and Ubuntu. It will ask you to choose an account name and set a password and then you will be running Ubuntu under WSL2.

    Kitten only works with WSL 2 using Windows Terminal and Windows Powershell (the sysctl command used by the installer fails on systems with WSL 1).

    To make sure your Linux container is running under WSL 2 (and not 1) before continuing, run:

    uname -a
    

    On my system, that gives me the following system information:

    Linux aral-win11 5.15.90.1-microsoft-standard-WSL2 #1 SMP Fri Jan 27 02:56:13 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
    

    Note that the string WSL2 is contained in the output. If you dont see that, youll have to either create a new container using WSL 2 or update your current container to use WSL 2.

    💡 For best results, please make sure youre running the latest Ubuntu version thats installed automatically by wsl when you run the wsl --install command.

    (If youre running a very old version of Ubuntu, it may not have the new Lets Encrypt certificate authority in its trust store and the Kitten installer may fail with a certificate error. If this happens, please either upgrade your version of Ubuntu or try tunning sudo dpkg-reconfigure ca-certificates to download the latest list of trusted certificate authorities into the system store.)

  3. Install Kitten:

    wget -qO- https://codeberg.org/kitten/app/raw/install | bash
    

    (Enter the password you set up for your Ubuntu installation under WSL 2 when asked.)

  4. Source your .profile to add Kittens binary to your path:

    source .profile
    

    (Kitten is now installed properly but Ubuntu doesnt add the ~/.local/bin folder that Kitten puts its binary into to your system path until that path exists on your computer. Instead of sourcing the .profile, you can also log out and log back in.)

  5. Test the Kitten binary.

    kitten --version
    

    You should see Kitten launch and the version get displayed.

    💡If Kitten fails to launch, please open an issue and try to give as much information about what went wrong as you can. Copy and paste any error messages you see. Also run uname -a and lsb_release --all and paste the output from those commands into your issue.

  6. Create your first project and run Kitten.

    ❣️ Dont skip this step! The first time Kitten runs, it creates your local development-time certificate authority (CA) and TLS certificate. You will need the CA to install in your Windows browsers in the next section.

    mkdir hello-kitten
    cd hello-kitten
    echo 'Hello, Kitten!' > index.html
    kitten
    

    Kitten should launch and start serving your site.

    However, if you open a web browser under Windows (e.g., the default Edge browser), you will see a NET::ERR_CERT_AUTHORITY_INVALID security error warning you that your connection isn't private.

    This is because Kitten is running under Ubuntu and your browser is running under Windows. While Kitten can (and does) update your Linux system trust store to accept its local development certificates, it cannot do that for your Windows machine because it doesnt even know that it exists.

    So, on Windows, you have to manually install Kittens certificate authority in your Windows browsers.

    The next section takes you through doing that.

Manually install the certificate authority in your Windows browsers.

For browsers you have installed under Windows to accept the TLS certificates created by Kitten, you must install its certificate authority into your browsers. (Remember that Kitten is running under Linux and doesnt know anything about your Windows environment so it cant do it for you.)

For example, if youre using Microsoft Edge:

  1. Go to edge://settings/privacy, scroll down to the Security section, and select “Manage certificates.”

    Screenshot of Settings page in Microsoft Edge.

  2. In the resulting “Certificates” modal, select the Trusted Root Certificate Authorities tab and press the Import… button.

    Screenshot of the Certificates modal showing the empty Personal tab selected

  3. In the resulting Certificate Import Wizard, press Next to pass from the welcome screen to the File to Import step and press the Browse… button.

    Screenshot of the File to Import stage of the Certificate Import Wizard with an empty File name textbox and the Browse… button mentioned in the text.

  4. Make sure the file extensions drop-down is set to show all extensions and select Linux → Ubuntu → home → (your home directory) → .local → share → small-tech.org → kitten → tls → local → auto-encrypt-localhost-CA.pem.

    Screenshot of the file dialogue showing the auto-encrypt-localhost-CA.pem certificate selected.

  5. In the next step, make sure “Place all certificates in the following store” is selected with “Trusted Root Certification Authorities” set.

    Screenshot showing the Certificate Import Wizard with the settings mentioned in the step applied.

  6. Press Next and, on the final screen, press Finish.

    Screenshot of Certification Import Wizard: Completing the Certificate Import Wizard. The certificate will be imported after you click Finish. (Contains a summary of the selected certificate settings.)

  7. After the wizard closes, you will see a Security Warning telling you that you are about to install a root certificate. Select Yes to continue.

    Screenshot of Security Warning modal: “You are about to install a certificate from a certification authority (CA) claiming to represent: Localhost Certificate Authority for aral at aral-win11. Windows cannot validate that the certificate is actually from …”

Finally, restart Edge and you should be able to hit https://localhost without certificate errors.

Now that youve successfully installed Kitten on Windows under WSL 2, you can continue learning about Kitten by following the Getting Started guide and tutorials.

Troubleshooting

Here are some edge cases you might encounter (because others have encountered them) and what you can do about it:

Linux: Untrusted localhost TLS certificates in Chrom(ium)

If at some point Chrom(ium) starts complaining that your development-time localhost certificates are untrusted under Linux, check that you havent accidentally installed a copy of ca-certificates and/or p11-kit in your unprivileged account using a third-party package manager like Homebrew (brew).

If this is the issue, you should:

  1. Remove the ca-certificates and p11-kit packages from Homebrew (you will also have to remove any packages that depend on them. e.g., python, OpenSSL, etc.)
  2. Restart your machine.
  3. Run sudo update-ca-trust extract
  4. Uninstall Kitten (kitten uninstall)
  5. Reinstall Kitten from here or from your working copy of the source code (./install)

That should do it.

To check if your system trust store is set up correctly, you can run the following command:

trust list --filter=ca-anchors | grep Localhost

And you should see output that resembles the following:

label: Localhost Certificate Authority for aral at dev.ar.al

Building Kitten

While developing Kitten, its best practice to run the install script and use the kitten command to run your installed build.

./install

A typical run of the install commands takes about half a second on a modern computer so it should not impact your development velocity negatively.

💡 In order to keep the development build/install process as quick as possible, dependencies are not updated unless you specifically request an npm install by passing the --npm flag:

./install --npm

(However, an npm install will be carried out if this is the first time youre building/installing Kitten locally.)

There is a separate build command (called internally by the install script) and if you use that, you will find the distribution under the dist/ folder.

To run Kitten from the distribution folder, use the following syntax:

dist/kitten [path to serve]

💡 Kittens build + install process (what happens when you run the install script) takes less than half a second on a modern computer and has the additional benefit of informing you of compile-time errors. Its highly recommended you dont run build by itself or run from the dist folder directly unless you have specific reason to.

Debugging

To run Kitten with the Node debugger active (equivalent of launching Node.js using node --inspect), start Kitten using:

INSPECT=true kitten

💡 If you use VSCodium, you can add breakpoints in your code and attach to the process using the Attach command in the Run and Debug panel.

💡 In Chromium, you can use the Node debugger (enter chrome://inspect in address bar → select Open dedicated DevTools for Node).

Profiling

You can see profiling information provided by Kittens console mixin methods profileTime() and profileTimeEnd() by passing the PROFILE=true environment variable:

e.g.,

PROFILE=true kitten examples/streamiverse

Flame Graphs

To narrow down performance issues by time spent on the stack, you can have Kitten generate a flame graph by setting the FLAME_GRAPH environment variable to true.

e.g.,

FLAME_GRAPH=true kitten examples/streamiverse

This will start your project in production mode and globally install and use the 0x node module to capture profiling information and automatically launch a browser when the process exits to show you the flame graph.

💡 Flame graphs can only be generated in production mode as the 0x module is not compatible with Nodes cluster module and the latter is what we use to implement Kittens process manager in development mode.

Testing

Tests are written in Tape With Promises, run using ESM Tape Runner, and displayed using Tap Monkey.

Coverage is provided by c8.

Run tests:

npm -s test

Run coverage:

npm run -s coverage

💡 The -s just silences the npm logs for cleaner output.

🚧 Tests are in the process of being ported from NodeKit to Kitten.

Deployment (of Kitten itself)

To build and deploy a new Kitten package to kittens.small-web.org, run the deploy script:

./deploy

💡You will need to add the secret deployment token to your system in order to deploy. If you have deployment rights for Kitten, follow the instructions available at https://kittens.small-web.org/settings.

Technical design

Kitten is an ESM-only project for Node.js and relies on (the currently experimental) ES Module Loaders (follow the latest work) functionality.

Additionally, Kitten relies on a number of core dependencies for its essential features.

Core dependencies

Dependency Purpose
@small-tech/https Drop-in replacement for Nodes native https module with automatic TLS for development and production using @small-tech/auto-encrypt and @small-tech/auto-encrypt-localhost.
@small-tech/jsdb Zero-dependency, transparent, in-memory, streaming write-on-update JavaScript database that persists to JavaScript transaction logs.
Polka@next Native HTTP server with added support for routing, middleware, and sub-applications. Polka uses Trouter as its router.
tinyws WebSocket middleware for Node.js based on ws.
xhtm XHTM is alternative implementation of HTM without HTM-specific limitations. Low-level machinery is rejected in favor of readability and better HTML support.
hyperscript-to-html-string Render xhtm/Hyperscript to HTML strings, without virtual DOM.
sanitize-html Clean up untrusted HTML, preserving whitelisted elements and whitelisted attributes on a per-element basis. Built on htmlparser2 for speed and tolerance
node-git-server Git server for hosting your source code. Used in deployments.
isomorphic-git Git client used in deployments on development and for handling auto-updates on production.
sade A small command-line interface (CLI) framework that uses mri for its argument parsing.
noble-ed25519 Cryptography: ed25519 key generation.
ed25519-keygen Cryptography: SSH key generation from ed25519 key material.

Kittens renderer is built upon xhtm and vhtml although both of those libraries have been inlined and extended.

For an automatically-generated full list of modules and contributors, please see CONTRIBUTORS.md.

Cryptographical properties

(The functionality described in this section is currently being developed both in Domain and Kitten.)

Overview

The security (and privacy) of Domain/Kitten are based on a 32-byte cryptographically random secret string that only the person who owns/controls a domain knows.

(This is basically an ed25519 secret key.)

When setting up a Small Web app via Domain, this key is generated in the persons browser, on their own computer, and is never communicated to either the Domain instance or the Kitten app being installed. Instead the ed25519 public key is sent to both and signed token authentication is used when the server needs to verify the owners identity (e.g., before allowing access to the administration area).

The expected/encouraged behaviour is for the person to store this secret in their password manager of choice.

From this key material, we derive SSH keys and the persons server is set up to allow SSH access via this key.

Kitten running on a development machine can also recreate these SSH keys and configure the persons SSH access to their server when the secret key is provided.

The persons ed25519 public key is used as their identity within the system and enables their Small Web place (Kitten app) to communicate with the Domain instance for administrative reasons (e.g., to cancel hosting, etc.)

Threat model

The audience for Domain/Kitten and the Small Web in general is everyday people who use technology as an everyday thing and want privacy by default in the systems they use.

If you are an activist, etc., who might be specifically targetted by nation states, etc., please do not use this system as it does not protect against, for example, infiltration of hosting providers by nation state actors.

Given that the secret key material is generated in a web app, you must trust that the code being served by the server is what you expect it to be. Currently, there is no validation mechanism for this, although this is on the roadmap going forward (via, for example, a browser extension). This is not a problem unique to web apps, given that native apps are commonly dynamically built by app stores (at which point a nation state actor could inject malicious code) and given the lack of verifiable builds in mainstream supply chains.

You can, however, overcome this limitation by hosting Domain yourself, on your own hardware (e.g., a single-board computer like a Raspberry Pi).

The other thing to be aware of is that the security of the system is based on your secret key remaining secret and, initially at least, there will not be a way to change this secret key. (On the longer roadmap, it would be nice to provide a means of changing it but this is not a trivial process, especially if encryption keys have been derived from it and encrypted messages exist within a Kitten app).

Frequently-Asked Questions (FAQs)

What about serverless?

Dude, this is literally a server.

If you want “serverless” (funny how folks who own servers want you to go serverless, isnt it? Its almost like a small group of people get to own stuff and you have to rent from them on their terms… hmm 🤔) then use some Big Tech framework like SvelteKit. Theyll bend over backwards to cater to all your Big Web needs.

Can you add <insert Big Tech feature here>?

No.

Will this scale?

*le sigh!*

(Yes, it will scale for the purposes it was designed for. It will not scale for the purposes of farming the population for their data and destroying our human rights and democracy in the process. Thats a feature, not a bug.)

Is there anything stopping me from using this to build sites or apps that violate peoples privacy and farm them for their data? (You know, the business model of Silicon Valley… you know the whole surveillance capitalism and people farming thing?)

No, there is nothing in the license to stop you from doing so.

But I will haunt you in your nightmares if you do.

(Just sayin)

Also, its not nice. Dont.

Is this really a Frequently Asked Questions section or a political statement?

Cant it be both?

(Its a political statement.)

Questions?

Contact Aral on the fediverse.