diff --git a/.gitignore b/.gitignore
index a88775f3c8b3b44f304c145a9583533ffc7fb5df..c777cdf886c3dc91c21a3929968830050c810334 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 dist/
 .idea/
+site/
 *.iml
diff --git a/config/config.yml b/config/config.yml
index 210df07144a411a4b76e1f743ac6966ec7887a16..695d41d282aa3b40b9a2d3af84b1c62dc0c836d4 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -6,7 +6,7 @@
 # listen-http: ":80"
 
 # If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app.
-# This is optional and only required to support Android apps (which don't allow background services anymore).
+# This is optional and only required to save battery when using the Android app.
 #
 # firebase-key-file: <filename>
 
@@ -23,6 +23,8 @@
 # Interval in which keepalive messages are sent to the client. This is to prevent
 # intermediaries closing the connection for inactivity.
 #
+# Note that the Android app has a hardcoded timeout at 77s, so it should be less than that.
+#
 # keepalive-interval: 30s
 
 # Interval in which the manager prunes old messages, deletes topics
diff --git a/docs/config.md b/docs/config.md
new file mode 100644
index 0000000000000000000000000000000000000000..96196a5ee16886c1140346fb996a8af6f7c53b51
--- /dev/null
+++ b/docs/config.md
@@ -0,0 +1,108 @@
+# Configuring the ntfy server
+The ntfy server can be configured in three ways: using a config file (typically at `/etc/ntfy/config.yml`, 
+see [config.yml](https://github.com/binwiederhier/ntfy/blob/main/config/config.yml)), via command line arguments 
+or using environment variables.
+
+## Quick start
+By default, simply running `ntfy` will start the server at port 80. No configuration needed. Batteries included 😀. 
+If everything works as it should, you'll see something like this:
+```
+$ ntfy                
+2021/11/30 19:59:08 Listening on :80
+```
+
+You can immediately start [publishing messages](publish/index.md), or subscribe via the [Android app](subscribe/phone.md),
+[the web UI](subscribe/web.md), or simply via [curl or your favorite HTTP client](subscribe/api.md). To configure 
+the server further, check out the [config options table](#config-options) or simply type `ntfy --help` to
+get a list of [command line options](#command-line-options).
+
+## Config options
+Each config options can be set in the config file `/etc/ntfy/config.yml` (e.g. `listen-http: :80`) or as a 
+CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment
+variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
+
+| Config option | Env variable | Format | Default | Description |
+|---|---|---|---|---|
+| `listen-http` | `NTFY_LISTEN_HTTP` | `[host]:port` | `:80` | Listen address for the HTTP web server |
+| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. |
+| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. |
+| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. |
+| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 30s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
+| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
+| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 5000 | Rate limiting: Total number of topics before the server rejects new topics. |
+| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
+| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
+| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 10s | Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
+| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
+
+The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
+
+## Firebase (FCM)
+!!! info
+    Using Firebase is **optional** and only works if you modify and build your own Android .apk.
+    For a self-hosted instance, it's easier to just not bother with FCM.
+
+[Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging) is the Google approved way to send
+push messages to Android devices. FCM is the only method that an Android app can receive messages without having to run a
+[foreground service](https://developer.android.com/guide/components/foreground-services). 
+
+For the main host [ntfy.sh](https://ntfy.sh), the [ntfy Android App](subscribe/phone.md) uses Firebase to send messages
+to the device. For other hosts, instant delivery is used and FCM is not involved.
+
+To configure FCM for your self-hosted instance of the ntfy server, follow these steps:
+
+1. Sign up for a [Firebase account](https://console.firebase.google.com/)
+2. Create an app and download the key file (e.g. `myapp-firebase-adminsdk-ahnce-....json`)
+3. Place the key file in `/etc/ntfy`, set the `firebase-key-file` in `config.yml` accordingly and restart the ntfy server
+4. Build your own Android .apk following [these instructions]()
+
+Example:
+```
+# If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app.
+# This is optional and only required to support Android apps (which don't allow background services anymore).
+#
+firebase-key-file: "/etc/ntfy/ntfy-sh-firebase-adminsdk-ahnce-9f4d6f14b5.json"
+```
+
+## Behind a proxy (TLS, etc.)
+
+!! warn
+If you are behind a proxy, you must set the `behind-proxy` flag. Otherwise all visitors are rate limited
+as if they are one.
+
+
+## Rate limiting
+Rate limiting: Allowed GET/PUT/POST requests per second, per visitor:
+- visitor-request-limit-burst is the initial bucket of requests each visitor has
+- visitor-request-limit-replenish is the rate at which the bucket is refilled
+
+
+## Command line options
+```
+$ ntfy --help
+NAME:
+   ntfy - Simple pub-sub notification service
+
+USAGE:
+   ntfy [OPTION..]
+
+GLOBAL OPTIONS:
+   --config value, -c value                           config file (default: /etc/ntfy/config.yml) [$NTFY_CONFIG_FILE]
+   --listen-http value, -l value                      ip:port used to as listen address (default: ":80") [$NTFY_LISTEN_HTTP]
+   --firebase-key-file value, -F value                Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
+   --cache-file value, -C value                       cache file used for message caching [$NTFY_CACHE_FILE]
+   --cache-duration since, -b since                   buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
+   --keepalive-interval value, -k value               interval of keepalive messages (default: 30s) [$NTFY_KEEPALIVE_INTERVAL]
+   --manager-interval value, -m value                 interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
+   --global-topic-limit value, -T value               total number of topics allowed (default: 5000) [$NTFY_GLOBAL_TOPIC_LIMIT]
+   --visitor-subscription-limit value, -V value       number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
+   --visitor-request-limit-burst value, -B value      initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
+   --visitor-request-limit-replenish value, -R value  interval at which burst limit is replenished (one per x) (default: 10s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
+   --behind-proxy, -P                                 if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
+
+Try 'ntfy COMMAND --help' for more information.
+
+ntfy v1.4.8 (7b8185c), runtime go1.17, built at 1637872539
+Copyright (C) 2021 Philipp C. Heckel, distributed under the Apache License 2.0
+```
+
diff --git a/docs/develop.md b/docs/develop.md
new file mode 100644
index 0000000000000000000000000000000000000000..1d5b2a722014eab646a84c09f2bd6890e109ecb9
--- /dev/null
+++ b/docs/develop.md
@@ -0,0 +1,5 @@
+# Building
+
+## ntfy server
+
+## Android app
diff --git a/docs/examples.md b/docs/examples.md
new file mode 100644
index 0000000000000000000000000000000000000000..df635b4e61303f9498a7b2edbc413cd6df86f06d
--- /dev/null
+++ b/docs/examples.md
@@ -0,0 +1 @@
+# Examples
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000000000000000000000000000000000000..947fcce6550fb8a55d6bf313573ed0d57ae2b3e1
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,47 @@
+# Frequently asked questions (FAQ)
+
+## Isn't this like ...?
+Who knows. I didn't do a lot of research before making this. It was fun making it.
+
+## Can I use this in my app? Will it stay free?
+Yes. As long as you don't abuse it, it'll be available and free of charge. I do not plan on monetizing
+the service.
+
+## What are the uptime guarantees?
+Best effort.
+
+## What happens if there are multiple subscribers to the same topic?
+As per usual with pub-sub, all subscribers receive notifications if they are
+subscribed to a topic.
+
+## Will you know what topics exist, can you spy on me?
+If you don't trust me or your messages are sensitive, run your own server. It's <a href="https://github.com/binwiederhier/ntfy">open source</a>.
+That said, the logs do not contain any topic names or other details about you.
+Messages are cached for the duration configured in `config.yml` (12h by default) to facilitate service restarts, message polling and to overcome
+client network disruptions.
+
+## Can I self-host it?
+Yes. The server (including this Web UI) can be self-hosted, and the Android app supports adding topics from
+your own server as well. There are <a href="https://github.com/binwiederhier/ntfy#installation">install instructions</a>
+on GitHub.
+
+## Why is Firebase used?
+In addition to caching messages locally and delivering them to long-polling subscribers, all messages are also
+published to Firebase Cloud Messaging (FCM) (if `FirebaseKeyFile` is set, which it is on ntfy.sh). This
+is to facilitate instant notifications on Android.
+    </p>
+
+## How much battery does the Android app use?
+If you use the ntfy.sh server and you don't use the <i>instant delivery</i> feature, the Android app uses no
+additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server, or you use
+<i>instant delivery</i>, the app has to maintain a constant connection to the server, which consumes about 4% of
+battery in 17h of use (on my phone). I use it and it makes no difference to me.
+
+## What is instant delivery?
+Instant delivery is a feature in the Android app. If turned on, the app maintains a constant connection to the
+server and listens for incoming notifications. This consumes <a href="#battery-usage">additional battery</a>,
+but delivers notifications instantly.
+
+## Why is there no iOS app (yet)?
+I don't have an iPhone or a Mac, so I didn't make an iOS app yet. It'd be awesome if
+<a href="https://github.com/binwiederhier/ntfy/issues/4">someone else could help out</a>.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..bf6d425addb69c94a0b81fba242900f6916f1a6c
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,10 @@
+# ntfy.sh | simple HTTP-based pub-sub
+
+**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)
+notification service. It allows you to send notifications to your phone or desktop via scripts from any computer,
+entirely **without signup, cost or setup**. It's also [open source](https://github.com/binwiederhier/ntfy) if you want 
+to run your own.
+
+(pub sub diagram)
+
+(screenshot / video / gif)
diff --git a/docs/install.md b/docs/install.md
new file mode 100644
index 0000000000000000000000000000000000000000..6da2e179e8973bb4216aae87ee9f54df7276fdfa
--- /dev/null
+++ b/docs/install.md
@@ -0,0 +1,94 @@
+# Install your own ntfy server
+The following steps are only required if you want to **self-host your own ntfy server**. If you just want to 
+[send messages using ntfy.sh](publish/index.md), you don't need to install anything. Just use `curl`
+or your favorite HTTP client.
+
+## General steps
+The ntfy server comes as a statically linked binary and is shipped as tarball, deb/rpm packages and as a Docker image.
+We support amd64, armv7 and arm64.
+
+1. Install ntfy using one of the methods described below
+2. Then (optionally) edit `/etc/ntfy/config.yml` (see [configuration](config.md))
+3. Then just run it with `ntfy` (or `systemctl start ntfy` when using the deb/rpm).
+
+
+## Binaries and packages
+Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
+deb/rpm packages.
+
+x86_64/amd64:
+```
+wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_x86_64.tar.gz
+sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
+```
+
+armv7:
+```
+wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_armv7.tar.gz
+sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
+```
+
+arm64/v8:
+```
+wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_arm64.tar.gz
+sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
+```
+
+## Debian/Ubuntu repository
+Installation via Debian repository:
+```bash
+curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
+sudo apt install apt-transport-https
+sudo sh -c "echo 'deb [arch=amd64] https://archive.heckel.io/apt debian main' > /etc/apt/sources.list.d/archive.heckel.io.list"  
+sudo apt update
+sudo apt install ntfy
+```
+
+Manually installing the .deb file:
+```bash
+wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_amd64.deb
+dpkg -i ntfy_1.5.0_amd64.deb
+```
+
+## Fedora/RHEL/CentOS
+```bash
+rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_amd64.rpm
+```
+
+## Docker
+The ntfy server exposes its web UI and the API on port 80, so you need to expose that in Docker. To use the persistent 
+message cache, you also need to map a volume to `/var/cache/ntfy`. To change other settings, you should map `/etc/ntfy`,
+so you can edit `/etc/ntfy/config.yml`.
+
+Basic usage (no cache or additional config):
+```
+docker run -p 80:80 -it binwiederhier/ntfy
+```
+
+With persistent cache (configured as command line arguments):
+```bash
+docker run \
+  -v /var/cache/ntfy:/var/cache/ntfy \
+  -p 80:80 \
+  -it \
+  binwiederhier/ntfy \
+    --cache-file /var/cache/ntfy/cache.db
+```
+
+With other config options (configured via `/etc/ntfy/config.yml`, see [configuration](config.md) for details):
+```bash
+docker run \
+  -v /etc/ntfy:/etc/ntfy \
+  -p 80:80 \
+  -it \
+  binwiederhier/ntfy
+```
+
+## Go
+To install via Go, simply run:
+```bash
+go install heckel.io/ntfy@latest
+```
+!!! info
+    Please [let me know](https://github.com/binwiederhier/ntfy/issues) if there are any issues with this installation
+    method. The SQLite bindings require CGO and it works for me, but I have the feeling it may not work for everyone.
diff --git a/docs/publish/index.md b/docs/publish/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..986dec3c00690f21e917a3e1d221b405865ab660
--- /dev/null
+++ b/docs/publish/index.md
@@ -0,0 +1,207 @@
+# Publishing
+
+Publishing messages can be done via PUT or POST. Topics are created on the fly by subscribing or publishing to them.
+Because there is no sign-up, <b>the topic is essentially a password</b>, so pick something that's not easily guessable.
+
+Here's an example showing how to publish a simple message using a POST request:
+=== "Command line (curl)"
+    ```
+    curl -d "Backup successful 😀" ntfy.sh/mytopic
+    ```
+
+=== "HTTP"
+    ``` http
+    POST /mytopic HTTP/1.1
+    Host: ntfy.sh
+
+    Backup successful 😀
+    ```
+=== "JavaScript"
+    ``` javascript
+    fetch('https://ntfy.sh/mytopic', {
+      method: 'POST', // PUT works too
+      body: 'Backup successful 😀'
+    })
+    ```
+
+=== "Go"
+    ``` go
+    http.Post("https://ntfy.sh/mytopic", "text/plain",
+        strings.NewReader("Backup successful 😀"))
+    ```
+
+=== "PHP"
+    ``` php
+    file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([
+        'http' => [
+            'method' => 'POST', // PUT also works
+            'header' => 'Content-Type: text/plain',
+            'content' => 'Backup successful 😀'
+        ]
+    ]));
+    ```
+
+If you have the [Android app](../subscribe/phone.md) installed on your phone, this will create a notification that looks like this:
+
+<figure markdown>
+  ![basic notification](../static/img/basic-notification.png){ width=500 }
+  <figcaption>Android notification</figcaption>
+</figure>
+
+There are more features related to publishing messages: You can set a [notification priority](#message-priority), 
+a [title](#message-title), and [tag messages](#tags-emojis) 🥳 🎉. Here's an example that uses all of them at once:
+
+=== "Command line (curl)"
+    ```
+    curl \
+      -H "Title: Unauthorized access detected" \
+      -H "Priority: urgent" \
+      -H "Tags: warning,skull" \
+      -d "Remote access to phils-laptop detected. Act right away." \
+      ntfy.sh/phil_alerts
+    ```
+
+=== "HTTP"
+    ``` http
+    POST /phil_alerts HTTP/1.1
+    Host: ntfy.sh
+    Title: Unauthorized access detected
+    Priority: urgent
+    Tags: warning,skull
+    
+    Remote access to phils-laptop detected. Act right away.
+    ```
+
+=== "JavaScript"
+    ``` javascript
+    fetch('https://ntfy.sh/phil_alerts', {
+        method: 'POST', // PUT works too
+        body: 'Remote access to phils-laptop detected. Act right away.',
+        headers: {
+            'Title': 'Unauthorized access detected',
+            'Priority': 'urgent',
+            'Tags': 'warning,skull'
+        }
+    })
+    ```
+
+=== "Go"
+    ``` go
+	req, _ := http.NewRequest("POST", "https://ntfy.sh/phil_alerts",
+		strings.NewReader("Remote access to phils-laptop detected. Act right away."))
+	req.Header.Set("Title", "Unauthorized access detected")
+	req.Header.Set("Priority", "urgent")
+	req.Header.Set("Tags", "warning,skull")
+	http.DefaultClient.Do(req)
+    ```
+
+=== "PHP"
+    ``` php
+    file_get_contents('https://ntfy.sh/phil_alerts', false, stream_context_create([
+        'http' => [
+            'method' => 'POST', // PUT also works
+            'header' =>
+                "Content-Type: text/plain\r\n" .
+                "Title: Unauthorized access detected\r\n" .
+                "Priority: urgent\r\n" .
+                "Tags: warning,skull",
+            'content' => 'Remote access to phils-laptop detected. Act right away.'
+        ]
+    ]));
+    ```
+
+<figure markdown>
+  ![priority notification](../static/img/priority-notification.png){ width=500 }
+  <figcaption>Urgent notification with tags and title</figcaption>
+</figure>
+
+## Message priority
+All messages have a priority, which defines how urgently your phone notifies you. You can set custom
+notification sounds and vibration patterns on your phone to map to these priorities (see [Android config](../subscribe/phone.md)).
+
+The following priorities exist:
+
+| Priority | Icon | ID | Name | Description |
+|---|---|---|---|---|
+| Max priority | ![min priority](../static/img/priority-5.svg) | `5` | `max`/`urgent` | Really long vibration bursts, default notification sound with a pop-over notification. |
+| High priority | ![min priority](../static/img/priority-4.svg) | `4` | `high` | Long vibration burst, default notification sound with a pop-over notification. |
+| **Default priority** | *(none)* | `3` | `default` | Short default vibration and sound. Default notification behavior. |
+| Low priority | ![min priority](../static/img/priority-2.svg) |`2` | `low` | No vibration or sound. Notification will not visibly show up until notification drawer is pulled down. |
+| Min priority | ![min priority](../static/img/priority-1.svg) | `1` | `min` | No vibration or sound. The notification will be under the fold in "Other notifications". |
+
+You can set the priority with the header `X-Priority` (or any of its aliases: `Priority`, `prio`, or `p`).
+
+=== "Command line (curl)"
+    ```
+    curl -H "X-Priority: 5" -d "An urgent message" ntfy.sh/phil_alerts
+    curl -H "Priority: low" -d "Low priority message" ntfy.sh/phil_alerts
+    curl -H p:4 -d "A high priority message" ntfy.sh/phil_alerts
+    ```
+
+=== "HTTP"
+    ``` http
+    POST /phil_alerts HTTP/1.1
+    Host: ntfy.sh
+    Priority: 5
+
+    An urgent message
+    ```
+
+=== "JavaScript"
+    ``` javascript
+    fetch('https://ntfy.sh/phil_alerts', {
+        method: 'POST',
+        body: 'An urgent message',
+        headers: { 'Priority': '5' }
+    })
+    ```
+
+=== "Go"
+    ``` go
+    req, _ := http.NewRequest("POST", "https://ntfy.sh/phil_alerts", strings.NewReader("An urgent message"))
+    req.Header.Set("Priority", "5")
+    http.DefaultClient.Do(req)
+    ```
+
+=== "PHP"
+    ``` php
+    file_get_contents('https://ntfy.sh/phil_alerts', false, stream_context_create([
+        'http' => [
+            'method' => 'POST',
+            'header' =>
+                "Content-Type: text/plain\r\n" .
+                "Priority: 5",
+            'content' => 'An urgent message'
+        ]
+    ]));
+    ```
+
+<figure markdown>
+  ![priority notification](../static/img/priority-detail-overview.png){ width=500 }
+  <figcaption>Detail view of priority notifications</figcaption>
+</figure>
+
+## Tags & emojis 🥳 🎉
+You can tag messages with emojis (or other relevant strings). If a tag matches a <a href="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json">known emoji short code</a>,
+it will be converted to an emoji. If it doesn't match, it will be listed below the notification. This is useful
+for things like warnings and such (⚠️, ️🚨, or 🚩), but also to simply tag messages otherwise (e.g. which script the
+message came from, ...).
+
+You can set tags with the `X-Tags` header (or any of its aliases: `Tags`, or `ta`).
+Use <a href="https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json">this reference</a>
+to figure out what tags can be converted to emojis. In the example below, the tag "warning" matches the emoji ⚠️,
+the tag "ssh-login" doesn't match and will be displayed below the message.
+
+```
+$ curl -H "Tags: warning,ssh-login" -d "Unauthorized SSH access" ntfy.sh/mytopic
+{"id":"ZEIwjfHlSS",...,"tags":["warning","ssh-login"],"message":"Unauthorized SSH access"}
+```
+
+## Message title
+The notification title is typically set to the topic short URL (e.g. `ntfy.sh/mytopic`.
+To override it, you can set the `X-Title` header (or any of its aliases: `Title`, `ti`, or `t`).
+
+```
+curl -H "Title: Dogs are better than cats" -d "Oh my ..." ntfy.sh/mytopic<
+```
+        
diff --git a/docs/static/css/extra.css b/docs/static/css/extra.css
new file mode 100644
index 0000000000000000000000000000000000000000..b834efce4b1579abf05a1b55c41257d1c9cd3501
--- /dev/null
+++ b/docs/static/css/extra.css
@@ -0,0 +1,4 @@
+figure img {
+    border-radius: 7px;
+    filter: drop-shadow(3px 3px 5px #ccc);
+}
diff --git a/docs/static/img/basic-notification.png b/docs/static/img/basic-notification.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a8a245d95bbe79e0e4699ed4d34f9581c86da6c
Binary files /dev/null and b/docs/static/img/basic-notification.png differ
diff --git a/docs/static/img/favicon.png b/docs/static/img/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..92312feac6a3c9effe8323e76a6d30f2ce88d724
Binary files /dev/null and b/docs/static/img/favicon.png differ
diff --git a/docs/static/img/ntfy.png b/docs/static/img/ntfy.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b969a846efc6c3dac70510793c72253e04389c9
Binary files /dev/null and b/docs/static/img/ntfy.png differ
diff --git a/docs/static/img/priority-1.svg b/docs/static/img/priority-1.svg
new file mode 100644
index 0000000000000000000000000000000000000000..df6a0a4907cb871d316fc72f34cf97623157197d
--- /dev/null
+++ b/docs/static/img/priority-1.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="24px"
+   viewBox="0 0 24 24"
+   width="24px"
+   fill="#000000"
+   version="1.1"
+   id="svg1428"
+   sodipodi:docname="priority_1_24dp.svg"
+   inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1432" />
+  <sodipodi:namedview
+     id="namedview1430"
+     pagecolor="#505050"
+     bordercolor="#eeeeee"
+     borderopacity="1"
+     inkscape:pageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="20.517358"
+     inkscape:cx="22.834324"
+     inkscape:cy="15.742768"
+     inkscape:window-width="1863"
+     inkscape:window-height="1025"
+     inkscape:window-x="57"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg1428" />
+  <path
+     style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="m 12.195014,20.828316 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984807,3.635327 -5.9848086,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464146,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
+     id="rect3554" />
+  <path
+     style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="m 12.195014,15.694014 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037176 A 1.2745823,1.2745823 0 0 0 19.930749,9.7205243 1.2745823,1.2745823 0 0 0 18.179821,9.2928073 L 12.195014,12.928134 6.2102054,9.2928073 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464146,4.037176 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
+     id="path9314" />
+  <path
+     style="color:#000000;fill:#cccccc;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="m 12.116784,10.426777 a 1.2747098,1.2747098 0 0 0 0.661606,-0.185205 l 6.646593,-4.0371767 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751108 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 l -5.984808,3.635327 -5.9848066,-3.635327 a 1.2745823,1.2745823 0 0 0 -1.750928,0.427718 1.2745823,1.2745823 0 0 0 0.427537,1.751108 L 11.455,10.241572 a 1.2747098,1.2747098 0 0 0 0.661784,0.185205 z"
+     id="path9316" />
+</svg>
diff --git a/docs/static/img/priority-2.svg b/docs/static/img/priority-2.svg
new file mode 100644
index 0000000000000000000000000000000000000000..10a89ad15f077c34076dffd8c14a12bff9ceba54
--- /dev/null
+++ b/docs/static/img/priority-2.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="24px"
+   viewBox="0 0 24 24"
+   width="24px"
+   fill="#000000"
+   version="1.1"
+   id="svg1428"
+   sodipodi:docname="priority_2_24dp.svg"
+   inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1432" />
+  <sodipodi:namedview
+     id="namedview1430"
+     pagecolor="#505050"
+     bordercolor="#eeeeee"
+     borderopacity="1"
+     inkscape:pageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="20.517358"
+     inkscape:cx="22.834324"
+     inkscape:cy="15.742768"
+     inkscape:window-width="1863"
+     inkscape:window-height="1025"
+     inkscape:window-x="57"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg1428" />
+  <path
+     style="color:#000000;fill:#999999;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="m 12.172712,17.774352 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 l 6.646593,-4.037178 a 1.2745823,1.2745823 0 0 0 0.427537,-1.751107 1.2745823,1.2745823 0 0 0 -1.750928,-0.427718 L 12.172712,15.00847 6.1879033,11.373143 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427718 1.2745823,1.2745823 0 0 0 0.427536,1.751107 l 6.6464147,4.037178 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
+     id="rect3554" />
+  <path
+     style="color:#000000;fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="m 12.172712,12.64005 a 1.2747098,1.2747098 0 0 0 0.661605,-0.185206 L 19.48091,8.4176679 A 1.2745823,1.2745823 0 0 0 19.908447,6.6665602 1.2745823,1.2745823 0 0 0 18.157519,6.2388432 L 12.172712,9.8741699 6.1879033,6.2388432 a 1.2745823,1.2745823 0 0 0 -1.750927,0.427717 1.2745823,1.2745823 0 0 0 0.427536,1.7511077 l 6.6464147,4.0371761 a 1.2747098,1.2747098 0 0 0 0.661785,0.185206 z"
+     id="path9314" />
+</svg>
diff --git a/docs/static/img/priority-4.svg b/docs/static/img/priority-4.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a1723cf82bbdce0734401e702605586549c67f43
--- /dev/null
+++ b/docs/static/img/priority-4.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="24px"
+   viewBox="0 0 24 24"
+   width="24px"
+   fill="#000000"
+   version="1.1"
+   id="svg1428"
+   sodipodi:docname="priority_4_24dp.svg"
+   inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1432" />
+  <sodipodi:namedview
+     id="namedview1430"
+     pagecolor="#505050"
+     bordercolor="#eeeeee"
+     borderopacity="1"
+     inkscape:pageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="20.517358"
+     inkscape:cx="22.834324"
+     inkscape:cy="15.742768"
+     inkscape:window-width="1863"
+     inkscape:window-height="1025"
+     inkscape:window-x="57"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg1428" />
+  <path
+     style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="M 12.116784,6.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,6.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,12.512932 1.2745823,1.2745823 0 0 0 19.424984,10.761824 L 12.778569,6.724648 A 1.2747098,1.2747098 0 0 0 12.116784,6.5394415 Z"
+     id="path9314" />
+  <path
+     style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="m 12.195014,11.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
+     id="path9316" />
+</svg>
diff --git a/docs/static/img/priority-5.svg b/docs/static/img/priority-5.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2e2c44475aad99425614152dfa365ed601355406
--- /dev/null
+++ b/docs/static/img/priority-5.svg
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="24px"
+   viewBox="0 0 24 24"
+   width="24px"
+   fill="#000000"
+   version="1.1"
+   id="svg1428"
+   sodipodi:docname="priority_5_24dp.svg"
+   inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs1432" />
+  <sodipodi:namedview
+     id="namedview1430"
+     pagecolor="#505050"
+     bordercolor="#eeeeee"
+     borderopacity="1"
+     inkscape:pageshadow="0"
+     inkscape:pageopacity="0"
+     inkscape:pagecheckerboard="0"
+     showgrid="false"
+     inkscape:zoom="20.517358"
+     inkscape:cx="22.834323"
+     inkscape:cy="15.742767"
+     inkscape:window-width="1863"
+     inkscape:window-height="1025"
+     inkscape:window-x="57"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg1428" />
+  <path
+     style="color:#000000;fill:#aa0000;fill-opacity:1;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="M 12.116784,3.40514 A 1.2747098,1.2747098 0 0 0 11.455179,3.5903463 L 4.8085864,7.6275238 A 1.2745823,1.2745823 0 0 0 4.3810494,9.3786313 1.2745823,1.2745823 0 0 0 6.1319775,9.8063489 L 12.116784,6.1710217 18.101593,9.8063489 A 1.2745823,1.2745823 0 0 0 19.85252,9.3786313 1.2745823,1.2745823 0 0 0 19.424984,7.6275238 L 12.778569,3.5903463 A 1.2747098,1.2747098 0 0 0 12.116784,3.40514 Z"
+     id="rect3554" />
+  <path
+     style="color:#000000;fill:#c60000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="M 12.116784,8.5394415 A 1.2747098,1.2747098 0 0 0 11.455179,8.724648 l -6.6465926,4.037176 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.7509281,0.427717 l 5.9848065,-3.635327 5.984809,3.635327 A 1.2745823,1.2745823 0 0 0 19.85252,14.512932 1.2745823,1.2745823 0 0 0 19.424984,12.761824 L 12.778569,8.724648 A 1.2747098,1.2747098 0 0 0 12.116784,8.5394415 Z"
+     id="path9314" />
+  <path
+     style="color:#000000;fill:#de0000;fill-opacity:1;stroke:none;stroke-width:0.0919748;stroke-linecap:round;stroke-linejoin:round;-inkscape-stroke:none"
+     d="m 12.195014,13.806679 a 1.2747098,1.2747098 0 0 0 -0.661606,0.185205 l -6.6465924,4.037177 a 1.2745823,1.2745823 0 0 0 -0.427537,1.751108 1.2745823,1.2745823 0 0 0 1.750928,0.427718 l 5.9848074,-3.635327 5.984807,3.635327 a 1.2745823,1.2745823 0 0 0 1.750928,-0.427718 1.2745823,1.2745823 0 0 0 -0.427537,-1.751108 l -6.646414,-4.037177 a 1.2747098,1.2747098 0 0 0 -0.661784,-0.185205 z"
+     id="path9316" />
+</svg>
diff --git a/docs/static/img/priority-detail-overview.png b/docs/static/img/priority-detail-overview.png
new file mode 100644
index 0000000000000000000000000000000000000000..c9321aa7f1c6b509ae93c5593b8157e6c9d2d420
Binary files /dev/null and b/docs/static/img/priority-detail-overview.png differ
diff --git a/docs/static/img/priority-notification.png b/docs/static/img/priority-notification.png
new file mode 100644
index 0000000000000000000000000000000000000000..31d151525a154920d927e2243932e7a456baa748
Binary files /dev/null and b/docs/static/img/priority-notification.png differ
diff --git a/docs/static/js/extra.js b/docs/static/js/extra.js
new file mode 100644
index 0000000000000000000000000000000000000000..d588866aed3d2e39ffb9650ee7cd756a0e5faad5
--- /dev/null
+++ b/docs/static/js/extra.js
@@ -0,0 +1,31 @@
+// Link tabs, as per https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs
+
+const savedTab = localStorage.getItem('savedTab')
+const tabs = document.querySelectorAll(".tabbed-set > input")
+for (const tab of tabs) {
+    tab.addEventListener("click", () => {
+        const current = document.querySelector(`label[for=${tab.id}]`)
+        const pos = current.getBoundingClientRect().top
+        const labelContent = current.innerHTML
+        const labels = document.querySelectorAll('.tabbed-set > label, .tabbed-alternate > .tabbed-labels > label')
+        for (const label of labels) {
+            if (label.innerHTML === labelContent) {
+                document.querySelector(`input[id=${label.getAttribute('for')}]`).checked = true
+            }
+        }
+
+        // Preserve scroll position
+        const delta = (current.getBoundingClientRect().top) - pos
+        window.scrollBy(0, delta)
+
+        // Save
+        localStorage.setItem('savedTab', labelContent)
+    })
+
+    // Select saved tab
+    const current = document.querySelector(`label[for=${tab.id}]`)
+    const labelContent = current.innerHTML
+    if (savedTab === labelContent) {
+        tab.checked = true
+    }
+}
diff --git a/docs/subscribe/api.md b/docs/subscribe/api.md
new file mode 100644
index 0000000000000000000000000000000000000000..8a77f13ca3c1abea10a331d30b944615c2da0c11
--- /dev/null
+++ b/docs/subscribe/api.md
@@ -0,0 +1 @@
+# Subscribe from your phone
diff --git a/docs/subscribe/phone.md b/docs/subscribe/phone.md
new file mode 100644
index 0000000000000000000000000000000000000000..8a77f13ca3c1abea10a331d30b944615c2da0c11
--- /dev/null
+++ b/docs/subscribe/phone.md
@@ -0,0 +1 @@
+# Subscribe from your phone
diff --git a/docs/subscribe/poll.md b/docs/subscribe/poll.md
new file mode 100644
index 0000000000000000000000000000000000000000..8a77f13ca3c1abea10a331d30b944615c2da0c11
--- /dev/null
+++ b/docs/subscribe/poll.md
@@ -0,0 +1 @@
+# Subscribe from your phone
diff --git a/docs/subscribe/since.md b/docs/subscribe/since.md
new file mode 100644
index 0000000000000000000000000000000000000000..8a77f13ca3c1abea10a331d30b944615c2da0c11
--- /dev/null
+++ b/docs/subscribe/since.md
@@ -0,0 +1 @@
+# Subscribe from your phone
diff --git a/docs/subscribe/web.md b/docs/subscribe/web.md
new file mode 100644
index 0000000000000000000000000000000000000000..7da6add2248b26f09da5ff10f1be1f3a0d57d2fe
--- /dev/null
+++ b/docs/subscribe/web.md
@@ -0,0 +1 @@
+# Subscribe from the web UI
diff --git a/examples/publish-go/main.go b/examples/publish-go/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..35fac138b41e32e2e743b58c3749ae08ac98c87f
--- /dev/null
+++ b/examples/publish-go/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"log"
+	"net/http"
+	"strings"
+)
+
+func main() {
+	// Without additional headers (priority, tags, title), it's a one liner.
+	// Check out https://ntfy.sh/mytopic in your browser after running this.
+	http.Post("https://ntfy.sh/mytopic", "text/plain", strings.NewReader("Backup successful 😀"))
+
+	// If you'd like to add title, priority, or tags, it's a little harder.
+	// Check out https://ntfy.sh/phil_alerts in your browser.
+	req, err := http.NewRequest("POST", "https://ntfy.sh/phil_alerts",
+		strings.NewReader("Remote access to phils-laptop detected. Act right away."))
+	if err != nil {
+		log.Fatal(err)
+	}
+	req.Header.Set("Title", "Unauthorized access detected")
+	req.Header.Set("Priority", "urgent")
+	req.Header.Set("Tags", "warning,skull")
+	if _, err := http.DefaultClient.Do(req); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/examples/publish-php/publish.php b/examples/publish-php/publish.php
new file mode 100644
index 0000000000000000000000000000000000000000..c7c6eefcb4f90122f5472aaf0f18bb3cc37237b7
--- /dev/null
+++ b/examples/publish-php/publish.php
@@ -0,0 +1,14 @@
+<?php
+
+// Check out https://ntfy.sh/phil_alerts in your browser after running this.
+file_get_contents('https://ntfy.sh/phil_alerts', false, stream_context_create([
+    'http' => [
+        'method' => 'POST', // PUT also works
+        'header' =>
+            "Content-Type: text/plain\r\n" .
+            "Title: Unauthorized access detected\r\n" .
+            "Priority: urgent\r\n" .
+            "Tags: warning,skull",
+        'content' => 'Remote access to phils-laptop detected. Act right away.'
+    ]
+]));
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a877bfb40afdd0b9f3e46c8e07aeac881c0547be
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,85 @@
+site_name: ntfy.sh
+site_url: https://ntfy.sh
+site_description: simple HTTP-based pub-sub
+copyright: Made with ❤️ by Philipp C. Heckel
+repo_name: binwiederhier/ntfy
+repo_url: https://github.com/binwiederhier/ntfy
+edit_uri: edit/main/docs/
+
+theme:
+  name: material
+#  custom_dir: docs/overrides
+  language: en
+  logo: static/img/ntfy.png
+  favicon: static/img/favicon.png
+  include_search_page: false
+  search_index_only: true
+  palette:
+    - media: "(prefers-color-scheme: light)"  # Light mode
+      scheme: default
+      primary: teal
+      toggle:
+        icon: material/lightbulb-outline
+        name: Switch to light mode
+    - media: "(prefers-color-scheme: dark)"  # Dark mode
+      scheme: slate
+      primary: teal
+      accent: indigo
+      toggle:
+        icon: material/lightbulb
+        name: Switch to dark mode
+  features:
+    - search.suggest
+    - search.highlight
+    - search.share
+    - navigation.sections
+    - toc.integrate
+    - content.tabs.link
+extra_javascript:
+  - static/js/extra.js
+extra_css:
+  - static/css/extra.css
+
+markdown_extensions:
+  - admonition
+  - codehilite
+  - meta
+  - toc:
+      permalink: true
+  - pymdownx.tabbed:
+      alternate_style: true
+  - pymdownx.superfences
+  - pymdownx.highlight
+  - pymdownx.tasklist:
+      custom_checkbox: true
+  - footnotes
+  - attr_list
+  - md_in_html
+
+plugins:
+  - search
+
+extra:
+  social:
+    - icon: fontawesome/brands/github-alt
+      link: https://github.com/binwiederhier
+
+nav:
+- "Getting started": index.md
+- "Installation": install.md
+- "Configuration": config.md
+- "Publishing":
+  - "Sending messages": publish/index.md
+- "Subscribing":
+    - "From the Android/iOS app": subscribe/phone.md
+    - "From the Web UI": subscribe/web.md
+    - "Using the API":
+        - "Basic API usage": subscribe/api.md
+        - "Fetching cached messages": subscribe/since.md
+        - "Polling": subscribe/poll.md
+- "Other things":
+  - "Examples": examples.md
+  - "FAQs": faq.md
+  - "Development": develop.md
+
+