// 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://git.kumardamani.net/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 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(srcurl)[site source] | #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 })