about summary refs log tree commit diff
path: root/templates
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-05-24 17:18:30 +0300
committerVika <vika@fireburn.ru>2022-05-24 17:18:30 +0300
commit5610a5f0bf1a9df02bd3d5b55e2cdebef2440360 (patch)
tree8394bcf1dcc204043d7adeb8dde2e2746977606e /templates
parent2f93873122b47e42f7ee1c38f1f04d052a63599c (diff)
downloadkittybox-5610a5f0bf1a9df02bd3d5b55e2cdebef2440360.tar.zst
flake.nix: reorganize
 - Kittybox's source code is moved to a subfolder
   - This improves build caching by Nix since it doesn't take changes
     to other files into account
 - Package and test definitions were spun into separate files
   - This makes my flake.nix much easier to navigate
   - This also makes it somewhat possible to use without flakes (but
     it is still not easy, so use flakes!)
 - Some attributes were moved in compliance with Nix 2.8's changes to
   flake schema
Diffstat (limited to 'templates')
-rw-r--r--templates/Cargo.toml26
-rw-r--r--templates/src/lib.rs347
-rw-r--r--templates/src/login.rs17
-rw-r--r--templates/src/onboarding.rs192
-rw-r--r--templates/src/templates.rs545
5 files changed, 0 insertions, 1127 deletions
diff --git a/templates/Cargo.toml b/templates/Cargo.toml
deleted file mode 100644
index fe8ac19..0000000
--- a/templates/Cargo.toml
+++ /dev/null
@@ -1,26 +0,0 @@
-[package]
-name = "kittybox-templates"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dev-dependencies]
-faker_rand = "^0.1.1"
-rand = "^0.8.5"
-test-logger = "^0.1.0"
-[dev-dependencies.microformats]
-version="^0.2.0"
-
-[dependencies]
-ellipse = "^0.2.0"           # Truncate and ellipsize strings in a human-friendly way
-http = "^0.2.7"              # Hyper's strong HTTP types
-log = "^0.4.14"              # A lightweight logging facade for Rust
-markup = "^0.12.0"           # HTML templating engine
-serde_json = "^1.0.64"       # A JSON serialization file format
-[dependencies.chrono]        # Date and time library for Rust
-version = "^0.4.19"
-features = ["serde"]
-[dependencies.kittybox-util]
-version = "0.1.0"
-path = "../util"
\ No newline at end of file
diff --git a/templates/src/lib.rs b/templates/src/lib.rs
deleted file mode 100644
index 39f1075..0000000
--- a/templates/src/lib.rs
+++ /dev/null
@@ -1,347 +0,0 @@
-mod templates;
-pub use templates::{ErrorPage, MainPage, Template, POSTS_PER_PAGE, Entry, VCard, Feed};
-mod onboarding;
-pub use onboarding::OnboardingPage;
-mod login;
-pub use login::LoginPage;
-
-#[cfg(test)]
-mod tests {
-    use faker_rand::lorem::Word;
-    use serde_json::json;
-    use microformats::types::{Document, Item, PropertyValue, Url};
-    use std::cell::RefCell;
-    use std::rc::Rc;
-    use faker_rand::en_us::internet::Domain;
-
-    enum PostType {
-        Note,
-        Article,
-        ReplyTo(serde_json::Value),
-        ReplyToLink(String),
-        LikeOf(serde_json::Value),
-        LikeOfLink(String)
-    }
-
-    fn gen_hcard(domain: &str) -> serde_json::Value {
-        use faker_rand::en_us::names::FirstName;
-
-        json!({
-            "type": ["h-card"],
-            "properties": {
-                "name": [rand::random::<FirstName>().to_string()],
-                "photo": [format!("https://{domain}/media/me.png")],
-                "uid": [format!("https://{domain}/")],
-                "url": [format!("https://{domain}/")]
-            }
-        })
-    }
-    
-    fn gen_random_post(domain: &str, kind: PostType) -> serde_json::Value {
-        use faker_rand::lorem::{Paragraph, Sentence};
-
-        fn html(content: Paragraph) -> serde_json::Value {
-            json!({
-                "html": format!("<p>{}</p>", content),
-                "value": content.to_string()
-            })
-        }
-            
-        let uid = format!(
-            "https://{domain}/posts/{}-{}-{}",
-            rand::random::<Word>(), rand::random::<Word>(), rand::random::<Word>()
-        );
-        let dt = chrono::offset::Local::now()
-            .to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
-
-        match kind {
-            PostType::Note => {
-                let content = rand::random::<Paragraph>();
-
-                json!({
-                    "type": ["h-entry"],
-                    "properties": {
-                        "content": [html(content)],
-                        "published": [dt],
-                        "uid": [&uid], "url": [&uid],
-                        "author": [gen_hcard(domain)]
-                    }
-                })
-            }
-            PostType::Article => {
-                let content = rand::random::<Paragraph>();
-                let name = rand::random::<Sentence>();
-
-                json!({
-                    "type": ["h-entry"],
-                    "properties": {
-                        "content": [html(content)],
-                        "published": [dt],
-                        "uid": [&uid], "url": [&uid],
-                        "author": [gen_hcard(domain)],
-                        "name": [name.to_string()]
-                    }
-                })
-            }
-            PostType::ReplyTo(ctx) => {
-                let content = rand::random::<Paragraph>();
-
-                json!({
-                    "type": ["h-entry"],
-                    "properties": {
-                        "content": [html(content)],
-                        "published": [dt],
-                        "uid": [&uid], "url": [&uid],
-                        "author": [gen_hcard(domain)],
-                        "in-reply-to": [{
-                            "type": ["h-cite"],
-                            "properties": ctx["properties"]
-                        }]
-                    }
-                })
-            },
-            PostType::ReplyToLink(link) => {
-                let content = rand::random::<Paragraph>();
-
-                json!({
-                    "type": ["h-entry"],
-                    "properties": {
-                        "content": [html(content)],
-                        "published": [dt],
-                        "uid": [&uid], "url": [&uid],
-                        "author": [gen_hcard(domain)],
-                        "in-reply-to": [link]
-                    }
-                })
-            },
-            PostType::LikeOf(ctx) => {
-                json!({
-                    "type": ["h-entry"],
-                    "properties": {
-                        "published": [dt],
-                        "author": [gen_hcard(domain)],
-                        "uid": [&uid], "url": [&uid],
-                        "like-of": [{
-                            "type": ["h-cite"],
-                            "properties": ctx["properties"]
-                        }]
-                    }
-                })
-            },
-            PostType::LikeOfLink(link) => {
-                json!({
-                    "type": ["h-entry"],
-                    "properties": {
-                        "published": [dt],
-                        "author": [gen_hcard(domain)],
-                        "uid": [&uid], "url": [&uid],
-                        "like-of": [link]
-                    }
-                })
-            }
-        }
-    }
-
-    fn check_dt_published(
-        mf2: &serde_json::Value,
-        item: &Rc<RefCell<Item>>
-    ) {
-        use microformats::types::temporal::Value as TemporalValue;
-
-        let _item = item.borrow();
-        let props = _item.properties.borrow();
-        assert!(props.contains_key("published"));
-
-        if let Some(PropertyValue::Temporal(
-            TemporalValue::Timestamp(item)
-        )) = props.get("published")
-            .and_then(|v| v.first())
-        {
-            use chrono::{DateTime, FixedOffset, NaiveDateTime};
-
-            // Faithfully reconstruct the original datetime
-            // I wonder why not just have an Enum that would
-            // get you either date, time or a datetime,
-            // potentially with an offset?
-            let offset = item.as_offset().unwrap().data;
-            let ndt: NaiveDateTime = item.as_date().unwrap().data
-                .and_time(item.as_time().unwrap().data)
-            // subtract the offset here, since we will add it back
-                - offset;
-            let dt = DateTime::<FixedOffset>::from_utc(ndt, offset);
-
-            let expected: DateTime<FixedOffset> = chrono::DateTime::parse_from_rfc3339(
-                mf2["properties"]["published"][0].as_str().unwrap()
-            ).unwrap();
-
-            assert_eq!(dt, expected);
-        } else {
-            unreachable!()
-        }
-    }
-
-    fn check_e_content(
-        mf2: &serde_json::Value,
-        item: &Rc<RefCell<Item>>
-    ) {
-        let _item = item.borrow();
-        let props = _item.properties.borrow();
-        assert!(props.contains_key("content"));
-
-        if let Some(PropertyValue::Fragment(content)) =
-            props.get("content")
-            .and_then(|v| v.first())
-        {
-            assert_eq!(
-                content.html,
-                mf2["properties"]["content"][0]["html"].as_str().unwrap()
-            );
-        } else {
-            unreachable!()
-        }
-
-    }
-
-    #[test]
-    #[ignore = "see https://gitlab.com/maxburon/microformats-parser/-/issues/7"]
-    fn test_note() {
-        test_logger::ensure_env_logger_initialized();
-
-        let mf2 = gen_random_post(
-            &rand::random::<Domain>().to_string(),
-            PostType::Note
-        );
-
-        let html = crate::templates::Entry {
-            post: &mf2
-        }.to_string();
-
-        let url: Url = mf2.pointer("/properties/uid/0")
-            .and_then(|i| i.as_str())
-            .and_then(|u| u.parse().ok())
-            .unwrap();
-        let parsed: Document = microformats::from_html(&html, url.clone()).unwrap();
-
-        if let Some(PropertyValue::Item(item)) = parsed.get_item_by_url(&url) {
-            let _item = item.borrow();
-            let props = _item.properties.borrow();
-
-            check_e_content(&mf2, &item);
-            check_dt_published(&mf2, &item);
-            assert!(props.contains_key("uid"));
-            assert!(props.contains_key("url"));
-            assert!(props.get("url")
-                    .unwrap()
-                    .iter()
-                    .any(|i| i == props.get("uid").and_then(|v| v.first()).unwrap()));
-            // XXX: fails because of https://gitlab.com/maxburon/microformats-parser/-/issues/7
-            assert!(!props.contains_key("name"));
-
-        } else {
-            unreachable!()
-        }
-    }
-
-    #[test]
-    fn test_article() {
-        test_logger::ensure_env_logger_initialized();
-
-        let mf2 = gen_random_post(
-            &rand::random::<Domain>().to_string(),
-            PostType::Article
-        );
-        let html = crate::templates::Entry {
-            post: &mf2
-        }.to_string();
-        let url: Url = mf2.pointer("/properties/uid/0")
-            .and_then(|i| i.as_str())
-            .and_then(|u| u.parse().ok())
-            .unwrap();
-        let parsed: Document = microformats::from_html(&html, url.clone()).unwrap();
-
-        if let Some(PropertyValue::Item(item)) = parsed.get_item_by_url(&url) {
-            let _item = item.borrow();
-            let props = _item.properties.borrow();
-
-            check_e_content(&mf2, &item);
-            check_dt_published(&mf2, &item);
-            assert!(props.contains_key("uid"));
-            assert!(props.contains_key("url"));
-            assert!(props.get("url")
-                    .unwrap()
-                    .iter()
-                    .any(|i| i == props.get("uid").and_then(|v| v.first()).unwrap()));
-            assert!(props.contains_key("name"));
-            if let Some(PropertyValue::Plain(name)) = props.get("name").and_then(|v| v.first()) {
-                assert_eq!(
-                    name,
-                    mf2.pointer("/properties/name/0")
-                        .and_then(|v| v.as_str())
-                        .unwrap()
-                );
-            } else {
-                panic!("Name wasn't a plain property!");
-            }
-        } else {
-            unreachable!()
-        }
-    }
-
-    #[test]
-    fn test_like_of() {
-        test_logger::ensure_env_logger_initialized();
-
-        for likeof in [
-            PostType::LikeOf(gen_random_post(
-                &rand::random::<Domain>().to_string(),
-                PostType::Note
-            )),
-            PostType::LikeOfLink(format!(
-                "https://{}/posts/{}-{}-{}",
-                &rand::random::<Domain>(),
-                &rand::random::<Word>(),
-                &rand::random::<Word>(),
-                &rand::random::<Word>(),
-            ))
-        ] {
-            let mf2 = gen_random_post(
-                &rand::random::<Domain>().to_string(),
-                likeof
-            );
-            let url: Url = mf2.pointer("/properties/uid/0")
-                .and_then(|i| i.as_str())
-                .and_then(|u| u.parse().ok())
-                .unwrap();
-            let html = crate::templates::Entry {
-                post: &mf2
-            }.to_string();
-            let parsed: Document = microformats::from_html(&html, url.clone()).unwrap();
-
-            if let Some(item) = parsed.items.get(0) {
-                let _item = item.borrow();
-                let props = _item.properties.borrow();
-
-                check_dt_published(&mf2, item);
-                assert!(props.contains_key("like-of"));
-                match props.get("like-of").and_then(|v| v.first()) {
-                    Some(PropertyValue::Url(url)) => {
-                        assert_eq!(
-                            url,
-                            &mf2.pointer("/properties/like-of/0")
-                                .and_then(|i| i.as_str())
-                                .or_else(|| mf2.pointer("/properties/like-of/0/properties/uid/0").and_then(|i| i.as_str()))
-                                .and_then(|u| u.parse::<Url>().ok())
-                                .unwrap()
-                        );
-                    }
-                    Some(PropertyValue::Item(_cite)) => {
-                        todo!()
-                    }
-                    other => panic!("Unexpected value in like-of: {:?}", other)
-                }
-            } else {
-                unreachable!()
-            }
-        }
-    }
-}
diff --git a/templates/src/login.rs b/templates/src/login.rs
deleted file mode 100644
index 042c308..0000000
--- a/templates/src/login.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-markup::define! {
-    LoginPage {
-        form[method="POST"] {
-            h1 { "Sign in with your website" }
-            p {
-                "Signing in to Kittybox might allow you to view private content "
-                    "intended for your eyes only."
-            }
-
-            section {
-                label[for="url"] { "Your website URL" }
-                input[id="url", name="url", placeholder="https://example.com/"];
-                input[type="submit"];
-            }
-        }
-    }
-}
diff --git a/templates/src/onboarding.rs b/templates/src/onboarding.rs
deleted file mode 100644
index 9d0f2e1..0000000
--- a/templates/src/onboarding.rs
+++ /dev/null
@@ -1,192 +0,0 @@
-markup::define! {
-    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 GNU AGPLv3) "
-                    "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" }
-                }
-            }
-        }
-    }
-}
diff --git a/templates/src/templates.rs b/templates/src/templates.rs
deleted file mode 100644
index 0054c91..0000000
--- a/templates/src/templates.rs
+++ /dev/null
@@ -1,545 +0,0 @@
-use kittybox_util::{MicropubChannel, IndiewebEndpoints};
-use ellipse::Ellipse;
-use http::StatusCode;
-use log::error;
-
-pub static POSTS_PER_PAGE: usize = 20;
-
-/// 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: Option<IndiewebEndpoints>, feeds: Vec<MicropubChannel>, user: Option<String>, 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
-                @if let Some(endpoints) = endpoints {
-                    link[rel="authorization_endpoint", href=&endpoints.authorization_endpoint];
-                    link[rel="token_endpoint", href=&endpoints.token_endpoint];
-                    @if let Some(webmention) = &endpoints.webmention {
-                        link[rel="webmention", href=&webmention];
-                    }
-                    @if let Some(microsub) = &endpoints.microsub {
-                        link[rel="microsub", href=&microsub];
-                    }
-                }
-            }
-            body {
-                // TODO Somehow compress headerbar into a menu when the screen space is tight
-                nav #headerbar {
-                    ul {
-                        li { a #homepage[href="/"] { @blog_name } }
-                        @for feed in feeds.iter() {
-                            li { a[href=&feed.uid] { @feed.name } }
-                        }
-                        li.shiftright {
-                            @if user.is_none() {
-                                a #login[href="/login"] { "Sign in" }
-                            } else {
-                                span {
-                                    @user.as_ref().unwrap() " - " a #logout[href="/logout"] { "Sign out" }
-                                }
-                            }
-                        }
-                    }
-                }
-                main {
-                    @markup::raw(content)
-                }
-                footer {
-                    p {
-                        "Powered by " a[href="https://sr.ht/~vikanezrimaya/kittybox"] {
-                            "Kittybox"
-                        }
-                    }
-                }
-            }
-        }
-    }
-    Entry<'a>(post: &'a serde_json::Value) {
-        @if post.pointer("/properties/like-of").is_none() && post.pointer("/properties/bookmark-of").is_none() {
-            @FullEntry { post }
-        } else {
-            // Show a mini-post.
-            @MiniEntry { post }
-        }
-    }
-    MiniEntry<'a>(post: &'a serde_json::Value) {
-        article."h-entry mini-entry" {
-            @if let Some(author) = post["properties"]["author"][0].as_object() {
-                span."mini-h-card"."u-author" {
-                    a."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] {
-                        @if let Some(photo) = author["properties"]["photo"][0].as_str() {
-                            img[src=photo, loading="lazy"];
-                        }
-                        @author["properties"]["name"][0].as_str().unwrap()
-                    }
-                }
-                @if let Some(likeof) = post["properties"]["like-of"][0].as_str() {
-                    " ❤️ "
-                    a."u-like-of"[href=likeof] { @likeof }
-                } else if let Some(likeof) = post["properties"]["like-of"][0].as_object() {
-                    a."u-like-of"[href=likeof["properties"]["url"][0].as_str().unwrap()] {
-                        @likeof["properties"]["name"][0]
-                            .as_str()
-                            .unwrap_or_else(|| likeof["properties"]["url"][0].as_str().unwrap())
-                    }
-                }
-                @if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_str() {
-                    " 🔖 "
-                    a."u-bookmark-of"[href=bookmarkof] { @bookmarkof }
-                } else if let Some(bookmarkof) = post["properties"]["bookmark-of"][0].as_object() {
-                    a."u-bookmark-of"[href=bookmarkof["properties"]["url"][0].as_str().unwrap()] {
-                        @bookmarkof["properties"]["name"][0]
-                            .as_str()
-                            .unwrap_or_else(|| bookmarkof["properties"]["url"][0].as_str().unwrap())
-                    }
-                }
-                @if let Some(published) = post["properties"]["published"][0].as_str() {
-                    time."dt-published"[datetime=published] {
-                        @chrono::DateTime::parse_from_rfc3339(published)
-                            .map(|dt| dt.format("on %a %b %e %T %Y").to_string())
-                            .unwrap_or("sometime in the past".to_string())
-                    }
-                }
-            }
-        }
-    }
-    FullEntry<'a>(post: &'a serde_json::Value) {
-        article."h-entry" {
-            header.metadata {
-                @if let Some(name) = post["properties"]["name"][0].as_str() {
-                    h1."p-name" { @name }
-                }
-                @if let Some(author) = post["properties"]["author"][0].as_object() {
-                    section."mini-h-card" {
-                        a.larger."u-author"[href=author["properties"]["uid"][0].as_str().unwrap()] {
-                            @if let Some(photo) = author["properties"]["photo"][0].as_str() {
-                                img[src=photo, loading="lazy"];
-                            }
-                            @author["properties"]["name"][0].as_str().unwrap()
-                        }
-                    }
-                }
-                div {
-                    span {
-                        a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] {
-                            @if let Some(published) = post["properties"]["published"][0].as_str() {
-                                time."dt-published"[datetime=published] {
-                                    @chrono::DateTime::parse_from_rfc3339(published)
-                                        .map(|dt| dt.format("%a %b %e %T %Y").to_string())
-                                        .unwrap_or("sometime in the past".to_string())
-                                }
-                            }
-                        }
-                    }
-                    @if post["properties"]["visibility"][0].as_str().unwrap_or("public") != "public" {
-                        span."p-visibility"[value=post["properties"]["visibility"][0].as_str().unwrap()] {
-                            @post["properties"]["visibility"][0].as_str().unwrap()
-                        }
-                    }
-                    @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"]["url"].as_array().unwrap().len() > 1 {
-                    hr;
-                    ul {
-                        "Pretty permalinks for this post:"
-                        @for url in post["properties"]["url"].as_array().unwrap().iter().filter(|i| **i != post["properties"]["uid"][0]).map(|i| i.as_str().unwrap()) {
-                            li {
-                                a."u-url"[href=url] { @url }
-                            }
-                        }
-                    }
-                }
-                @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_or("#")] {
-                                                @food["properties"]["name"][0].as_str()
-                                                    .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").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_or("#")] {
-                                                @food["properties"]["name"][0].as_str()
-                                                    .unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").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 let Some(photos) = photos {
-            @for photo in photos.iter() {
-                @if let Some(photo) = photo.as_str() {
-                    img."u-photo"[src=photo, loading="lazy"];
-                } else if photo.is_object() {
-                    @if let Some(thumbnail) = photo["thumbnail"].as_str() {
-                        a."u-photo"[href=photo["value"].as_str().unwrap()] {
-                            img[src=thumbnail,
-                                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) }
-                }
-            }
-            /*@if (
-                post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0)
-                    + post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0)
-                    + post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0)
-                    + post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0)
-            ) > 0 {
-                details {
-                    summary { "Show comments and reactions" }
-                    // TODO actually render facepiles and comments
-                    @if let Some(likes) = post["properties"]["like"].as_array() {
-                        @if !likes.is_empty() {
-                            // Show a facepile of likes for a post
-                        }
-                    }
-                    @if let Some(bookmarks) = post["properties"]["bookmark"].as_array() {
-                        @if !bookmarks.is_empty() {
-                            // Show a facepile of bookmarks for a post
-                        }
-                    }
-                    @if let Some(reposts) = post["properties"]["repost"].as_array() {
-                        @if !reposts.is_empty() {
-                            // Show a facepile of reposts for a post
-                        }
-                    }
-                    @if let Some(comments) = post["properties"]["comment"].as_array() {
-                        @for comment in comments.iter() {
-                            // 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, msg: Option<String>) {
-        h1 { @format!("HTTP {}", code) }
-        @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::UNAVAILABLE_FOR_LEGAL_REASONS => {
-            p { "The page is there, but I can't legally provide it to you because the censorship said so." }
-            }
-            StatusCode::NOT_FOUND => {
-                p { "Looks like there's no such page. Maybe you or someone else mistyped a URL or my database experienced data loss." }
-            }
-            StatusCode::IM_A_TEAPOT => {
-                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!!!~ >.<!"
-                            }
-                        }
-                    }
-                }
-            }
-            StatusCode::BAD_REQUEST => {
-                @match msg {
-                    None => {
-                        p {
-                            "There was an undescribed error in your request. "
-                            "Please try again later or with a different request."
-                        }
-                    }
-                    Some(msg) => {
-                        p {
-                            "There was a following error in your request:"
-                        }
-                        blockquote { pre { @msg } }
-                    }
-                }
-            }
-            StatusCode::INTERNAL_SERVER_ERROR => {
-                @match msg {
-                    None => {
-                        p { "It seems like you have found an error. Not to worry, it has already been logged." }
-                    }
-                    Some(msg) => {
-                        p { "The server encountered an error while processing your request:" }
-                        blockquote { @msg }
-                        p { "Don't worry, it has already been logged." }
-                    }
-                }
-            }
-            _ => {
-                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?" }
-
-    }
-}