···11+# webbed site
22+33+```sh
44+gleam run
55+gleam run -m serve
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+import gleam/bytes_tree
22+import gleam/erlang/process
33+import gleam/http/request.{type Request}
44+import gleam/http/response.{type Response}
55+import gleam/list
66+import gleam/string
77+import mist.{type Connection, type ResponseData}
88+import simplifile
99+1010+pub fn main() {
1111+ let assert Ok(_) =
1212+ mist.new(handler)
1313+ // Listen on all interfaces so I can check the site on my phone
1414+ // Careful if you're in a public wifi!
1515+ |> mist.bind("0.0.0.0")
1616+ |> mist.port(8080)
1717+ |> mist.start
1818+1919+ process.sleep_forever()
2020+}
2121+2222+fn handler(req: Request(Connection)) -> Response(ResponseData) {
2323+ let path = case req.path {
2424+ // I tinker with the css a lot so I like the direct updates
2525+ "/style.css" -> "/../assets/style.css"
2626+ "/" -> "/index.html"
2727+ p ->
2828+ case string.ends_with(p, "/") {
2929+ True -> p <> "index.html"
3030+ False ->
3131+ case string.contains(p, ".") {
3232+ True -> p
3333+ False -> p <> ".html"
3434+ }
3535+ }
3636+ }
3737+3838+ let file_path = "./dist" <> path
3939+4040+ case simplifile.read_bits(file_path) {
4141+ Ok(bits) ->
4242+ response.new(200)
4343+ |> response.set_header("content-type", get_content_type(path))
4444+ |> response.set_body(mist.Bytes(bytes_tree.from_bit_array(bits)))
4545+ Error(simplifile.Enoent) ->
4646+ response.new(404)
4747+ |> response.set_body(mist.Bytes(bytes_tree.from_string("Not Found")))
4848+ Error(_) ->
4949+ response.new(500)
5050+ |> response.set_body(
5151+ mist.Bytes(bytes_tree.from_string("Internal server error")),
5252+ )
5353+ }
5454+}
5555+5656+fn get_content_type(path: String) -> String {
5757+ case string.split(path, ".") |> list.last {
5858+ Ok("html") -> "text/html; charset=utf-8"
5959+ Ok("css") -> "text/css; charset=utf-8"
6060+ Ok("js") -> "application/javascript"
6161+ Ok("png") -> "image/png"
6262+ Ok("jpg") | Ok("jpeg") -> "image/jpeg"
6363+ Ok("svg") -> "image/svg+xml"
6464+ _ -> "application/octet-stream"
6565+ }
6666+}
+28
gleam.toml
···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 = { git = "https://github.com/fruno-bulax/lustre_ssg", ref = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" }
1818+lustre = ">= 5.4.0 and < 6.0.0"
1919+gleam_time = ">= 1.6.0 and < 2.0.0"
2020+simplifile = ">= 2.3.2 and < 3.0.0"
2121+tom = ">= 2.0.0 and < 3.0.0"
2222+shellout = ">= 1.7.0 and < 2.0.0"
2323+2424+[dev-dependencies]
2525+gleeunit = ">= 1.0.0 and < 2.0.0"
2626+mist = ">= 5.0.3 and < 6.0.0"
2727+gleam_http = ">= 4.3.0 and < 5.0.0"
2828+gleam_erlang = ">= 1.3.0 and < 2.0.0"
+46
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_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
1111+ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
1212+ { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
1313+ { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
1414+ { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" },
1515+ { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" },
1616+ { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
1717+ { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
1818+ { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" },
1919+ { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" },
2020+ { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
2121+ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
2222+ { name = "jot", version = "8.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "CCE11C8904B129CC9DA3A293B645884B91C96D252183F6DBCAEFA8F2587CAEFD" },
2323+ { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
2424+ { 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" },
2525+ { 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" },
2626+ { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" },
2727+ { name = "shellout", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "1BDC03438FEB97A6AF3E396F4ABEB32BECF20DF2452EC9A8C0ACEB7BDDF70B14" },
2828+ { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" },
2929+ { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
3030+ { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
3131+ { 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" },
3232+ { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" },
3333+]
3434+3535+[requirements]
3636+gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
3737+gleam_http = { version = ">= 4.3.0 and < 5.0.0" }
3838+gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
3939+gleam_time = { version = ">= 1.6.0 and < 2.0.0" }
4040+gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
4141+lustre = { version = ">= 5.4.0 and < 6.0.0" }
4242+lustre_ssg = { git = "https://github.com/fruno-bulax/lustre_ssg", ref = "6c132bd34ab75a1144d31c0f896cab0e3cbf80fc" }
4343+mist = { version = ">= 5.0.3 and < 6.0.0" }
4444+shellout = { version = ">= 1.7.0 and < 2.0.0" }
4545+simplifile = { version = ">= 2.3.2 and < 3.0.0" }
4646+tom = { version = ">= 2.0.0 and < 3.0.0" }
···11+import component
22+import lustre/element.{type Element}
33+import lustre/element/html
44+import page
55+66+pub fn view() -> Element(Nil) {
77+ page.render(page.Index, "index", [
88+ component.header("wip", html.h1),
99+ html.p([], [
1010+ html.text(
1111+ "This site is very work-in-progress but it kind of looks like a terminal and that's pretty neat.",
1212+ ),
1313+ ]),
1414+ component.header("Nested headers work too!", html.h2),
1515+ html.p([], [
1616+ html.text(
1717+ "This is a purely static site without javascript but with mutli-page view transitions instead. Firefox can't do them yet. boooo!",
1818+ ),
1919+ ]),
2020+ ])
2121+}
+59
src/page/posts.gleam
···11+import gleam/list
22+import gleam/string
33+import gleam/time/calendar.{type Date}
44+import lustre/attribute
55+import lustre/element.{type Element}
66+import lustre/element/html
77+import lustre/ssg/djot
88+import page
99+import simplifile
1010+import tom
1111+1212+pub type Post {
1313+ Post(title: String, date: Date, slug: String, content: List(Element(Nil)))
1414+}
1515+1616+const posts_dir = "./posts"
1717+1818+pub fn all() -> List(Post) {
1919+ let assert Ok(files) = simplifile.read_directory(posts_dir)
2020+ as "Failed to read posts directory"
2121+2222+ files
2323+ |> list.map(read_post)
2424+ |> list.sort(fn(a, b) { calendar.naive_date_compare(a.date, b.date) })
2525+}
2626+2727+pub fn view_all(posts: List(Post)) -> Element(Nil) {
2828+ page.render(page.Blog, "blog", [
2929+ html.article([], [
3030+ html.header([], [html.h1([], [html.text("blog")])]),
3131+ html.ul([], list.map(posts, post_list_item)),
3232+ ]),
3333+ ])
3434+}
3535+3636+fn post_list_item(post: Post) -> Element(Nil) {
3737+ html.a([attribute.href("/blog/" <> post.slug)], [html.text(post.title)])
3838+}
3939+4040+pub fn view(post: Post) -> Element(Nil) {
4141+ page.render(page.Blog, post.title, post.content)
4242+}
4343+4444+fn read_post(filename: String) -> Post {
4545+ let assert [slug, "djot"] = string.split(filename, ".")
4646+ as "Unexpected post file type"
4747+ let assert Ok(content) = simplifile.read(posts_dir <> "/" <> filename)
4848+ as "Failed to read file"
4949+5050+ let assert Ok(meta) = djot.metadata(content) as "Failed to read post metadata"
5151+ let assert Ok(title) = tom.get_string(meta, ["title"]) as "Missing post title"
5252+ let assert Ok(date) = tom.get_date(meta, ["date"])
5353+ as "Missing or malformed post date"
5454+5555+ // TODO scaffolding like nav and such
5656+ let content = djot.render(content, djot.default_renderer())
5757+5858+ Post(title:, date:, slug:, content:)
5959+}