1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
|
// Site template for kumardamani.net, adapted from the example Typst blog.
// Structured for Typst's bundle export: pages are emitted as #document
// elements from the main.typ entrypoint and cross-page links use labels.
#let _site-info = state("site-info", none)
#let _page-name = state("page-name", none)
#let _page-dir = state("page-dir", "")
// Image paths collected from pages, emitted as bundle assets in main.typ.
#let _asset-paths = state("asset-paths", ())
#let srcurl = "https://gitlab.com/kdam0/kumardamani.net"
// Deployment info, overwritten by CI per build:
// printf '{ "date": "%s", "link": "%s" }\n' \
// "$(date '+%B %d, %Y' | sed 's/ 0/ /')" "$CI_JOB_URL" > ci.json
// date is pre-formatted for display, e.g. "July 24, 2026".
#let ci = json("/ci.json")
// Page ids ("post/self-host") map to labels on the corresponding #document
// elements ("post-self-host"); typst resolves relative links between
// documents automatically.
#let label-for(id) = label(id.replace("/", "-"))
#let extlink(
dest,
attrs: (:),
body
) = html.elem(
"a",
attrs: (
class: "extlink",
rel: "external",
target: "_blank",
href: dest,
) + attrs,
body
)
#let navbar(pages, current) = {
html.elem("nav", html.elem("ul", {
for (pagename, prefs) in pages {
let (title,) = prefs
let hidden = prefs.at("hidden", default: false)
if hidden and pagename != current {
continue
}
let attrs = (:)
let class = ()
if pagename == current {
class += ("current",)
}
if hidden {
class += ("hidden",)
}
if class != () {
attrs += ("class": class.join(" "))
}
if pagename == current {
// the current page's nav item is not a link
html.elem("li", attrs: attrs, title)
} else if "url" in prefs {
html.elem("li", attrs: attrs, extlink(prefs.url, title))
} else {
html.elem("li", attrs: attrs, link(label-for(pagename), title))
}
}
}))
}
// Image helpers. These emit plain <img> tags with literal src paths so that
// images stay as separate files instead of being embedded into the HTML, and
// register local sources as bundle assets (emitted in main.typ).
#let img(src, alt: "", width: none) = {
if not src.starts-with("http") {
context {
let dir = _page-dir.get()
_asset-paths.update(s => s + (dir + src,))
}
}
let attrs = (src: src, alt: alt)
if width != none {
attrs.insert("style", "width: " + width)
}
html.elem("img", attrs: attrs)
}
#let fig(src, alt: "", caption: none, width: none) = html.elem("figure", {
img(src, alt: alt, width: width)
if caption != none {
html.elem("figcaption", caption)
}
})
#let _preferred-title(id) = {
let page = _site-info.get().at("pages").at(id)
if "long-title" in page {
return page.at("long-title")
} else {
return page.at("title")
}
}
// The preferred (long, if available) title of the current page.
#let preferred-title = context {
_preferred-title(_page-name.get())
}
// The publication date of the current page, if any.
#let page-date = context {
let info = _site-info.get().at("pages").at(_page-name.get())
if "date" in info {
html.elem("p", attrs: (class: "date"),
html.elem("time", attrs: (datetime: info.date), info.date))
}
}
// Link to another page on the site; relative paths are resolved by typst.
#let pagelink(id, body) = link(label-for(id), body)
// Link to another page, using its preferred title as the link text.
#let pageref(id) = context {
link(label-for(id), _preferred-title(id))
}
#let page(
pagename,
site-info,
banner: none,
stylesheets: (),
body
) = {
let site = site-info.at("title", default: "Untitled")
let pages = site-info.at("pages", default: (:))
if banner == none {
banner = html.elem("h1", attrs: (class: "banner"), site)
}
// Prefix for links back to the site root from nested pages (posts).
let parts = pagename.split("/")
let depth = parts.len() - 1
let base = "../" * depth
html.html(
lang: "en",
{
html.head({
html.meta(charset: "utf-8")
html.meta(name: "viewport", content: "width=device-width, initial-scale=1")
html.title(pages.at(pagename).title + " – " + site)
html.elem("link", attrs: ("rel": "vcs-git", "title": "site source", "href": srcurl))
html.link(rel: "icon", href: base + "favicon.svg", type: "image/svg+xml")
html.link(rel: "icon", href: base + "favicon.ico")
html.elem("link", attrs: (rel: "preload", href: base + "extlink.svg", "as": "image"))
html.link(rel: "stylesheet", href: base + "main.css")
for ss in stylesheets {
html.link(rel: "stylesheet", href: base + ss)
}
})
html.body({
_site-info.update(site-info)
_page-name.update(pagename)
_page-dir.update(if depth > 0 { parts.slice(0, -1).join("/") + "/" } else { "" })
show html.elem: it => {
if "title" in it.attrs {
let attrs = it.attrs
let has-class = "class" in it.attrs
if not has-class or regex("\btooltip\b") not in it.attrs.class {
if has-class {
attrs.class += " tooltip"
} else {
attrs.class = "tooltip"
}
it = html.elem(it.tag, attrs: attrs, it.body)
}
}
it
}
show outline.entry: it => {
if it.prefix() != none {
html.span(class: "prefix", it.prefix())
}
link(it.element.location(), it.body())
}
html.header({
banner
navbar(pages, pagename)
})
html.main(body)
html.hr()
html.footer({
html.span(class: "updated")[Updated: #ci.date.]
html.span(class: "logs")[#extlink(ci.link)[Logs] for nerds!]
})
})
}
)
}
// Scaffold for post pages: numbered headings (the heading counter is global
// to the bundle, so it is reset per document) with title and date on top.
#let post-page(pagename, site-info, body) = page(pagename, site-info, {
set heading(numbering: "1.")
counter(heading).update(0)
title(preferred-title)
page-date
body
})
|