[idea:idea-reference]reference2026-08-20

#idea

For an intuition on how to think about ideas, see An idea. The #idea function takes the following arguments, which are all optional:

ArgumentTypeDescription
namelabel | strA 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.
titlecontentThe idea’s title, which appears as both the link text in hyperlinks to the idea and the header text in windows on it.
bodycontentThe 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.
levelintThe heading depth of the idea’s title, so a nested idea can sit under the one containing it. Defaults to 1.
tagsstr | array | dictionaryThe 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.
tagstrA 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-tagsstr | array | dictionaryAs 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-tagsstr | arrayTags 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.
createddatetimeThe date the idea was written. In its absence the containing document’s own #set document(date: ..) is used; a date is otherwise never invented.
displaydictionaryWhat 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:idea-tag-names-reference]reference

#idea-tag-names

One 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:idea-tag-value-reference]reference

#idea-tag-value

One 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:idea-href-reference]reference

#idea-href

Where 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 down

The 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:idea-path-reference]reference

#idea-path

The 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 above

Reach 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:idea-body-reference]reference

#idea-body

One 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
ArgumentTypeDescription
namestr | labelThe idea to render, in the same name forms every accessor here takes.
unfurlint | autoThe 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.
limitint | noneHow 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:idea-row-reference]reference

#idea-row

One 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.

ArgumentTypeDescription
whencontent | noneThe date cell, already formatted. none draws an em dash.
isostr | noneA machine-readable date, which wraps when in a <time> element carrying it.
softboolWhether 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-attrsstr | array, dictionaryExtra 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.
titlecontentThe row’s title.
hrefstr | noneWhere the title links. none renders it as a span instead.
badgesarrayThe 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.
cellsarrayExtra 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.
tagsarrayTag names, which become .idea-tag-<tag> classes and a data-rookery-tags attribute on the <li>.
extra, attrsarray, dictionaryExtra 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:idea-row-body-reference]reference

#idea-row-body

The 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.