Sites and pages
A documentation site with 30 pages repeats its header, footer and theme list 30 times, and sooner or later one copy falls out of date. In chimera you set those parts once on a Site and give each Page only its own content.
Pages
A Page takes a title, its content and an optional file name. Without one, the file is named after the title in lowercase with dashes: Page("Getting started") writes getting-started.html.
const home = Page("Home", { h1("Welcome"), Paragraph("Start here.") }, "index.html")
const faq = Page("FAQ", h1("FAQ"))
faq:add(h2("Is it free?"), Paragraph("Yes."))
add appends more content later.
Nested pages
Pages can hold pages of their own, and those go in a folder named after their parent. A parent called index.html shares its folder with its children instead:
const docs = Page("Docs", h1("Docs"), "docs/index.html")
:add_page(
Page("Install"), -- docs/install.html
Page("Themes") -- docs/themes.html
)
const guide = Page("Guide") -- guide.html
:add_page(Page("Setup")) -- guide/setup.html
This page is guide/sites-and-pages.html, a child of the Guide. Nested pages get breadcrumbs above their content like the ones at the top of this page, and site:breadcrumbs(false) turns them off.
Links between pages
PageLink points at a Page object rather than a path. When the page renders, the link works out the relative path from its own folder. Moving a page never breaks the links to it. The text defaults to the page's title:
PageLink(faq) -- "FAQ"
PageLink(docs, "Read the docs")
PageLink("docs/install.html") -- a path from the site's root
Because every link is relative, the built site also works from disk over file://. You don't need a server to check it.
Tables of contents
TableOfContents() lists its page's headings and the pages under it. It's built when the page renders, from whatever the page holds at that moment. The box at the top of this page is one; on wide screens it moves into a column beside the text.
TableOfContents() -- this page's h2 and h3 headings, and its subpages
TableOfContents("page", 2) -- h2 headings only
TableOfContents("site", 3, "Pages") -- every page of the site, for a sidebar
The site
A Site holds the top-level pages and everything they share. build writes them all, creating folders as it goes, and returns the paths it wrote:
const site = Site("My docs")
:describe("Guides and reference for my library")
:default_dark_theme(catppuccin.Mocha)
:default_light_theme(catppuccin.Latte)
:add_page(home, docs, faq)
for _, path in site:build("out") do
print(`wrote {path}`)
end
Two pages with the same path are an error rather than a silent overwrite. describe sets the summary search engines show, and a page can override it with its own describe.
Header, footer and sidebar
Unless you say otherwise, every page gets a header with a menu of the top-level pages and a theme switcher, plus a footer with the site's copyright. Pass your own to replace either one, or none to leave it out:
site
:header(Header { Menu { PageLink(home, "My docs"), PageLink(docs), ThemeSwitcher() } })
:footer(none)
:sidebar(TableOfContents("site", 2, "Pages"))
SiteFooter builds a footer like this site's with link columns, a copyright line and legal links:
const footer = SiteFooter()
:link_column("Docs", { Link("Install", "#"), Link("Themes", "#") })
:link_column("Community", { Link("Discord", "#"), Link("GitHub", "#") })
:copyright("Ada Lovelace", "Built with chimera.")
Search
Readers of an API reference usually know a name, or half of one. They remember readFileSync but type read file, or they misspell it raedfilesync on the way. site:search() puts a box in the header that finds pages, sections and anything your own elements add while the reader types. It needs no server and works over file:// too:
site:search()
build writes the index the box searches, search-index.js, next to the pages. A site with its own header adds a Search() to it instead, and the index gets written all the same. Results show up under the box grouped by page, with the matching words marked. Arrow keys move through them and Enter opens one, and / or Ctrl+K jumps to the box from anywhere on the page.
How it matches
Every word of a query has to match somewhere in an entry: its title, its keywords, the title of its page or its text. Each word is tried several ways, and the best one counts:
Exact: the whole name, or the part after its last dot, soreadfilefindsfs.readfile.Prefix: the start of a name, likefs.read.Words: names split the way programmers write them.readFileSync,read_file_syncandReadFileSyncall have the words read, file and sync, soread syncandhttp serverfind them.Initials: the first letter of each word, sorfsfindsreadFileSyncandgcdfindsget_current_dir.Substring: anywhere inside a name, solinesfindssplitlines.Typos: a name within one or two typos, sosplitlniesfindssplitlines. Words under four letters allow none, which keepsosfrom findingfs.FullText: words in an entry's text and code samples, soutf8finds the section that explains encodings even when its title doesn't say so. Prose counts more than code, and a query that appears word for word ranks higher.
A query with capital letters ranks names written in the same case higher. Pass Search your own list of matchers to drop some or change how much each one counts. A matcher left out never matches, and weigh sets how much each part of an entry counts:
site:search(Search()
:matchers({ Exact(200), Prefix(), Words(), Typos(25, 1) })
:weigh({ page = 0 })
)
What it shows
An API reference's search might show functions only, five at a time, without the text around each match. These settings cover that:
Search()
:placeholder("Search the API")
:kinds({ "function", "type" })
:limit(5)
:min_score(20)
:snippets(false)
:group_by_page(false)
:shortcut("s")
Pages are the kind "page" and their sections are "section". Other kinds come from your own elements.
Your own entries
An element that stands for something readers search for, like a function in a reference, says so in search_entries. The index adds its entries and skips its children. Give the element its id before the page renders so the result has somewhere to land:
class ApiFunction implements Component
function tag(): string
return "div"
end
name = ""
summary = ""
function __init(self, name: string?, summary: string?)
self.name = name or ""
self.summary = summary or ""
self:id((string.gsub(self.name, "%W+", "-")))
end
function search_entries(self): { SearchEntry }?
return {
{
kind = "function",
title = self.name,
anchor = self:get_id(),
text = self.summary,
keywords = { "read" },
},
}
end
end
Your own matchers
Matching runs in the reader's browser, so a matcher of your own is a class with a little JavaScript. source returns a function that scores a query word against a name from 0 to 1. Pages store the matcher by its class name, so parsed pages find it again:
class Suffix(weight: number = 30) implements Matcher
function source(self): string?
return [=[
function (term, field) {
return field.lower.endsWith(term.lower) ? 1 : 0;
}
]=]
end
end
site:search(Search():matchers({ Exact(), Prefix(), Suffix() }))
Per-page changes
For anything the site doesn't set itself, customize runs a function on each page's Document just before it's written:
site:customize(function(document, page)
document:link_stylesheet("https://fonts.googleapis.com/css2?family=Inter")
if page.title == "Home" then
document:describe("My library: fast, typed and small")
end
end)
chimera only writes pages. Copy images and other files into the output folder yourself with fs.copy.