💻 My personal website blog.kacaii.dev/
blog gleam lustre

:zap: build static files before deploying

+106 -82
+2
.gitignore
··· 13 # Added automatically by Lustre Dev Tools 14 /.lustre 15 /dist
··· 13 # Added automatically by Lustre Dev Tools 14 /.lustre 15 /dist 16 + 17 + priv/static/posts/
+11 -1
.tangled/workflows/fly.yml
··· 9 - flyctl 10 - tailwindcss_4 11 12 steps: 13 - - name: Generate CSS 14 command: | 15 tailwindcss -i input.css -o priv/static/output.css 16 17 - name: Deploy app 18 command: |
··· 9 - flyctl 10 - tailwindcss_4 11 12 + github:NixOS/nixpkgs/nixpkgs-unstable: 13 + - gleam 14 + - beamMinimal28Packages.erlang 15 + - beamMinimal28Packages.rebar3 16 + 17 steps: 18 + - name: tailwind 19 command: | 20 tailwindcss -i input.css -o priv/static/output.css 21 + 22 + - name: build 23 + command: | 24 + export PATH="$HOME/.nix-profile/bin:$PATH" 25 + gleam dev -- build 26 27 - name: Deploy app 28 command: |
+1 -1
Dockerfile
··· 1 ARG ERLANG_VERSION=28.0.2.0 2 - ARG GLEAM_VERSION=v1.13.0 3 4 # Gleam stage 5 FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-scratch AS gleam
··· 1 ARG ERLANG_VERSION=28.0.2.0 2 + ARG GLEAM_VERSION=v1.14.0 3 4 # Gleam stage 5 FROM ghcr.io/gleam-lang/gleam:${GLEAM_VERSION}-scratch AS gleam
+4
justfile
··· 10 update-post-titles: 11 gleam dev -- update_titles 12 13 #  Deploy app 14 [confirm(" Deploy app?")] 15 deploy:
··· 10 update-post-titles: 11 gleam dev -- update_titles 12 13 + #  Build static HTML files 14 + build: 15 + gleam run -m build 16 + 17 #  Deploy app 18 [confirm(" Deploy app?")] 19 deploy:
-72
src/blog/page/content.gleam
··· 1 - import blog/page/footer 2 - import blog/page/navbar 3 - import blog/post 4 - import blog/root 5 - import contour 6 - import gleam/list 7 - import gleam/option 8 - import jot 9 - import lustre/attribute.{class} as attr 10 - import lustre/element 11 - import lustre/element/html 12 - import web 13 - import wisp 14 - 15 - pub fn handle_request(ctx: web.Context, post_uri: String) -> wisp.Response { 16 - case list.find(ctx.posts, fn(post) { post.meta.slug == post_uri }) { 17 - Error(_) -> wisp.not_found() 18 - Ok(post) -> { 19 - let title = post.meta.title 20 - 21 - let post_header = 22 - html.header([class("hidden md:block")], [ 23 - html.h1([class("mb-1 text-4xl font-bold")], [html.text(title)]), 24 - html.p([class("my-2")], [html.text(post.meta.description)]), 25 - html.hr([class("border text-ctp-surface0")]), 26 - ]) 27 - 28 - let back_button_style = 29 - class( 30 - "mx-auto w-full text-right underline underline-offset-2 text-ctp-lavender", 31 - ) 32 - 33 - let back_button = 34 - html.a([attr.href("/posts"), back_button_style], [ 35 - html.text("Back"), 36 - ]) 37 - 38 - root.view(title:, content: [ 39 - navbar.view([]), 40 - post_header, 41 - view(post), 42 - back_button, 43 - footer.view(), 44 - ]) 45 - } 46 - } 47 - } 48 - 49 - pub fn view(post: post.Post) -> element.Element(a) { 50 - let content = 51 - post.body.content 52 - |> list.map(highlight_codeblock) 53 - 54 - let post_body = 55 - jot.Document(..post.body, content:) 56 - |> jot.document_to_html 57 - 58 - let style = class("grid grid-cols-1 gap-4 w-full text-pretty post-content") 59 - 60 - element.fragment([element.unsafe_raw_html("", "article", [style], post_body)]) 61 - } 62 - 63 - fn highlight_codeblock(container: jot.Container) -> jot.Container { 64 - case container { 65 - jot.Codeblock(_, language: option.Some("gleam"), content:) -> { 66 - let code = "<pre><code>" <> contour.to_html(content) <> "</code></pre>" 67 - jot.RawBlock(code) 68 - } 69 - 70 - other -> other 71 - } 72 - }
···
+1
src/blog/page/home.gleam
··· 21 ] 22 23 root.view(content:, title: "Home") 24 }
··· 21 ] 22 23 root.view(content:, title: "Home") 24 + |> wisp.html_response(200) 25 }
+1 -1
src/blog/page/navbar.gleam
··· 8 html.nav([style, ..attributes], [ 9 route(icon: "nf-fa-home", href: "/", label: "Home"), 10 route(icon: "nf-md-file_document_edit", href: "/posts", label: "Articles"), 11 - route(icon: "nf-md-file_star", href: "/uses", label: "Uses"), 12 ]) 13 } 14
··· 8 html.nav([style, ..attributes], [ 9 route(icon: "nf-fa-home", href: "/", label: "Home"), 10 route(icon: "nf-md-file_document_edit", href: "/posts", label: "Articles"), 11 + route(icon: "nf-md-file_star", href: "/posts/uses.html", label: "Uses"), 12 ]) 13 } 14
+2 -1
src/blog/page/posts.gleam
··· 24 25 // RENDER 26 root.view(content:, title: "Posts") 27 } 28 29 pub fn post_preview(post: post.Post) -> element.Element(_) { ··· 43 class("flex-col p-4 mx-auto w-full rounded-lg shadow-sm bg-ctp-mantle") 44 45 html.li([li_styles], [ 46 - html.a([attr.href("/posts/" <> post.meta.slug)], [ 47 html.h2([class("text-2xl font-bold text-pretty")], [ 48 html.text(meta.title), 49 ]),
··· 24 25 // RENDER 26 root.view(content:, title: "Posts") 27 + |> wisp.html_response(200) 28 } 29 30 pub fn post_preview(post: post.Post) -> element.Element(_) { ··· 44 class("flex-col p-4 mx-auto w-full rounded-lg shadow-sm bg-ctp-mantle") 45 46 html.li([li_styles], [ 47 + html.a([attr.href("/posts/" <> post.meta.slug <> ".html")], [ 48 html.h2([class("text-2xl font-bold text-pretty")], [ 49 html.text(meta.title), 50 ]),
+1 -1
src/blog/page/recent_posts.gleam
··· 37 string.join([day, month, year], with: " ") 38 } 39 40 - let href = attr.href("/posts/" <> post.meta.slug) 41 let style = class("flex flex-col p-4 mx-auto w-full rounded-lg bg-ctp-mantle") 42 43 html.li([style], [
··· 37 string.join([day, month, year], with: " ") 38 } 39 40 + let href = attr.href("/posts/" <> post.meta.slug <> ".html") 41 let style = class("flex flex-col p-4 mx-auto w-full rounded-lg bg-ctp-mantle") 42 43 html.li([style], [
-2
src/blog/root.gleam
··· 1 import lustre/attribute.{class} as attr 2 import lustre/element 3 import lustre/element/html 4 - import wisp 5 6 pub fn view(title title: String, content content: List(element.Element(_))) { 7 let viewport_meta = ··· 32 33 html.html([attr.lang("en")], [head, html.body([style], content)]) 34 |> element.to_document_string 35 - |> wisp.html_response(200) 36 }
··· 1 import lustre/attribute.{class} as attr 2 import lustre/element 3 import lustre/element/html 4 5 pub fn view(title title: String, content content: List(element.Element(_))) { 6 let viewport_meta = ··· 31 32 html.html([attr.lang("en")], [head, html.body([style], content)]) 33 |> element.to_document_string 34 }
+83
src/build.gleam
···
··· 1 + import blog 2 + import blog/page/footer 3 + import blog/page/navbar 4 + import blog/post 5 + import blog/root 6 + import contour 7 + import filepath as path 8 + import gleam/list 9 + import gleam/option 10 + import jot 11 + import lustre/attribute.{class} as attr 12 + import lustre/element 13 + import lustre/element/html 14 + import simplifile 15 + 16 + pub fn main() { 17 + let assert Ok(priv) = blog.priv_directory() 18 + let posts_path = path.join(priv, "posts") 19 + let out_path = path.join(priv, "static") |> path.join("posts") 20 + 21 + let assert Ok(entries) = simplifile.read_directory(posts_path) 22 + as "Read posts directory" 23 + 24 + use file_name <- list.each(entries) 25 + let file_path = path.join(posts_path, file_name) 26 + let assert Ok(post) = post.from_string(path: file_path) as "Parse post" 27 + 28 + let title = post.meta.title 29 + 30 + let post_header = 31 + html.header([class("hidden md:block")], [ 32 + html.h1([class("mb-1 text-4xl font-bold")], [html.text(title)]), 33 + html.p([class("my-2")], [html.text(post.meta.description)]), 34 + html.hr([class("border text-ctp-surface0")]), 35 + ]) 36 + 37 + let back_button_style = 38 + class( 39 + "mx-auto w-full text-right underline underline-offset-2 text-ctp-lavender", 40 + ) 41 + 42 + let back_button = 43 + html.a([attr.href("/posts"), back_button_style], [ 44 + html.text("Back"), 45 + ]) 46 + 47 + let content_html = 48 + root.view(title:, content: [ 49 + navbar.view([]), 50 + post_header, 51 + view(post), 52 + back_button, 53 + footer.view(), 54 + ]) 55 + 56 + let out_file = path.join(out_path, post.meta.slug <> ".html") 57 + let assert Ok(_) = simplifile.write(out_file, content_html) 58 + } 59 + 60 + pub fn view(post: post.Post) -> element.Element(a) { 61 + let content = 62 + post.body.content 63 + |> list.map(highlight_codeblock) 64 + 65 + let post_body = 66 + jot.Document(..post.body, content:) 67 + |> jot.document_to_html 68 + 69 + let style = class("grid grid-cols-1 gap-4 w-full text-pretty post-content") 70 + 71 + element.fragment([element.unsafe_raw_html("", "article", [style], post_body)]) 72 + } 73 + 74 + fn highlight_codeblock(container: jot.Container) -> jot.Container { 75 + case container { 76 + jot.Codeblock(_, language: option.Some("gleam"), content:) -> { 77 + let code = "<pre><code>" <> contour.to_html(content) <> "</code></pre>" 78 + jot.RawBlock(code) 79 + } 80 + 81 + other -> other 82 + } 83 + }
-3
src/web/http_router.gleam
··· 1 - import blog/page/content 2 import blog/page/home 3 import blog/page/posts 4 import web ··· 9 10 case wisp.path_segments(req) { 11 [] -> home.handle_request(ctx) 12 - ["uses"] -> content.handle_request(ctx, "uses") 13 ["posts"] -> posts.handle_request(ctx) 14 - ["posts", post] -> content.handle_request(ctx, post) 15 16 ["healthcheck"] -> wisp.ok() 17 _ -> wisp.not_found()
··· 1 import blog/page/home 2 import blog/page/posts 3 import web ··· 8 9 case wisp.path_segments(req) { 10 [] -> home.handle_request(ctx) 11 ["posts"] -> posts.handle_request(ctx) 12 13 ["healthcheck"] -> wisp.ok() 14 _ -> wisp.not_found()