···11+# webbed site
22+33+```sh
44+gleam run
55+./serve💅.py
66+```
77+88+# Credits
99+1010+- The [Myna](https://github.com/sayyadirfanali/Myna) monospace font
1111+- [Inclusive Sans](https://github.com/LivKing/Inclusive-Sans)
···11+name = "webbed_site"
22+version = "1.0.0"
33+44+# Fill out these fields if you intend to generate HTML documentation or publish
55+# your project to the Hex package manager.
66+#
77+# description = ""
88+# licences = ["Apache-2.0"]
99+# repository = { type = "github", user = "", repo = "" }
1010+# links = [{ title = "Website", href = "" }]
1111+#
1212+# For a full reference of all the available options, you can have a look at
1313+# https://gleam.run/writing-gleam/gleam-toml/.
1414+1515+[dependencies]
1616+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
1717+# lustre ssg has a lot of outdated deps :/
1818+lustre_ssg = { git = "https://github.com/fruno-bulax/lustre_ssg", ref = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" }
1919+lustre = ">= 5.4.0 and < 6.0.0"
2020+gleam_time = ">= 1.6.0 and < 2.0.0"
2121+simplifile = ">= 2.3.2 and < 3.0.0"
2222+shellout = ">= 1.7.0 and < 2.0.0"
2323+jot = ">= 8.0.0 and < 9.0.0"
2424+tom = ">= 2.0.0 and < 3.0.0"
+34
manifest.toml
···11+# This file was generated by Gleam
22+# You typically do not need to edit this file
33+44+packages = [
55+ { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" },
66+ { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
77+ { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
88+ { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
99+ { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
1010+ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
1111+ { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
1212+ { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
1313+ { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" },
1414+ { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" },
1515+ { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
1616+ { name = "jot", version = "8.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "CCE11C8904B129CC9DA3A293B645884B91C96D252183F6DBCAEFA8F2587CAEFD" },
1717+ { name = "lustre", version = "5.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "40E097BABCE65FB7C460C073078611F7F5802EB07E1A9BFB5C229F71B60F8E50" },
1818+ { name = "lustre_ssg", version = "0.12.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_regexp", "gleam_stdlib", "jot", "lustre", "simplifile", "temporary", "tom"], source = "git", repo = "https://github.com/fruno-bulax/lustre_ssg", commit = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" },
1919+ { name = "shellout", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "1BDC03438FEB97A6AF3E396F4ABEB32BECF20DF2452EC9A8C0ACEB7BDDF70B14" },
2020+ { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" },
2121+ { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
2222+ { name = "temporary", version = "1.0.0", build_tools = ["gleam"], requirements = ["envoy", "exception", "filepath", "gleam_crypto", "gleam_stdlib", "simplifile"], otp_app = "temporary", source = "hex", outer_checksum = "51C0FEF4D72CE7CA507BD188B21C1F00695B3D5B09D7DFE38240BFD3A8E1E9B3" },
2323+ { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" },
2424+]
2525+2626+[requirements]
2727+gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
2828+gleam_time = { version = ">= 1.6.0 and < 2.0.0" }
2929+jot = { version = ">= 8.0.0 and < 9.0.0" }
3030+lustre = { version = ">= 5.4.0 and < 6.0.0" }
3131+lustre_ssg = { git = "https://github.com/fruno-bulax/lustre_ssg", ref = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" }
3232+shellout = { version = ">= 1.7.0 and < 2.0.0" }
3333+simplifile = { version = ">= 2.3.2 and < 3.0.0" }
3434+tom = { version = ">= 2.0.0 and < 3.0.0" }
+84
pages/dots.djot
···11+# what i use
22+33+We all use software. Good software, bad software, great software, shit software.
44+Here's a wall of text about the software I use.
55+Oh, also some hardware too, I guess.
66+77+## operating system
88+99+Arch (btw). I tried to like NixOS. I really did. I tried twice! I gave up twice…
1010+1111+The idea of having a single configuration for your whole system is incredibly
1212+appealing, but I never got over Nix-the-language and the sheer complexity
1313+of it all. So back to Arch it is.
1414+1515+## desktop environment
1616+1717+I am currently quite enjoying [Niri](https://github.com/YaLTeR/niri) in combination
1818+with the [Noctalia](https://noctalia.dev/) shell. Tiling window managers are a way
1919+of life, and Noctalia comes with basic shell things I need. If I never have to
2020+configure a Waybar again, it'll be too soon.
2121+2222+I tend to go _all in_ on color schemes. After yeeeaars on Gruvbox, I switched to
2323+[Kanagawa](https://github.com/rebelot/kanagawa.nvim) a while ago.
2424+My shell, terminal, editor, GTK/QT theme, _even this website_ are styled accordingly.
2525+2626+## terminal stuff
2727+2828+Yeah, I thought it was hype too, but [Ghostty](https://ghostty.org/)
2929+is actually really nice. No complaints so far!
3030+3131+After a long time of using zsh at work, I decided to pick up
3232+[Elvish](https://elv.sh/) for a few days. While it's definitely worth
3333+checking out, I ultimately went back to [fish](https://fishshell.com/).
3434+Dunno why I left it, it's really, really nice! Bonus points for being
3535+one of the blessed shells that usually gets shell completions out of the box.
3636+3737+Like everyone else I use [Starship](https://starship.rs/) for my prompt.
3838+I will say that configuring that did _not_ spark joy. Until native
3939+support comes along, I'm also using the
4040+[starship-jj](https://gitlab.com/lanastara_foss/starship-jj) module.
4141+4242+## browser
4343+4444+I finally did it. I stopped using Firefox. It was a long time coming,
4545+but evolving into a "Modern AI Browser" finally tipped me over the edge.
4646+I'm currently using [Helium](https://helium.computer/), a chromium-based browser.
4747+I definitely do miss some Firefox-isms, but overall I'm pretty happy.
4848+4949+By the way, if you're using Firefox, you're not seeing the multi-page
5050+view transitions on this website!
5151+5252+## coding
5353+5454+I'll probably leave this a stub because otherwise it will escalate.
5555+5656+After years I finally wrote a decent config from actual scratch.
5757+No LazyVim. Not even kickstart. Just nvim nightly and the new native
5858+package manager. I mostly use plugins from the `mini` family, as well as
5959+`blink` for completions and `flash` for navigation.
6060+6161+Of course I also use LSP. LSP is great! I just wish it was good…
6262+6363+## multimedia
6464+6565+When I abandoned the last remnants of Windows I jumped from VLC to mpv
6666+because it didn't quite work right on Wayland somehow.
6767+The default UI is butt-ugly (and this is coming from a VLC user!),
6868+but my nvim config skills have prepared me for also configuring mpv.
6969+Of course I've changed the colors over to Kanagawa.
7070+7171+I also have a local music library (sourced from Bandcamp), but I haven't
7272+settled on a music player yet.
7373+7474+## keeeeeebs
7575+7676+i love dem ortho split keebs. looove em!
7777+I use a [3w6](https://github.com/weteor/3W6) at work and a
7878+[corne](https://github.com/foostan/crkbd) at home. The extra column is really
7979+a must for the gaming-layer.
8080+Both layouts are heavily based on [miryoku](https://github.com/manna-harbour/miryoku),
8181+but I'm still stuck on QWERTY.
8282+8383+I'd really like to try something fancier with tenting or a curved surface or something,
8484+but for now I'm happy with what I've got.
+13
pages/index.djot
···11+# hello, world
22+33+hi, I'm fruno, a professional software developer and amateur
44+[crytpid](https://en.wikipedia.org/wiki/List_of_cryptids) based in Vienna.
55+I also like opossums.
66+77+At my day job I use Kotlin, but my free time is mostly spent with
88+[Gleam](https://gleam.run/) or Rust (usually on the Gleam compiler).
99+You can find me over on
1010+[GitHub](https://github.com/fruno-bulax) or [tangled](https://tangled.org/fruno.win),
1111+which also hosts the [source code](https://tangled.org/fruno.win/webbed-site/)
1212+of this very site! I'm not really on any socials but you can message me on
1313+[Bluesky](https://bsky.app/profile/fruno.win).
+7
posts/2026-01-03-initial-commit.djot
···11+---
22+title = "Initial Commit"
33+---
44+55+# initial commit
66+77+This post is gonna detail how I made this site, but for now it's just a placeholder.
+30
serve💅.py
···11+#!/usr/bin/env python3
22+33+import http.server
44+import socketserver
55+import sys
66+import os.path
77+88+PORT = 8080
99+# Be careful in public networks!
1010+# I just wanna check the site on my phone
1111+HOST = "0.0.0.0"
1212+ROOT = "./dist"
1313+1414+class CleanUrlHandler(http.server.SimpleHTTPRequestHandler):
1515+ def __init__(self, *args, **kwargs):
1616+ super().__init__(*args, directory=ROOT, **kwargs)
1717+1818+ def do_GET(self):
1919+ # This is a very shitty check, but it's good enough
2020+ if not '.' in self.path and not os.path.isdir(self.translate_path(self.path)):
2121+ self.path += '.html'
2222+2323+ super().do_GET()
2424+2525+with socketserver.TCPServer((HOST, PORT), CleanUrlHandler) as http:
2626+ print(f"Serving 💅 at {HOST}:{PORT}")
2727+ try:
2828+ http.serve_forever()
2929+ except KeyboardInterrupt:
3030+ sys.exit(0)
+50
src/blog.gleam
···11+import component
22+import gleam/list
33+import gleam/string
44+import lustre/attribute
55+import lustre/element.{type Element}
66+import lustre/element/html
77+import lustre/ssg/djot
88+import simplifile
99+import tom
1010+1111+pub type Post {
1212+ Post(slug: String, title: String, content: String)
1313+}
1414+1515+const posts_dir = "./posts"
1616+1717+pub fn posts() -> List(Post) {
1818+ let assert Ok(files) = simplifile.read_directory(posts_dir)
1919+ as "Failed to read posts directory"
2020+2121+ files
2222+ |> list.sort(string.compare)
2323+ |> list.map(read_post)
2424+}
2525+2626+fn read_post(filename: String) {
2727+ let assert [slug, "djot"] = string.split(filename, ".")
2828+ as "Unexpected post file type"
2929+3030+ let assert Ok(content) = simplifile.read(posts_dir <> "/" <> filename)
3131+ as "Failed to read file"
3232+ let assert Ok(meta) = djot.metadata(content) as "Failed to read post metadata"
3333+ let assert Ok(title) = tom.get_string(meta, ["title"]) as "Missing post title"
3434+3535+ Post(slug, title, content)
3636+}
3737+3838+pub fn list_posts(posts: List(Post)) -> List(Element(msg)) {
3939+ [
4040+ component.header(1, "blog", [], [html.text("blog")]),
4141+ html.ul([], list.map(posts, post_list_item)),
4242+ ]
4343+}
4444+4545+fn post_list_item(post: Post) -> Element(msg) {
4646+ html.a([attribute.href("/blog/" <> post.slug)], [
4747+ html.text(post.slug),
4848+ html.text(".djot"),
4949+ ])
5050+}