Reference
The functions and types Twyla adds to Typst’s standard library.
document
document( output: strauto, title: contentnone, date: datetimenone, description: contentnone, kind: strauto, extra: any, draft: bool, body: content, ) -> content
Defines a page of the website.
Conventionally, document metadata is provided near the top of each page. with a set rule. Every field is then available on this page via #context, and the whole site’s metadata is available through
document.<field>documents().
#set document(
title: "Rewriting My Blog",
date: datetime(year: 2026, month: 4, day: 12),
description: "Why I moved off Markdown.",
kind: "post",
)
It is also possible to create new pages inline, by constructing documents:
#context link(
document(
title: "Page within a page",
output: "subpage/index.html",
)[Hello from a page within a page!].url(),
)[This is a link to a page within a page]
Where to write the document, as a bundle output path — e.g. "foo/index.html" for a page called “foo”, or "foo.html". Can be used to create a page literally called “main”.
A relative path (no leading /) resolves against a base: for a full-file page, the source’s folder below content/ (content/blog/post.typ → blog/, so output: "extra.html" → blog/extra.html); for an inline document(..), the enclosing page’s output directory. A leading / is bundle-root-absolute (output: "/feed.xml"), ignoring that base. When auto, a full-file page derives its output from the source filename; an inline document must set it explicitly.
What kind of page this is, e.g. "post" or "page" — used to group pages in listings and feeds, and to pick the {kind}-template a convert draft shows. If auto, defaults from the source filename:
| Kind | Default for |
|---|---|
"root" |
content/main.typ — the site index |
"dir" |
content/<dir>/main.typ — a section index |
"page" |
any other file |
document.extra and as the extra field of this page’s [documents] entry. Defaults to an empty dictionary, so templates can document.extra.at(.., default: ..) without first checking for none.document is called as a constructor — #document(output: "x")[stuff] — this carries stuff, which twyla hoists into its own bundle output (see [crate::compile]). #set document(..) never touches this (required positional fields aren’t settable), so the set-rule-only metadata-carrier use is unaffected..url()
document.url() -> str
The page’s public URL. Two call forms:
document.url()(no self) → the current page’s URL, built from the output twyla derived for it.document(output: "x")[..].url()(method, on an instance) → that document’s URL, and discovers it for emission — so an inline document consumed only through.url()(never shown) is still routed. Discovery dedups byoutput, so calling.url()repeatedly, or.url()on a document that is also shown, emits it exactly once.
Contextual — call it inside #context. context precedes the optional this self-positional: the #[func] macro classifies special params by name and forwards them ahead of ordinary positionals (the instance is prepended as that positional on a method call).
documents
documents() -> array
The list of every page in the site.
Returns an array with one dictionary per page, each carrying that page’s url plus the metadata it set via #set document(..): url, title, date, description, kind, draft, and extra. Access fields with ordinary dot syntax, e.g. doc.title.
This is contextual — call it inside a #context block:
#context for doc in documents() {
if doc.kind == "post" and not doc.draft [
== #link(doc.url, doc.title)
#doc.date.display()
#doc.description
]
}
asset
.url()
asset.*.url() -> str
The resolved, fingerprinted URL of this asset (e.g. /assets/logo-<hash>.svg). Contextual — call it inside #context.
context must precede the this self-positional: the #[func] macro classifies special params by name and forwards them ahead of ordinary positionals, and the method call prepends the element as that positional.
.read()
asset.*.read( encoding: "utf8"none, ) -> strbytes
The file’s contents — for inlining instead of linking. Mirrors the native read function: UTF-8 str by default, raw bytes with encoding: none. Contextual.
{none}, returns raw bytes; otherwise the bytes are decoded as UTF-8 into a string.asset.file
asset.file( path: pathstr, ) -> content
Reference a project file as an asset, copied verbatim and fingerprinted.
#context html.elem("img", attrs: (src: asset.file("logo.svg").url()))
asset.sass
asset.sass( path: pathstr, minify: bool, ) -> content
Compile a Sass/SCSS file to a fingerprinted CSS asset.
minify is a settable field, so #set asset.sass(minify: false) switches a whole page (or site) to expanded output.
#context html.elem("link", attrs: (
rel: "stylesheet",
href: asset.sass("main.scss").url(),
))
.sass/.scss file, relative to the calling file.asset.image
asset.image( path: pathstr, width: intnone, height: intnone, fit: "contain""cover""stretch", filter: "nearest""triangle""catmull-rom""gaussian""lanczos", format: "png""jpeg""gif""webp""avif"none, quality: int, ) -> content
Decode, resize, and re-encode a raster image asset.
#context html.elem("img", attrs: (
src: asset.image("photo.jpg", width: 600, format: "webp").url(),
))
With no width/height the image is only transcoded (to format); with no format it keeps its source format and is only resized. All parameters are settable, so #set asset.image(format: "webp") applies to a whole scope.
width/height set the other is computed to preserve the aspect ratio; with neither, the image is not resized.width](Self::width).width×height when both are given: "contain" scales to fit inside the box (aspect preserved), "cover" scales and crops to fill it (aspect preserved), "stretch" forces the exact dimensions (aspect distorted). cover/stretch require both dimensions."lanczos" (the default) gives the best downscaling quality; cheaper options trade quality for speed.{none} (the default) keeps the source format; otherwise the image is transcoded to the named format.asset.typst
asset.typst( source: pathstrcontent, format: "svg""png""pdf""html", ppi: int, ) -> content
Compile a typst document (content or a project .typ file) to a fingerprinted asset.
format (default svg) and ppi are settable, so #set asset.typst(format: "pdf") configures a whole scope.
#context html.elem("a", attrs: (
href: asset.typst("/resume/cv.typ", format: "pdf").url(),
))[Download my CV]
content value, or a path string to a project .typ file (resolved relative to the calling file)."svg" (the default), "png", "pdf", or "html". svg/png/pdf lay the document out as pages; html compiles it the same way the site’s pages are compiled.png format. Ignored by the other formats (and excluded from the cache key for them, so it never fragments their output).raw-html
raw-html( html: strbytes, ) -> content
Splice a string of raw HTML into the output verbatim.
Typst has no first-class raw-HTML node, so for now this wraps the markup in <script type="…">…</script> — a raw-text element typst won’t escape — and twyla’s post-render pass ([crate::render::resolve_raw_html_placeholders]) strips the wrapper back out. Exposing it as a builtin rather than a typst #let keeps call sites stable if we later swap the implementation (e.g. parse the HTML with html5ever and emit real typst nodes).
asset.file("icon.svg").read(encoding: none)) decoded as UTF-8.plain-text
plain-text( content: content, ) -> str
The plain text of some content — the same flattening typst uses to derive the document <title> from document(title: …). Useful for slugs, alt text, and other places that need a string from rich content.