about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2021-08-03 08:52:07 +0300
committerVika <vika@fireburn.ru>2021-08-03 08:52:07 +0300
commit1589475ebefa72c2981d1b50611531545c1dd32a (patch)
tree383a48e75149ad2eb2ed9c7cfc4a68c7d7161d53
parent949a961c19ba994c3f0846e7b54d9a55a94d7b9a (diff)
downloadkittybox-1589475ebefa72c2981d1b50611531545c1dd32a.tar.zst
Moved templates into their own module
frontend.rs was too hard to navigate. Additionally, this allows playing
with the organizational structure of templates, since they only need to
export a certain API and how exactly they work is an implementation
detail.
-rw-r--r--src/frontend/mod.rs587
-rw-r--r--src/frontend/templates.rs584
2 files changed, 585 insertions, 586 deletions
diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs
index 2cef026..b974a93 100644
--- a/src/frontend/mod.rs
+++ b/src/frontend/mod.rs
@@ -6,592 +6,7 @@ use tide::{Next, Request, Response, Result, StatusCode};
 
 static POSTS_PER_PAGE: usize = 20;
 
-mod templates {
-    use super::IndiewebEndpoints;
-    use ellipse::Ellipse;
-    use http_types::StatusCode;
-    use log::error;
-
-    /// Return a pretty location specifier from a geo: URI.
-    fn decode_geo_uri(uri: &str) -> String {
-        if let Some(part) = uri.split(':').collect::<Vec<_>>().get(1) {
-            if let Some(part) = part.split(';').next() {
-                let mut parts = part.split(',');
-                let lat = parts.next().unwrap();
-                let lon = parts.next().unwrap();
-                // TODO - format them as proper latitude and longitude
-                return format!("{}, {}", lat, lon);
-            } else {
-                uri.to_string()
-            }
-        } else {
-            uri.to_string()
-        }
-    }
-
-    markup::define! {
-        Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, content: String) {
-            @markup::doctype()
-            html {
-                head {
-                    title { @title }
-                    link[rel="preconnect", href="https://fonts.gstatic.com"];
-                    link[rel="stylesheet", href="/static/style.css"];
-                    meta[name="viewport", content="initial-scale=1, width=device-width"];
-                    // TODO: link rel= for common IndieWeb APIs: webmention, microsub
-                    link[rel="micropub", href="/micropub"]; // Static, because it's built into the server itself
-                    link[rel="authorization_endpoint", href=&endpoints.authorization_endpoint];
-                    link[rel="token_endpoint", href=&endpoints.token_endpoint];
-                    @if endpoints.webmention.is_some() {
-                        link[rel="webmention", href=&endpoints.webmention.as_ref()];
-                    }
-                    @if endpoints.microsub.is_some() {
-                        link[rel="microsub", href=&endpoints.microsub.as_ref()];
-                    }
-                }
-                body {
-                    nav#headerbar {
-                        // TODO: Find a way to store the website name somewhere in the database
-                        // Maybe in the settings?
-                        ul {
-                            li { a#homepage[href="/"] { @blog_name } }
-                            li.shiftright { a#login[href="/login"] { "Login" } }
-                        }
-                    }
-                    main {
-                        @markup::raw(content)
-                    }
-                }
-            }
-        }
-        OnboardingPage {
-            h1[style="text-align: center"] {
-                "Welcome to Kittybox"
-            }
-            script[type="module", src="/static/onboarding.js"] {}
-            link[rel="stylesheet", href="/static/onboarding.css"];
-            form.onboarding[action="/", method="POST"] {
-                noscript {
-                    p {
-                        "Ok, let's be honest. Most of this software doesn't require JS to be enabled "
-                        "to view pages (and in some cases, even edit them if logged in)."
-                    }
-                    p { "This page is a little bit different. It uses JavaScript to provide interactive features, such as:" }
-                    ul {
-                        li { "Multiple-input questions" }
-                        li { "Answers spanning multiple fields" }
-                        li { "Preview of files being uploaded" }
-                        li { "Pretty pagination so you won't feel overwhelmed" }
-                    }
-                    p {
-                        "Sadly, it's very hard or even impossible to recreate this without any JavaScript. "
-                        "Good news though - the code is " b { "open-source AND free software" }
-                        " (under MIT (X11) or Apache-2.0 license - your choice) "
-                        "and I promise to not obfuscate it or minify it. "
-                        a[href="/static/onboarding.js"] { "Here" }
-                        "'s the link - you can try reading it so you'll be 200% sure "
-                        "it won't steal your cookies or turn your kitty into a soulless monster."
-                        @markup::raw("<!-- do cats even have souls? I'm not sure. But this code won't steal their souls anyway. -->")
-                    }
-                    hr;
-                    p { "In other words: " b { "please enable JavaScript for this page to work properly." } small { "sorry T__T" } }
-                }
-                ul#progressbar[style="display: none"] {
-                    li#intro { "Introduction" }
-                    li#hcard { "Your profile" }
-                    li#settings { "Your website" }
-                    li#firstpost { "Your first post" }
-                }
-                fieldset#intro[style="display: none"] {
-                    legend { "Introduction" }
-                    p {
-                        "Kittybox is a CMS that can act as a member of the IndieWeb. "
-                        "IndieWeb is a global distributed social network built on top of open Web standards "
-                        "and composed of blogs around the Internet supporting these standards."
-                    }
-                    p { "There is no registration or centralized database of any sort - everyone owns their data and is responsible for it." }
-                    p { "If you're seeing this page, it looks like your configuration is correct and we can proceed with the setup." }
-
-                    div.switch_card_buttons {
-                        button.switch_card.next_card[type="button", "data-card"="hcard"] { "Next" }
-                    }
-                }
-
-                fieldset#hcard[style="display: none"] {
-                    legend { "Your profile" }
-                    p { "An h-card is an IndieWeb social profile, and we're gonna make you one!" }
-                    p { "Thanks to some clever markup, it will be readable by both humans and machines looking at your homepage."}
-                    p {
-                        "If you make a mistake, don't worry, you're gonna be able to edit this later."
-                        "The only mandatory field is your name."
-                    }
-
-                    div.form_group {
-                        label[for="hcard_name"] { "Your name" }
-                        input#hcard_name[name="hcard_name", placeholder="Your name"];
-                        small {
-                            "No need to write the name as in your passport, this is not a legal document "
-                            "- just write how you want to be called on the network. This name will be also "
-                            "shown whenever you leave a comment on someone else's post using your website."
-                        }
-                    }
-
-                    div.form_group {
-                        label[for="pronouns"] { "Your pronouns" }
-                        div.multi_input#pronouns {
-                            template {
-                                input#hcard_pronouns[name="hcard_pronouns", placeholder="they/them?"];
-                            }
-                            button.add_more[type="button", "aria-label"="Add more"] { "[+] Add more" }
-                        }
-                        small {
-                            "Write which pronouns you use for yourself. It's a free-form field "
-                            "so don't feel constrained - but keep it compact, as it'll be shown in a lot of places."
-                        }
-                    }
-
-                    div.form_group {
-                        label[for="urls"] { "Links to other pages of you" }
-                        div.multi_input#urls {
-                            template {
-                                input#hcard_url[name="hcard_url", placeholder="https://example.com/"];
-                            }
-                            button.add_more[type="button", "aria-label"="Add more"] { "[+] Add more" }
-                        }
-                        small {
-                            "These URLs will help your readers find you elsewhere and will help you that whoever owns these pages owns your website too"
-                            " in case the links are mutual. So make sure to put a link to your site in your other social profiles!"
-                        }
-                    }
-
-                    div.form_group {
-                        label[for="hcard_note"] { "A little about yourself" }
-                        textarea#hcard_note[name="hcard_note", placeholder="Loves cooking, plants, cats, dogs and racoons."] {}
-                        small { "A little bit of introduction. Just one paragraph, and note, you can't use HTML here (yet)." }
-                        // TODO: HTML e-note instead of p-note
-                    }
-
-                    // TODO: u-photo upload - needs media endpoint cooperation
-
-                    div.switch_card_buttons {
-                        button.switch_card.prev_card[type="button", "data-card"="intro"] { "Previous" }
-                        button.switch_card.next_card[type="button", "data-card"="settings"] { "Next" }
-                    }
-                }
-
-                fieldset#settings[style="display: none"] {
-                    legend { "Your website" }
-                    p { "Ok, it's nice to know you more. Tell me about what you'll be writing and how you want to name your blog." }
-                    // TODO: site-name, saved to settings
-
-                    div.form_group {
-                        label[for="blog_name"] { "Your website's name"}
-                        input#blog_name[name="blog_name", placeholder="Kitty Box!"];
-                        small { "It'll get shown in the title of your blog, in the upper left corner!" }
-                    }
-
-                    div.form_group {
-                        label[for="custom_feeds"] { "Custom feeds" }
-                        small {
-                            p {
-                                "You can set up custom feeds to post your stuff to. "
-                                "This is a nice way to organize stuff into huge folders, like all your trips or your quantified-self data."
-                            }
-                            p {
-                                "Feeds can be followed individually, which makes it easy for users who are interested in certain types "
-                                "of content you produce to follow your adventures in certain areas of your life without cluttering their "
-                                "readers."
-                            }
-                            p {
-                                "We will automatically create some feeds for you aside from these so you won't have to - including a main feed, "
-                                "address book (for venues you go to and people you talk about), a cookbook for your recipes and some more."
-                                // TODO: Put a link to documentation explaining feeds in more detail.
-                            }
-                        }
-                        div.multi_input#custom_feeds {
-                            template {
-                                fieldset.feed {
-                                    div.form_group {
-                                        label[for="feed_name"] { "Name" }
-                                        input#feed_name[name="feed_name", placeholder="My cool feed"];
-                                        small { "This is a name that will identify this feed to the user. Make it short and descriptive!" }
-                                    }
-                                    div.form_group {
-                                        label[for="feed_slug"] { "Slug" }
-                                        input#feed_slug[name="feed_slug", placeholder="my-cool-feed"];
-                                        small { "This will form a pretty URL for the feed. For example: https://example.com/feeds/my-cool-feed" }
-                                    }
-                                }
-                            }
-                            button.add_more[type="button", "aria-label"="Add more"] { "[+] Add More" }
-                        }
-                    }
-
-                    div.switch_card_buttons {
-                        button.switch_card.prev_card[type="button", "data-card"="hcard"] { "Previous" }
-                        button.switch_card.next_card[type="button", "data-card"="firstpost"] { "Next" }
-                    }
-                }
-
-                fieldset#firstpost[style="display: none"] {
-                    legend { "Your first post" }
-                    p { "Maybe you should start writing your first posts now. How about a short note?" }
-                    p { "A note is a short-form post (not unlike a tweet - but without the actual character limit) that doesn't bear a title." }
-                    p {
-                        "Consider telling more about yourself, your skills and interests in this note "
-                        @markup::raw("&mdash;")
-                        " though you're free to write anything you want. (By the way, you can use "
-                        a[href="https://daringfireball.net/projects/markdown/syntax"] { "Markdown" }
-                        " here to spice up your note!)"
-                    }
-
-                    textarea#first_post_content[style="width: 100%; height: 8em", placeholder="Hello! I am really excited about #IndieWeb"] {}
-
-                    div.switch_card_buttons {
-                        button.switch_card.prev_card[type="button", "data-card"="settings"] { "Previous" }
-                        button[type="submit"] { "Finish" }
-                    }
-                }
-            }
-        }
-        Entry<'a>(post: &'a serde_json::Value) {
-            article."h-entry" {
-                header.metadata {
-                    @if post["properties"]["name"][0].is_string() {
-                        h1."p-name" {
-                            @post["properties"]["name"][0].as_str().unwrap()
-                        }
-                    }
-                    div {
-                        span {
-                            a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] {
-                                time."dt-published"[datetime=post["properties"]["published"][0].as_str().unwrap()] {
-                                    @chrono::DateTime::parse_from_rfc3339(post["properties"]["published"][0].as_str().unwrap())
-                                        .map(|dt| dt.format("%a %b %e %T %Y").to_string())
-                                        .unwrap_or("ERROR: Couldn't parse the datetime".to_string())
-                                }
-                            }
-                        }
-                        @if post["properties"]["category"].is_array() {
-                            span {
-                                ul.categories {
-                                    "Tagged: "
-                                    @for cat in post["properties"]["category"].as_array().unwrap() {
-                                        li."p-category" { @cat.as_str().unwrap() }
-                                    }
-                                }
-                            }
-                        }
-                        @if post["properties"]["in-reply-to"].is_array() {
-                            // TODO: Rich reply contexts - blocked on MF2 parser
-                            span {
-                                "In reply to: "
-                                ul.replyctx {
-                                    @for ctx in post["properties"]["in-reply-to"].as_array().unwrap() {
-                                        li { a."u-in-reply-to"[href=ctx.as_str().unwrap()] {
-                                            @ctx.as_str().unwrap().truncate_ellipse(24).as_ref()
-                                        } }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                    @if post["properties"]["location"].is_array() || post["properties"]["checkin"].is_array() {
-                        div {
-                            @if post["properties"]["checkin"].is_array() {
-                                span {
-                                    "Check-in to: "
-                                    @if post["properties"]["checkin"][0].is_string() {
-                                        // It's a URL
-                                        a."u-checkin"[href=post["properties"]["checkin"][0].as_str().unwrap()] {
-                                            @post["properties"]["checkin"][0].as_str().unwrap().truncate_ellipse(24).as_ref()
-                                        }
-                                    } else {
-                                        a."u-checkin"[href=post["properties"]["checkin"][0]["properties"]["uid"][0].as_str().unwrap()] {
-                                            @post["properties"]["checkin"][0]["properties"]["name"][0].as_str().unwrap()
-                                        }
-                                    }
-                                }
-                            }
-                            @if post["properties"]["location"].is_array() {
-                                span {
-                                    "Location: "
-                                    @if post["properties"]["location"][0].is_string() {
-                                        // It's a geo: URL
-                                        // We need to decode it
-                                        a."u-location"[href=post["properties"]["location"][0].as_str().unwrap()] {
-                                            @decode_geo_uri(post["properties"]["location"][0].as_str().unwrap())
-                                        }
-                                    } else {
-                                        // It's an inner h-geo object
-                                        a."u-location"[href=post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))] {
-                                            // I'm a lazy bitch
-                                            @decode_geo_uri(&post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap())))
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                    @if post["properties"]["ate"].is_array() || post["properties"]["drank"].is_array() {
-                        div {
-                            @if post["properties"]["ate"].is_array() {
-                                span { ul {
-                                    "Ate:"
-                                    @for food in post["properties"]["ate"].as_array().unwrap() {
-                                        li {
-                                            @if food.is_string() {
-                                                // If this is a string, it's a URL.
-                                                a."u-ate"[href=food.as_str().unwrap()] {
-                                                    @food.as_str().unwrap().truncate_ellipse(24).as_ref()
-                                                }
-                                            } else {
-                                                // This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
-                                                a."u-ate"[href=food["properties"]["uid"][0].as_str().unwrap()] {
-                                                    @food["properties"]["name"][0].as_str()
-                                                        .unwrap_or(food["properties"]["uid"][0].as_str().unwrap().truncate_ellipse(24).as_ref())
-                                                }
-                                            }
-                                        }
-                                    }
-                                } }
-                            }
-                            @if post["properties"]["drank"].is_array() {
-                                span { ul {
-                                    "Drank:"
-                                    @for food in post["properties"]["drank"].as_array().unwrap() {
-                                        li {
-                                            @if food.is_string() {
-                                                // If this is a string, it's a URL.
-                                                a."u-drank"[href=food.as_str().unwrap()] {
-                                                    @food.as_str().unwrap().truncate_ellipse(24).as_ref()
-                                                }
-                                            } else {
-                                                // This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
-                                                a."u-drank"[href=food["properties"]["uid"][0].as_str().unwrap()] {
-                                                    @food["properties"]["name"][0].as_str()
-                                                        .unwrap_or(food["properties"]["uid"][0].as_str().unwrap().truncate_ellipse(24).as_ref())
-                                                }
-                                            }
-                                        }
-                                    }
-                                } }
-                            }
-                        }
-                    }
-                }
-                @PhotoGallery { photos: post["properties"]["photo"].as_array() }
-                @if post["properties"]["content"][0]["html"].is_string() {
-                    main."e-content" {
-                        @markup::raw(post["properties"]["content"][0]["html"].as_str().unwrap().trim())
-                    }
-                }
-                @WebInteractions { post }
-            }
-        }
-        PhotoGallery<'a>(photos: Option<&'a Vec<serde_json::Value>>) {
-            @if photos.is_some() {
-                @for photo in photos.unwrap() {
-                    @if photo.is_string() {
-                        img."u-photo"[src=photo.as_str().unwrap(), loading="lazy"];
-                    } else if photo.is_array() {
-                        @if photo["thumbnail"].is_string() {
-                            a."u-photo"[href=photo["value"].as_str().unwrap()] {
-                                img[src=photo["thumbnail"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
-                            }
-                        } else {
-                            img."u-photo"[src=photo["value"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
-                        }
-                    }
-                }
-            }
-        }
-        WebInteractions<'a>(post: &'a serde_json::Value) {
-            footer.webinteractions {
-                ul.counters {
-                    li {
-                        span.icon { "❤️" }
-                        span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) }
-                    }
-                    li {
-                        span.icon { "💬" }
-                        span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) }
-                    }
-                    li {
-                        span.icon { "🔄" }
-                        span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) }
-                    }
-                    li {
-                        span.icon { "🔖" }
-                        span.counter { @post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) }
-                    }
-                }
-                // Needs rich webmention support which may or may not depend on an MF2 parser
-                // Might circumvent with an external parser with CORS support
-                // why write this stuff in rust then tho
-                /*details {
-                    summary { "Show comments and reactions" }
-                    @if post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
-                        // Show a facepile of likes for a post
-                    }
-                    @if post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
-                        // Show a facepile of bookmarks for a post
-                    }
-                    @if post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
-                        // Show a facepile of reposts for a post
-                    }
-                    @if post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
-                        // Show all the comments recursively (so we could do Salmention with them)
-                    }
-                }*/
-            }
-        }
-        VCard<'a>(card: &'a serde_json::Value) {
-            article."h-card" {
-                @if card["properties"]["photo"][0].is_string() {
-                    img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()];
-                }
-                h1 {
-                    a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] {
-                        @card["properties"]["name"][0].as_str().unwrap()
-                    }
-                }
-                @if card["properties"]["pronoun"].is_array() {
-                    span {
-                        "("
-                        @for (i, pronoun) in card["properties"]["pronoun"].as_array().unwrap().iter().filter_map(|v| v.as_str()).enumerate() {
-                            span."p-pronoun" {
-                                @pronoun
-                            }
-                            // Insert commas between multiple sets of pronouns
-                            @if i < (card["properties"]["pronoun"].as_array().unwrap().len() - 1) {", "}
-                        }
-                        ")"
-                    }
-                }
-                @if card["properties"]["note"].is_array() {
-                    p."p-note" {
-                        @card["properties"]["note"][0]["value"].as_str().unwrap_or_else(|| card["properties"]["note"][0].as_str().unwrap())
-                    }
-                }
-                @if card["properties"]["url"].is_array() {
-                    ul {
-                        "Can be found elsewhere at:"
-                        @for url in card["properties"]["url"].as_array().unwrap().iter().filter_map(|v| v.as_str()).filter(|v| v != &card["properties"]["uid"][0].as_str().unwrap()).filter(|v| !v.starts_with(&card["properties"]["author"][0].as_str().unwrap())) {
-                            li { a."u-url"[href=url, rel="me"] { @url } }
-                        }
-                    }
-                }
-            }
-        }
-        Food<'a>(food: &'a serde_json::Value) {
-            article."h-food" {
-                header.metadata {
-                    h1 {
-                        a."p-name"."u-url"[href=food["properties"]["url"][0].as_str().unwrap()] {
-                            @food["properties"]["name"][0].as_str().unwrap()
-                        }
-                    }
-                }
-                @PhotoGallery { photos: food["properties"]["photo"].as_array() }
-            }
-        }
-        Feed<'a>(feed: &'a serde_json::Value) {
-            div."h-feed" {
-                div.metadata {
-                    @if feed["properties"]["name"][0].is_string() {
-                        h1."p-name".titanic {
-                            a[href=feed["properties"]["uid"][0].as_str().unwrap(), rel="feed"] {
-                                @feed["properties"]["name"][0].as_str().unwrap()
-                            }
-                        }
-                    }
-                }
-                @if feed["children"].is_array() {
-                    @for child in feed["children"].as_array().unwrap() {
-                        @match child["type"][0].as_str().unwrap() {
-                            "h-entry" => { @Entry { post: child } }
-                            "h-feed" => { @Feed { feed: child } }
-                            "h-event" => {
-                                @{error!("Templating error: h-events aren't implemented yet");}
-                            }
-                            "h-card" => { @VCard { card: child }}
-                            something_else => {
-                                @{error!("Templating error: found a {} object that couldn't be parsed", something_else);}
-                            }
-                        }
-                    }
-                }
-                @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == 0 {
-                    p {
-                        "Looks like you reached the end. Wanna jump back to the "
-                        a[href=feed["properties"]["uid"][0].as_str().unwrap()] {
-                            "beginning"
-                        } "?"
-                    }
-                }
-                @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == super::POSTS_PER_PAGE {
-                    a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string()
-                        + "?after=" + feed["children"][super::POSTS_PER_PAGE - 1]["properties"]["uid"][0].as_str().unwrap()] {
-                        "Older posts"
-                    }
-                }
-            }
-        }
-        MainPage<'a>(feed: &'a serde_json::Value, card: &'a serde_json::Value) {
-            .sidebyside {
-                @VCard { card }
-                #dynamicstuff {
-                    p { "This section will provide interesting statistics or tidbits about my life in this exact moment (with maybe a small delay)." }
-                    p { "It will probably require JavaScript to self-update, but I promise to keep this widget lightweight and open-source!" }
-                    p { small {
-                        "JavaScript isn't a menace, stop fearing it or I will switch to WebAssembly "
-                        "and knock your nico-nico-kneecaps so fast with its speed you won't even notice that... "
-                        small { "omae ha mou shindeiru" }
-                        @markup::raw("<!-- NANI?!!! -->")
-                    } }
-                }
-            }
-            @Feed { feed }
-        }
-        ErrorPage(code: StatusCode) {
-            h1 { @format!("HTTP {} {}", code, code.canonical_reason()) }
-            @match code {
-                StatusCode::Unauthorized => {
-                    p { "Looks like you need to authenticate yourself before seeing this page. Try logging in with IndieAuth using the Login button above!" }
-                }
-                StatusCode::Forbidden => {
-                    p { "Looks like you're forbidden from viewing this page." }
-                    p {
-                        "This might've been caused by being banned from viewing my website"
-                        "or simply by trying to see what you're not supposed to see, "
-                        "like a private post that's not intended for you. It's ok, it happens."
-                    }
-                }
-                StatusCode::Gone => {
-                    p { "Looks like the page you're trying to find is gone and is never coming back." }
-                }
-                StatusCode::UnavailableForLegalReasons => {
-                p { "The page is there, but I can't legally provide it to you because the censorship said so." }
-                }
-                StatusCode::NotFound => {
-                    p { "Looks like there's no such page. Maybe you or someone else mistyped a URL or my database experienced data loss." }
-                }
-                StatusCode::ImATeapot => {
-                    p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" }
-
-                    p {
-                        small { "I could brew you some coffee tho if we meet one day... "
-                        small { i { "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!" } } }
-                    }
-                }
-                _ => { p { "It seems like you have found an error. Not to worry, it has already been logged." } }
-            }
-            P { "For now, may I suggest to visit " a[href="/"] {"the main page"} " of this website?" }
-
-        }
-    }
-}
+mod templates;
 
 use templates::{ErrorPage, MainPage, OnboardingPage, Template};
 
diff --git a/src/frontend/templates.rs b/src/frontend/templates.rs
new file mode 100644
index 0000000..7ed1755
--- /dev/null
+++ b/src/frontend/templates.rs
@@ -0,0 +1,584 @@
+use super::IndiewebEndpoints;
+use ellipse::Ellipse;
+use http_types::StatusCode;
+use log::error;
+
+/// Return a pretty location specifier from a geo: URI.
+fn decode_geo_uri(uri: &str) -> String {
+    if let Some(part) = uri.split(':').collect::<Vec<_>>().get(1) {
+        if let Some(part) = part.split(';').next() {
+            let mut parts = part.split(',');
+            let lat = parts.next().unwrap();
+            let lon = parts.next().unwrap();
+            // TODO - format them as proper latitude and longitude
+            return format!("{}, {}", lat, lon);
+        } else {
+            uri.to_string()
+        }
+    } else {
+        uri.to_string()
+    }
+}
+
+markup::define! {
+    Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, content: String) {
+        @markup::doctype()
+        html {
+            head {
+                title { @title }
+                link[rel="preconnect", href="https://fonts.gstatic.com"];
+                link[rel="stylesheet", href="/static/style.css"];
+                meta[name="viewport", content="initial-scale=1, width=device-width"];
+                // TODO: link rel= for common IndieWeb APIs: webmention, microsub
+                link[rel="micropub", href="/micropub"]; // Static, because it's built into the server itself
+                link[rel="authorization_endpoint", href=&endpoints.authorization_endpoint];
+                link[rel="token_endpoint", href=&endpoints.token_endpoint];
+                @if endpoints.webmention.is_some() {
+                    link[rel="webmention", href=&endpoints.webmention.as_ref()];
+                }
+                @if endpoints.microsub.is_some() {
+                    link[rel="microsub", href=&endpoints.microsub.as_ref()];
+                }
+            }
+            body {
+                nav#headerbar {
+                    // TODO: Find a way to store the website name somewhere in the database
+                    // Maybe in the settings?
+                    ul {
+                        li { a#homepage[href="/"] { @blog_name } }
+                        li.shiftright { a#login[href="/login"] { "Login" } }
+                    }
+                }
+                main {
+                    @markup::raw(content)
+                }
+            }
+        }
+    }
+    OnboardingPage {
+        h1[style="text-align: center"] {
+            "Welcome to Kittybox"
+        }
+        script[type="module", src="/static/onboarding.js"] {}
+        link[rel="stylesheet", href="/static/onboarding.css"];
+        form.onboarding[action="/", method="POST"] {
+            noscript {
+                p {
+                    "Ok, let's be honest. Most of this software doesn't require JS to be enabled "
+                    "to view pages (and in some cases, even edit them if logged in)."
+                }
+                p { "This page is a little bit different. It uses JavaScript to provide interactive features, such as:" }
+                ul {
+                    li { "Multiple-input questions" }
+                    li { "Answers spanning multiple fields" }
+                    li { "Preview of files being uploaded" }
+                    li { "Pretty pagination so you won't feel overwhelmed" }
+                }
+                p {
+                    "Sadly, it's very hard or even impossible to recreate this without any JavaScript. "
+                    "Good news though - the code is " b { "open-source AND free software" }
+                    " (under MIT (X11) or Apache-2.0 license - your choice) "
+                    "and I promise to not obfuscate it or minify it. "
+                    a[href="/static/onboarding.js"] { "Here" }
+                    "'s the link - you can try reading it so you'll be 200% sure "
+                    "it won't steal your cookies or turn your kitty into a soulless monster."
+                    @markup::raw("<!-- do cats even have souls? I'm not sure. But this code won't steal their souls anyway. -->")
+                }
+                hr;
+                p { "In other words: " b { "please enable JavaScript for this page to work properly." } small { "sorry T__T" } }
+            }
+            ul#progressbar[style="display: none"] {
+                li#intro { "Introduction" }
+                li#hcard { "Your profile" }
+                li#settings { "Your website" }
+                li#firstpost { "Your first post" }
+            }
+            fieldset#intro[style="display: none"] {
+                legend { "Introduction" }
+                p {
+                    "Kittybox is a CMS that can act as a member of the IndieWeb. "
+                    "IndieWeb is a global distributed social network built on top of open Web standards "
+                    "and composed of blogs around the Internet supporting these standards."
+                }
+                p { "There is no registration or centralized database of any sort - everyone owns their data and is responsible for it." }
+                p { "If you're seeing this page, it looks like your configuration is correct and we can proceed with the setup." }
+
+                div.switch_card_buttons {
+                    button.switch_card.next_card[type="button", "data-card"="hcard"] { "Next" }
+                }
+            }
+
+            fieldset#hcard[style="display: none"] {
+                legend { "Your profile" }
+                p { "An h-card is an IndieWeb social profile, and we're gonna make you one!" }
+                p { "Thanks to some clever markup, it will be readable by both humans and machines looking at your homepage."}
+                p {
+                    "If you make a mistake, don't worry, you're gonna be able to edit this later."
+                    "The only mandatory field is your name."
+                }
+
+                div.form_group {
+                    label[for="hcard_name"] { "Your name" }
+                    input#hcard_name[name="hcard_name", placeholder="Your name"];
+                    small {
+                        "No need to write the name as in your passport, this is not a legal document "
+                        "- just write how you want to be called on the network. This name will be also "
+                        "shown whenever you leave a comment on someone else's post using your website."
+                    }
+                }
+
+                div.form_group {
+                    label[for="pronouns"] { "Your pronouns" }
+                    div.multi_input#pronouns {
+                        template {
+                            input#hcard_pronouns[name="hcard_pronouns", placeholder="they/them?"];
+                        }
+                        button.add_more[type="button", "aria-label"="Add more"] { "[+] Add more" }
+                    }
+                    small {
+                        "Write which pronouns you use for yourself. It's a free-form field "
+                        "so don't feel constrained - but keep it compact, as it'll be shown in a lot of places."
+                    }
+                }
+
+                div.form_group {
+                    label[for="urls"] { "Links to other pages of you" }
+                    div.multi_input#urls {
+                        template {
+                            input#hcard_url[name="hcard_url", placeholder="https://example.com/"];
+                        }
+                        button.add_more[type="button", "aria-label"="Add more"] { "[+] Add more" }
+                    }
+                    small {
+                        "These URLs will help your readers find you elsewhere and will help you that whoever owns these pages owns your website too"
+                        " in case the links are mutual. So make sure to put a link to your site in your other social profiles!"
+                    }
+                }
+
+                div.form_group {
+                    label[for="hcard_note"] { "A little about yourself" }
+                    textarea#hcard_note[name="hcard_note", placeholder="Loves cooking, plants, cats, dogs and racoons."] {}
+                    small { "A little bit of introduction. Just one paragraph, and note, you can't use HTML here (yet)." }
+                    // TODO: HTML e-note instead of p-note
+                }
+
+                // TODO: u-photo upload - needs media endpoint cooperation
+
+                div.switch_card_buttons {
+                    button.switch_card.prev_card[type="button", "data-card"="intro"] { "Previous" }
+                    button.switch_card.next_card[type="button", "data-card"="settings"] { "Next" }
+                }
+            }
+
+            fieldset#settings[style="display: none"] {
+                legend { "Your website" }
+                p { "Ok, it's nice to know you more. Tell me about what you'll be writing and how you want to name your blog." }
+                // TODO: site-name, saved to settings
+
+                div.form_group {
+                    label[for="blog_name"] { "Your website's name"}
+                    input#blog_name[name="blog_name", placeholder="Kitty Box!"];
+                    small { "It'll get shown in the title of your blog, in the upper left corner!" }
+                }
+
+                div.form_group {
+                    label[for="custom_feeds"] { "Custom feeds" }
+                    small {
+                        p {
+                            "You can set up custom feeds to post your stuff to. "
+                            "This is a nice way to organize stuff into huge folders, like all your trips or your quantified-self data."
+                        }
+                        p {
+                            "Feeds can be followed individually, which makes it easy for users who are interested in certain types "
+                            "of content you produce to follow your adventures in certain areas of your life without cluttering their "
+                            "readers."
+                        }
+                        p {
+                            "We will automatically create some feeds for you aside from these so you won't have to - including a main feed, "
+                            "address book (for venues you go to and people you talk about), a cookbook for your recipes and some more."
+                            // TODO: Put a link to documentation explaining feeds in more detail.
+                        }
+                    }
+                    div.multi_input#custom_feeds {
+                        template {
+                            fieldset.feed {
+                                div.form_group {
+                                    label[for="feed_name"] { "Name" }
+                                    input#feed_name[name="feed_name", placeholder="My cool feed"];
+                                    small { "This is a name that will identify this feed to the user. Make it short and descriptive!" }
+                                }
+                                div.form_group {
+                                    label[for="feed_slug"] { "Slug" }
+                                    input#feed_slug[name="feed_slug", placeholder="my-cool-feed"];
+                                    small { "This will form a pretty URL for the feed. For example: https://example.com/feeds/my-cool-feed" }
+                                }
+                            }
+                        }
+                        button.add_more[type="button", "aria-label"="Add more"] { "[+] Add More" }
+                    }
+                }
+
+                div.switch_card_buttons {
+                    button.switch_card.prev_card[type="button", "data-card"="hcard"] { "Previous" }
+                    button.switch_card.next_card[type="button", "data-card"="firstpost"] { "Next" }
+                }
+            }
+
+            fieldset#firstpost[style="display: none"] {
+                legend { "Your first post" }
+                p { "Maybe you should start writing your first posts now. How about a short note?" }
+                p { "A note is a short-form post (not unlike a tweet - but without the actual character limit) that doesn't bear a title." }
+                p {
+                    "Consider telling more about yourself, your skills and interests in this note "
+                    @markup::raw("&mdash;")
+                    " though you're free to write anything you want. (By the way, you can use "
+                    a[href="https://daringfireball.net/projects/markdown/syntax"] { "Markdown" }
+                    " here to spice up your note!)"
+                }
+
+                textarea#first_post_content[style="width: 100%; height: 8em", placeholder="Hello! I am really excited about #IndieWeb"] {}
+
+                div.switch_card_buttons {
+                    button.switch_card.prev_card[type="button", "data-card"="settings"] { "Previous" }
+                    button[type="submit"] { "Finish" }
+                }
+            }
+        }
+    }
+    Entry<'a>(post: &'a serde_json::Value) {
+        article."h-entry" {
+            header.metadata {
+                @if post["properties"]["name"][0].is_string() {
+                    h1."p-name" {
+                        @post["properties"]["name"][0].as_str().unwrap()
+                    }
+                }
+                div {
+                    span {
+                        a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] {
+                            time."dt-published"[datetime=post["properties"]["published"][0].as_str().unwrap()] {
+                                @chrono::DateTime::parse_from_rfc3339(post["properties"]["published"][0].as_str().unwrap())
+                                    .map(|dt| dt.format("%a %b %e %T %Y").to_string())
+                                    .unwrap_or("ERROR: Couldn't parse the datetime".to_string())
+                            }
+                        }
+                    }
+                    @if post["properties"]["category"].is_array() {
+                        span {
+                            ul.categories {
+                                "Tagged: "
+                                @for cat in post["properties"]["category"].as_array().unwrap() {
+                                    li."p-category" { @cat.as_str().unwrap() }
+                                }
+                            }
+                        }
+                    }
+                    @if post["properties"]["in-reply-to"].is_array() {
+                        // TODO: Rich reply contexts - blocked on MF2 parser
+                        span {
+                            "In reply to: "
+                            ul.replyctx {
+                                @for ctx in post["properties"]["in-reply-to"].as_array().unwrap() {
+                                    li { a."u-in-reply-to"[href=ctx.as_str().unwrap()] {
+                                        @ctx.as_str().unwrap().truncate_ellipse(24).as_ref()
+                                    } }
+                                }
+                            }
+                        }
+                    }
+                }
+                @if post["properties"]["location"].is_array() || post["properties"]["checkin"].is_array() {
+                    div {
+                        @if post["properties"]["checkin"].is_array() {
+                            span {
+                                "Check-in to: "
+                                @if post["properties"]["checkin"][0].is_string() {
+                                    // It's a URL
+                                    a."u-checkin"[href=post["properties"]["checkin"][0].as_str().unwrap()] {
+                                        @post["properties"]["checkin"][0].as_str().unwrap().truncate_ellipse(24).as_ref()
+                                    }
+                                } else {
+                                    a."u-checkin"[href=post["properties"]["checkin"][0]["properties"]["uid"][0].as_str().unwrap()] {
+                                        @post["properties"]["checkin"][0]["properties"]["name"][0].as_str().unwrap()
+                                    }
+                                }
+                            }
+                        }
+                        @if post["properties"]["location"].is_array() {
+                            span {
+                                "Location: "
+                                @if post["properties"]["location"][0].is_string() {
+                                    // It's a geo: URL
+                                    // We need to decode it
+                                    a."u-location"[href=post["properties"]["location"][0].as_str().unwrap()] {
+                                        @decode_geo_uri(post["properties"]["location"][0].as_str().unwrap())
+                                    }
+                                } else {
+                                    // It's an inner h-geo object
+                                    a."u-location"[href=post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))] {
+                                        // I'm a lazy bitch
+                                        @decode_geo_uri(&post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap())))
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                @if post["properties"]["ate"].is_array() || post["properties"]["drank"].is_array() {
+                    div {
+                        @if post["properties"]["ate"].is_array() {
+                            span { ul {
+                                "Ate:"
+                                @for food in post["properties"]["ate"].as_array().unwrap() {
+                                    li {
+                                        @if food.is_string() {
+                                            // If this is a string, it's a URL.
+                                            a."u-ate"[href=food.as_str().unwrap()] {
+                                                @food.as_str().unwrap().truncate_ellipse(24).as_ref()
+                                            }
+                                        } else {
+                                            // This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
+                                            a."u-ate"[href=food["properties"]["uid"][0].as_str().unwrap()] {
+                                                @food["properties"]["name"][0].as_str()
+                                                    .unwrap_or(food["properties"]["uid"][0].as_str().unwrap().truncate_ellipse(24).as_ref())
+                                            }
+                                        }
+                                    }
+                                }
+                            } }
+                        }
+                        @if post["properties"]["drank"].is_array() {
+                            span { ul {
+                                "Drank:"
+                                @for food in post["properties"]["drank"].as_array().unwrap() {
+                                    li {
+                                        @if food.is_string() {
+                                            // If this is a string, it's a URL.
+                                            a."u-drank"[href=food.as_str().unwrap()] {
+                                                @food.as_str().unwrap().truncate_ellipse(24).as_ref()
+                                            }
+                                        } else {
+                                            // This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
+                                            a."u-drank"[href=food["properties"]["uid"][0].as_str().unwrap()] {
+                                                @food["properties"]["name"][0].as_str()
+                                                    .unwrap_or(food["properties"]["uid"][0].as_str().unwrap().truncate_ellipse(24).as_ref())
+                                            }
+                                        }
+                                    }
+                                }
+                            } }
+                        }
+                    }
+                }
+            }
+            @PhotoGallery { photos: post["properties"]["photo"].as_array() }
+            @if post["properties"]["content"][0]["html"].is_string() {
+                main."e-content" {
+                    @markup::raw(post["properties"]["content"][0]["html"].as_str().unwrap().trim())
+                }
+            }
+            @WebInteractions { post }
+        }
+    }
+    PhotoGallery<'a>(photos: Option<&'a Vec<serde_json::Value>>) {
+        @if photos.is_some() {
+            @for photo in photos.unwrap() {
+                @if photo.is_string() {
+                    img."u-photo"[src=photo.as_str().unwrap(), loading="lazy"];
+                } else if photo.is_array() {
+                    @if photo["thumbnail"].is_string() {
+                        a."u-photo"[href=photo["value"].as_str().unwrap()] {
+                            img[src=photo["thumbnail"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
+                        }
+                    } else {
+                        img."u-photo"[src=photo["value"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
+                    }
+                }
+            }
+        }
+    }
+    WebInteractions<'a>(post: &'a serde_json::Value) {
+        footer.webinteractions {
+            ul.counters {
+                li {
+                    span.icon { "❤️" }
+                    span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) }
+                }
+                li {
+                    span.icon { "💬" }
+                    span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) }
+                }
+                li {
+                    span.icon { "🔄" }
+                    span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) }
+                }
+                li {
+                    span.icon { "🔖" }
+                    span.counter { @post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) }
+                }
+            }
+            // Needs rich webmention support which may or may not depend on an MF2 parser
+            // Might circumvent with an external parser with CORS support
+            // why write this stuff in rust then tho
+            /*details {
+                summary { "Show comments and reactions" }
+                @if post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
+                    // Show a facepile of likes for a post
+                }
+                @if post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
+                    // Show a facepile of bookmarks for a post
+                }
+                @if post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
+                    // Show a facepile of reposts for a post
+                }
+                @if post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
+                    // Show all the comments recursively (so we could do Salmention with them)
+                }
+            }*/
+        }
+    }
+    VCard<'a>(card: &'a serde_json::Value) {
+        article."h-card" {
+            @if card["properties"]["photo"][0].is_string() {
+                img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()];
+            }
+            h1 {
+                a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] {
+                    @card["properties"]["name"][0].as_str().unwrap()
+                }
+            }
+            @if card["properties"]["pronoun"].is_array() {
+                span {
+                    "("
+                    @for (i, pronoun) in card["properties"]["pronoun"].as_array().unwrap().iter().filter_map(|v| v.as_str()).enumerate() {
+                        span."p-pronoun" {
+                            @pronoun
+                        }
+                        // Insert commas between multiple sets of pronouns
+                        @if i < (card["properties"]["pronoun"].as_array().unwrap().len() - 1) {", "}
+                    }
+                    ")"
+                }
+            }
+            @if card["properties"]["note"].is_array() {
+                p."p-note" {
+                    @card["properties"]["note"][0]["value"].as_str().unwrap_or_else(|| card["properties"]["note"][0].as_str().unwrap())
+                }
+            }
+            @if card["properties"]["url"].is_array() {
+                ul {
+                    "Can be found elsewhere at:"
+                    @for url in card["properties"]["url"].as_array().unwrap().iter().filter_map(|v| v.as_str()).filter(|v| v != &card["properties"]["uid"][0].as_str().unwrap()).filter(|v| !v.starts_with(&card["properties"]["author"][0].as_str().unwrap())) {
+                        li { a."u-url"[href=url, rel="me"] { @url } }
+                    }
+                }
+            }
+        }
+    }
+    Food<'a>(food: &'a serde_json::Value) {
+        article."h-food" {
+            header.metadata {
+                h1 {
+                    a."p-name"."u-url"[href=food["properties"]["url"][0].as_str().unwrap()] {
+                        @food["properties"]["name"][0].as_str().unwrap()
+                    }
+                }
+            }
+            @PhotoGallery { photos: food["properties"]["photo"].as_array() }
+        }
+    }
+    Feed<'a>(feed: &'a serde_json::Value) {
+        div."h-feed" {
+            div.metadata {
+                @if feed["properties"]["name"][0].is_string() {
+                    h1."p-name".titanic {
+                        a[href=feed["properties"]["uid"][0].as_str().unwrap(), rel="feed"] {
+                            @feed["properties"]["name"][0].as_str().unwrap()
+                        }
+                    }
+                }
+            }
+            @if feed["children"].is_array() {
+                @for child in feed["children"].as_array().unwrap() {
+                    @match child["type"][0].as_str().unwrap() {
+                        "h-entry" => { @Entry { post: child } }
+                        "h-feed" => { @Feed { feed: child } }
+                        "h-event" => {
+                            @{error!("Templating error: h-events aren't implemented yet");}
+                        }
+                        "h-card" => { @VCard { card: child }}
+                        something_else => {
+                            @{error!("Templating error: found a {} object that couldn't be parsed", something_else);}
+                        }
+                    }
+                }
+            }
+            @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == 0 {
+                p {
+                    "Looks like you reached the end. Wanna jump back to the "
+                    a[href=feed["properties"]["uid"][0].as_str().unwrap()] {
+                        "beginning"
+                    } "?"
+                }
+            }
+            @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == super::POSTS_PER_PAGE {
+                a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string()
+                    + "?after=" + feed["children"][super::POSTS_PER_PAGE - 1]["properties"]["uid"][0].as_str().unwrap()] {
+                    "Older posts"
+                }
+            }
+        }
+    }
+    MainPage<'a>(feed: &'a serde_json::Value, card: &'a serde_json::Value) {
+        .sidebyside {
+            @VCard { card }
+            #dynamicstuff {
+                p { "This section will provide interesting statistics or tidbits about my life in this exact moment (with maybe a small delay)." }
+                p { "It will probably require JavaScript to self-update, but I promise to keep this widget lightweight and open-source!" }
+                p { small {
+                    "JavaScript isn't a menace, stop fearing it or I will switch to WebAssembly "
+                    "and knock your nico-nico-kneecaps so fast with its speed you won't even notice that... "
+                    small { "omae ha mou shindeiru" }
+                    @markup::raw("<!-- NANI?!!! -->")
+                } }
+            }
+        }
+        @Feed { feed }
+    }
+    ErrorPage(code: StatusCode) {
+        h1 { @format!("HTTP {} {}", code, code.canonical_reason()) }
+        @match code {
+            StatusCode::Unauthorized => {
+                p { "Looks like you need to authenticate yourself before seeing this page. Try logging in with IndieAuth using the Login button above!" }
+            }
+            StatusCode::Forbidden => {
+                p { "Looks like you're forbidden from viewing this page." }
+                p {
+                    "This might've been caused by being banned from viewing my website"
+                    "or simply by trying to see what you're not supposed to see, "
+                    "like a private post that's not intended for you. It's ok, it happens."
+                }
+            }
+            StatusCode::Gone => {
+                p { "Looks like the page you're trying to find is gone and is never coming back." }
+            }
+            StatusCode::UnavailableForLegalReasons => {
+            p { "The page is there, but I can't legally provide it to you because the censorship said so." }
+            }
+            StatusCode::NotFound => {
+                p { "Looks like there's no such page. Maybe you or someone else mistyped a URL or my database experienced data loss." }
+            }
+            StatusCode::ImATeapot => {
+                p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" }
+
+                p {
+                    small { "I could brew you some coffee tho if we meet one day... "
+                    small { i { "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!" } } }
+                }
+            }
+            _ => { p { "It seems like you have found an error. Not to worry, it has already been logged." } }
+        }
+        P { "For now, may I suggest to visit " a[href="/"] {"the main page"} " of this website?" }
+
+    }
+}
\ No newline at end of file