#ideaFor an intuition on how to think about ideas, see An idea. The #idea function takes the following arguments, which are all optional:
| Argument | Type | Description |
|---|---|---|
name | label | str | A unique name for the idea, allowing it to be referenced as @idea:<name> across the rookery. Positional, and written either as a label or as a string—#idea(<etal>) and #idea("etal") are the same call. In the absence of an explicit name, an idea with a title takes a slug of it; an idea with no usable title instead takes a slug of its own body plus a content digest, as described under auto-naming. |
title | content | The idea’s title, which appears as both the link text in hyperlinks to the idea and the header text in windows on it. |
body | content | The idea’s content, rendered inside its card and transcluded by any window on it. An idea with no body is a title-only stub, which is how a placeholder for an idea not yet written is filed. |
level | int | The heading depth of the idea’s title, so a nested idea can sit under the one containing it. Defaults to 1. |
tags | str | array | dictionary | The idea’s tags, as one name, a list of names, or a dictionary whose values carry arbitrary metadata for the tag. Set at the call site, replacing whatever a constructor bound. |
tag | str | A single tag that is merged into tags rather than replacing it, so a constructor such as idea.with(tag: "note") keeps its tag when the call site names tags of its own. |
base-tags | str | array | dictionary | As tag, but for several tags at once — for a family of ideas that is a narrowing of a broader one, so that every idea the constructor mints is still reachable under the wider tag. |
exclude-tags | str | array | Tags that drop the idea from this build entirely. An excluded idea is absent rather than hidden: nothing renders where it was written, no page is minted for it, and it appears in no outline, search index or backlink list. |
created | datetime | The date the idea was written. In its absence the containing document’s own #set document(date: ..) is used; a date is otherwise never invented. |
display | dictionary | What the idea shows of itself, as a dictionary of nine flags — see below. |
Every display key takes a boolean, and defaults to auto: the rookery-wide setting given to #rookery(display: ..). An idea states an opinion only where it differs from the rest of the rookery.
#idea(
display: (
// the idea's own minted page carries its title as an `<h1>`
title: true,
// the `created` date sits at the right-hand end of the hat
date: true,
// the tags are worn in the hat as pills, metadata tags excepted
tags: true,
// the idea is drawn as a card, with its left rule and indent
frame: false,
// the name is shown in the hat as a permalink
name: true,
// the minted page footer links back to the page written on
context: true,
// the minted page footer lists everything that links here
backlinks: true,
// unused by the card itself — seeds what a later window on
// this idea falls back to when it does not override them
label: true,
background: true,
),
)[..]Each key is also an argument in its own right, with the prefix restored — display-frame: false — and an argument on the same call wins over the dictionary’s value for that key.
Everything else spelled idea- reads one back out, and is nested here: its tags, where its page sits, its body, and the row that stands for it in a list.
#idea-tag-namesOne idea’s tag names, as a flat array.
#import "@rookery/core:0.1.0": idea, idea-tag-names
#idea("etal", title: [Et al.], tags: (note: none, draft: none, priority: 1))[
Attribution flattens a crowd into one name.
]
#context idea-tag-names("etal") // -> ("note", "draft", "priority")Every key, valued tags included — priority comes back alongside the two plain tags, because a tag carrying metadata is still a tag. This is the question of what an idea is tagged, not of what those tags hold. Key order is unspecified.
() comes back both for an untagged idea and for a name that does not exist. A missing idea is not an error here: a caller asking what something is tagged is filtering, not dereferencing. Must be called inside #context.
Where you are walking the whole rookery, read the tags field off #ideas instead: this resolves the registry once per idea, where #ideas resolves it once for the pass.
#idea-tag-valueOne tag’s value on one idea.
#import "@rookery/core:0.1.0": idea, idea-tag-value
#idea("etal", title: [Et al.], tags: (note: none, priority: 1))[
Attribution flattens a crowd into one name.
]
#context {
idea-tag-value("etal", "priority") // -> 1
idea-tag-value("etal", "note") // -> none, a plain tag
idea-tag-value("etal", "nope", default: 4) // -> 4
}Takes the same name forms as #idea-tag-names, and returns default where the idea does not exist or carries no such key.
A plain tag’s value is none, which is indistinguishable from a default of none on a key that is absent. This function answers what a tag is set to, and a plain tag is set to nothing; ask #idea-tag-names or #tag-data when the question is presence. Must be called inside #context.
#idea-hrefWhere an idea’s minted page sits, relative to the page you are calling from. Takes a bare name, a full name or a label — the same forms #window and #hyperlink take — and must be called inside #context.
#import "@rookery/core:0.1.0": idea, idea-href
#idea("etal", title: [Et al.])[
Attribution flattens a crowd into one name.
]
#context idea-href("etal")
// -> "ideas/etal.html" called from a root page
// -> "../ideas/etal.html" called from a page one directory downThe depth arithmetic is measured from the calling page, so the same idea yields a different string on a nested page than on the root one. That is the point of the function, and it is why a caller must never cache the answer across pages.
none wherever no page is minted: a plain typst compile with no Rheo, and the combined PDF target.
#idea-pathThe same page as #idea-href, from the site root rather than from here.
#import "@rookery/core:0.1.0": idea, idea-path
#idea("etal", title: [Et al.])[
Attribution flattens a crowd into one name.
]
#context idea-path("etal")
// -> "ideas/etal.html", from either page aboveReach for it where the caller has no page of its own to measure depth from — a feed configuration or a sitemap invoked once from shared code rather than from a page. It is none under the same two conditions #idea-href is.
#idea-bodyOne idea’s body, rendered, with none of a window’s chrome: no summary line, no permalink, no disclosure. For a consumer that wants to show an idea rather than describe it — a preview pane, a card in a view of the caller’s own design — where #ideas’ body field only hands over plain text.
#import "@rookery/core:0.1.0": idea, idea-body
#idea("etal", title: [Et al.])[
Attribution flattens a crowd into one name.
The convention is older than the citation styles that inherited it.
]
// anywhere after it, on any page
#idea-body("etal") // both paragraphs, no title, no permalink
#idea-body("etal", limit: 1) // the first paragraph alone| Argument | Type | Description |
|---|---|---|
name | str | label | The idea to render, in the same name forms every accessor here takes. |
unfurl | int | auto | The transclusion budget, as on #window. Pinned at 1 rather than auto, so a preview’s size stays bounded however deeply the idea it shows nests. 0 asks for no unfurling and still renders the body — there is no chrome here to fall back to a link. |
limit | int | none | How many blocks of the body to show, the same unit #window’s own limit cuts by. The whole body by default. |
It supplies its own #context, unlike the data accessors on this page, so it can be called anywhere. The body comes wrapped in the same classes a window’s does, minus the box, so every rule the stylesheet already writes for prose inside a window — link colors, code, lists, footnotes — applies with nothing for a consumer to restyle.
#idea-rowOne row of a list of ideas: a date, a title, cells and badges, as an <li>. The shape @rookery/search and @rookery/timeline both draw, kept in core so that a project reaching a row through one of those packages still gets core’s CSS with it.
#import "@rookery/core:0.1.0": idea, ideas, idea-row
#idea(
"etal",
title: [Et al.],
tags: (note: none),
created: datetime(year: 2026, month: 8, day: 20),
)[Attribution flattens a crowd into one name.]
#context html.elem("ul", ideas(tagged: "note").map(e => idea-row(
when: e.created.display("[day] [month repr:short]"),
iso: e.created.display("[year]-[month]-[day]"),
title: e.label,
href: e.href,
tags: e.tags,
badges: e.tags.map(t => (text: t, tag: t)),
)).join())The row is fed from #ideas rather than from the idea directly, which is the usual shape: a view selects its ideas, decides what their dates mean, and hands the row cells that are already content.
| Argument | Type | Description |
|---|---|---|
when | content | none | The date cell, already formatted. none draws an em dash. |
iso | str | none | A machine-readable date, which wraps when in a <time> element carrying it. |
soft | bool | Whether this date answers a different question than the list asked — a booked event standing in for a deadline. Dropped silently on a row with no date, which makes no such claim. |
when-class, when-attrs | str | array, dictionary | Extra classes and attributes on the date cell, for a consumer that bands a deadline by how close it is. Core defines none of these names and styles none of them. |
title | content | The row’s title. |
href | str | none | Where the title links. none renders it as a span instead. |
badges | array | The chip strip at the end of the row. A (text: .., tag: ..) dictionary draws the ordinary chip, wearing the same .idea-tag-<tag> class a pill and an outline row do, so a themed tag colors itself; anything else is placed verbatim and the caller owns its markup. An empty strip is omitted rather than drawn empty, since a childless span still takes a grid track. |
cells | array | Extra content cells between the title and the badges — a host institution, a path — for what a reader scans down a column for rather than reads inside the title. |
tags | array | Tag names, which become .idea-tag-<tag> classes and a data-rookery-tags attribute on the <li>. |
extra, attrs | array, dictionary | Extra classes and attributes on the <li>, merged under the classes this function computes so a caller cannot drop the row’s own. This is what lets @rookery/search’s panels hang their data-panel-* attributes off a row without re-emitting its markup. |
Cells arrive formatted, and that is the whole of the design: the row asks no questions about what a date or a badge means, so one row shape serves a log-derived queue, a filter panel and a hand-built table of submissions, none of which agree about what a date is. It carries no JavaScript — interaction stays in @rookery/search.
HTML only, and deliberately: a paged target has no grid to align and no anchor to click, so every view in this family keeps its own branch that builds a plain list(..) there. Calling it on a paged target fails with a message about the mistake rather than about the element.
#idea-row-bodyThe same row, without the <li> around it. It takes every #idea-row argument except the three that describe that wrapper — tags, extra and attrs — and is for a caller that owns the list item itself.
#import "@rookery/core:0.1.0": idea, ideas, idea-row-body
#idea(
"etal",
title: [Et al.],
tags: (note: none),
created: datetime(year: 2026, month: 8, day: 20),
)[Attribution flattens a crowd into one name.]
#context html.elem("ul", ideas(tagged: "note").map(e => html.elem(
// the `<li>` is the caller's, with its own classes and attributes
"li",
attrs: (class: "my-row", data-my-kind: "note"),
idea-row-body(
when: e.created.display("[day] [month repr:short]"),
iso: e.created.display("[year]-[month]-[day]"),
title: e.label,
href: e.href,
),
)).join())The split is forced rather than tidy: @rookery/search ships two widgets that want this shape and disagree about who owns the <li>. Its filter panel builds its own list and uses #idea-row as the item; its general panel wraps whatever it is handed in an item of its own, which with #idea-row would nest one <li> inside another.