Live mode — this page may load external map tiles and query remote endpoints; the standard hub is fully self-contained (same cells, no network).

Hub posts are ordinary versioned Markdown pages with small live cells. They borrow Observable's reactive notebook model — values can be named once and used by later cells — while remaining a static Eleventy site with all runtime code vendored in this repository. For Observable's wider ecosystem and its static-site framework, see the official Observable Framework documentation. Factoidal does not require an Observable account or a CDN at runtime.

A minimal query notebook#

Use an observable-js fence for a live cell. Give reusable values a name; later cells then depend on that value rather than duplicating data.

notebookTurtle = '<http://example.org/alice> <http://example.org/name> "Alice" .'
notebookDataset = fn.parse(notebookTurtle)
rows = fn.query(notebookDataset, `# The ex:name of every subject in the dataset declared by the cell above.
SELECT ?name WHERE { ?s <http://example.org/name> ?name }`)
return pretty(rows);

The runtime orders those cells by their references and recomputes dependent cells when a reader edits and runs one. The available bindings are deliberately small: fn is the typed Factoidal interface (parse, query, and selected other capabilities); pretty renders binding rows; d3, Plot, html, and md support presentation. Prefer fn to the lower-level Factoidal binding.

Keep cells small and make I/O explicit. A live cell should demonstrate a real capability, not merely decorate prose. Use ordinary fenced code for static syntax examples. Browser pages are sandboxed and normally make no network requests; a post that intentionally allows a third-party dependency must declare and test that boundary.

Closed cells and library cells at the end#

A fence line can carry flags after the language: closed mounts the cell collapsed, and title="..." sets the summary line a reader sees before opening it. Use closed for a cell whose value a reader is more likely to call than to read line by line — a helper library, a large fixture.

A closed cell does not have to be defined before the cells that use it. hub.njk mounts a post's cells in two passes: pass 1 registers every cell's declared name, then pass 2 mounts every cell and hands it to the reactive runtime, which computes the dependency order from those names rather than from document order. The cell below calls notebookHelpers.count(rows); notebookHelpers is defined in the closed cell at the very end of this post, and it still resolves, because the runtime resolves each variable's inputs by name, not by mount order. The one requirement: the defining cell must be a named cell (name = ...), so there is a name for this one to reference.

notebookRowCount = notebookHelpers.count(rows)

Put a library cell last when its content would otherwise push the post's actual subject further down the page — several small helper functions, a large block of fixture data, anything meant to be reused rather than read on first pass.

The test contract#

Every post with cells has a Node test under tests/hub/. It extracts the literal fenced source from the Markdown and executes it against the same typed API used in the browser. That pins the page's examples to real results rather than allowing prose and code to drift apart. The implementation details and full authoring reference remain in README.md.

For a richer reactive example, see post 26.

notebookHelpers = ({
  count: (rows) => rows.length,
  labels: (rows) => rows.map((row) => row.get('name').value),
})