Developer reference for the xref: cross-reference feature. User docs: xref — Cross-Reference Enrichment.
When an xref-enabled note opens, nb-web calls /api/xref with:
target=hledger:(or folder-scoped)stems=reconcili,anomali,period,balance,...— stemmed words extracted from headings
On the server, every target note is indexed once (cached until its directory mtime changes). The index maps word stems → note entries. A note's title and its annotation sidecar text are both indexed.
Both Python (server) and JavaScript (client) apply the same suffix-stripping rules, in order:
| Suffix removed | Replacement |
|---|---|
ations, ation |
(none) |
ings, ing |
(none) |
ions, ion |
(none) |
ments, ment |
(none) |
ness |
(none) |
ities, ity |
(none) |
ies |
y |
ves |
f |
ed, ly, er |
(none) |
es, s |
(none) |
Each word must be at least 4 characters before stemming; the resulting stem must be at least 3 characters.
After stemming, a heading word stem matches an index stem if:
- Exact match — always matches, regardless of length
- Prefix match — either stem starts with the other, and the shared prefix is ≥ 5 characters
This handles plurals, conjugations, and compound forms without a full morphological analyser.
type: book notes use {{inline:}} to pull in chapters lazily (viewport-driven). xref needs all headings before it can inject references, so it hooks into the inline loading sequence:
- Eager inlines (chapters near the viewport) load sequentially;
nb-inlines-settledfires when done. - xref checks whether deferred chapters remain; if so calls
forceAll()— triggers every outstanding inline fetch immediately rather than waiting for the user to scroll. nb-inlines-completefires when the last chapter resolves.- xref scans all headings across all chapters.
Side effect: opening a book with xref: front-loads all chapter fetches. On a local server this is imperceptible; the status pill counts them all down at once up front. For very long books, add xref: to individual chapter notes instead of the book root to avoid forcing all fetches eagerly.
GET /api/xref?target=<target>&stems=<stem1>,<stem2>,...
| Parameter | Description |
|---|---|
target |
notebook: or notebook:folder/ |
stems |
Comma-separated list of already-stemmed words to look up |
Returns a JSON object keyed by stem, each value an array of {selector, title} entries:
{
"reconcili": [
{"selector": "hledger:reconciliation.md", "title": "hledger reconciliation"}
],
"anomali": [
{"selector": "hledger:check.md", "title": "hledger check"}
]
}The cache is per-target-string and invalidated by directory mtime. No auth required (nb-web is a local tool).
xref: is resolved via effective_xref in the api_note response alongside effective_access and effective_checks:
'effective_xref': (nb_meta['xref'] or '') if 'xref' in nb_meta else None,- Key present with any value (including bare
xref:→ PythonNone) → normalize to""(suppress) - Key absent from config chain → return
None→ JS falls through to note's ownmeta.xref
This means null (JSON) = "not set in chain, check note FM" and "" = "explicitly suppressed at this level." The ?? operator in JS correctly distinguishes them because ?? only falls through on null/undefined, not "".
In main.js, both the guard and xrefRaw use effective_xref ?? meta.xref:
if (note?.effective_xref ?? note?.meta?.xref) _enrichXref(container, note);
// ...
const xrefRaw = note.effective_xref ?? note.meta?.xref;The xref scanner queries:
rendered.querySelectorAll('h1,h2,h3,h4,h5,h6,[data-xref-heading]')Any element with data-xref-heading participates in xref exactly like a real heading. The stemmer reads .textContent from each element; the attribute value is ignored (the text is the signal).
Two sites currently emit data-xref-heading:
| Site | Element | When |
|---|---|---|
_buildConfigForm (main.js) |
.nb-cfg-label spans in the dotfile config dialog |
Always — one per config field (access, pinned, check, …) |
_configRender (nbweb-codeblocks.js) |
<span> key names in the "all keys" value cell |
Config codeblock in show-all-keys mode |
_configRender (nbweb-codeblocks.js) |
.nb-config-key-label div |
Config codeblock in single-key mode |
Adding data-xref-heading to a new element is the extension point for any renderer that wants its labels to participate in xref — no changes to the xref scanner needed.