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