docs: more best practices codified

This commit is contained in:
Zef Hemel
2026-06-30 16:27:37 +02:00
parent 9d3642bc72
commit 283022c3e7
7 changed files with 150 additions and 3 deletions
+56
View File
@@ -0,0 +1,56 @@
---
description: A thin page whose body is primarily drived through queries that collects items from across your space.
tags: glossary guide
---
An aggregator page is a thin page whose body is primarily drived through queries that collects items from across your space.
Instead of hand-maintaining a list of all open questions, all ADRs, or all team members, you tag each item on its natural home page and let a single query page assemble the collection automatically. Add an item anywhere in your space, tag it, and the aggregator picks it up automatically.
# The pattern
Three moving parts combine to make an aggregator:
**1. A tag** you apply to items on their natural home page, for example, `#open-question` on any page that tracks an open question.
**2. An aggregator page** whose body is a single [[Space Lua/Integrated Query|SLIQ]] query that pulls everything carrying that tag:
```lua
${query[[
from t = index.contentPages("open-question")
order by t.name
select templates.pageItem(t)
]]}
```
The query lives inside `${...}` and renders as a live widget in [[Live Preview]]. Because the prose body explains the pattern, the page retains value even outside SilverBullet.
**3. A `tagPage` mapping** in [[CONFIG]] so clicking the tag in the editor jumps straight to the aggregator overview:
```lua
tag.define {
name = "open-question",
tagPage = "Open Questions",
}
```
See [[API/tag#tag.define(spec)]] for the full list of options `tag.define` accepts.
# Why aggregators beat hand-maintained lists
- **Zero maintenance** — to add an item, tag it on its home page; the aggregator updates itself on the next index cycle.
- **No drift** — the list is always exactly what the index says it is; typos or deletions surface instantly.
- **Single source of truth** — the item's data lives once, on the item's own page; the aggregator is just a view.
# Recipe
To set up a new collection:
1. **Pick a tag** — choose a short, lowercase, hyphenated name (`meeting-note`, `open-question`, `decision`).
2. **Write the aggregator page** — create a page (e.g. `Open Questions`) whose body is the SLIQ query above, substituting your tag. Use `index.contentPages("tag")` to filter out [[Meta Page|Meta Pages]]; use `index.objects("tag")` if you also want items from meta pages.
3. **Register `tagPage` in [[CONFIG]]** — add a `tag.define` block so clicking the tag navigates to your aggregator. Place `tag.define` calls in `CONFIG` so they run during the index phase.
4. **Link the overview from your index or catalog page** — add a `[[Open Questions]]` link wherever people would naturally look for the collection.
# Examples in this manual
This docs space already uses the pattern for two collections:
- **[[ADR]]** — aggregates all pages tagged `adr`; the `tag.define` in [[CONFIG]] maps `tagPage = "ADR"`.
- **[[Architecture]]** — aggregates all pages tagged `component`; mapped via `tagPage = "Architecture"` in [[CONFIG]].
Open either page to see a live query-driven aggregator with no hand-maintained list.
+17 -1
View File
@@ -7,4 +7,20 @@ references:
An aspiring page is a [[Page|page]] that does not yet exist, but is already linked to.
Aspiring pages appear in the [[Page Picker]] (with a `Create page` hint) as well as in auto complete when creating [[Link]].
Aspiring pages appear in the [[Page Picker]] (with a `Create page` hint) as well as in auto complete when creating [[Link]].
# Finding dangling links
Every `[[link]]` to a non-existent page produces an `aspiring-page` object in the [[Object Index]], so you can query them to audit broken or forward-references:
${query[[
from t = index.aspiringPages()
where not string.startsWith(t.page, "Library/")
select { target = t.name, linkedFrom = t.page }
]]}
In this query, `t.name` is the link **target** (the page that does not yet exist); `t.page` is **where the link lives** (the source page). Filter on `t.page` to scope results to your own content.
Triage guidance:
- **Real typo**: fix the link on the source page.
- **Intentional placeholder**: you mean to write the page eventually, then leave it. Aspiring pages double as a "to-write" backlog and appear in the page picker as a reminder.
- **Library/meta target**: not yours to fix, those links are maintained by the library.
+18 -1
View File
@@ -31,4 +31,21 @@ When adding a [[Frontmatter]] section to a page, it becomes cleaner to move any
role: Data analyst
tags: sometag anothertag
---
```
```
# Page conventions
* **Title-Case page names**: name a page as you’d write its title in prose: `Customer Persona`, not `customer-persona`. See [[Names]] for additional rules and hard constraints.
* **No top-of-page H1**: the page name is already the title, don’t restate it as an `# H1`.
* **First line is a one-sentence summary**: the opening body line defines or summarises the page, this is what link previews and catalogs quote.
* **Absolute wiki links**: links are paths from the space root: `[[Folder/Page]]`. This way, links stay valid no matter where the linking page lives or moves. See [[Link]].
# Querying your space
SilverBullet's [[Object Index]] is the engine behind every live query:
* Prefer `index.contentPages()` over `tags.page` for page lists and lint sweeps — it filters out [[Meta Page|Meta Pages]] (pages tagged `meta` or `meta/*`) so Library and configuration pages do not pollute your results. See [[Object Index]].
* The index is **asynchronous** — after editing a page, expect a few seconds before queries reflect the change. Re-run before drawing conclusions. See [[Object Index]].
* For cross-cutting collections (all open questions, all ADRs), use the **aggregator page + `tagPage`** pattern instead of hand-maintained lists. See [[Aggregator Pages]].
* Find broken or forward-references via the `aspiring-page` tag. See [[Aspiring Pages]].
# Authoring Space Lua
When developing on a `space-lua` based feature, follow the edit, reload, check (browser) logs, verify loop: edit a script, run `System: Reload` (Ctrl-Alt-r) to re-execute all definitions, then **check the console logs**. A successful reload does not mean the script is healthy, Lua errors surface in logs, not always as a visible reload failure. For a programmatic “reboot to ready” call that also drains the index queue, see [[API/system#system.reboot()]].
+3
View File
@@ -87,4 +87,7 @@ While SilverBullet allows arbitrary metadata to be added to pages, there are a f
- "#tag2"
```
> **note** Recommended style
> Prefer **space-separated bare words on a single line** — `tags: market competitors` — over the YAML-list form. It is the most common SilverBullet convention and the least visually noisy. All the forms above remain valid for compatibility. If you care about Obsidian compatibility, use YAML lists.
For specific use cases, like [[^Library/Std/Infrastructure/Page Templates]] or [[^Library/Std/Infrastructure/Slash Templates]], frontmatter may have specific meaning.
+8
View File
@@ -30,3 +30,11 @@ Names _must_ also follow certain rules:
# Special characters
Certain HTTP reverse proxies may block “suspicious” characters (such as `?`, `#` and `;`) by default, including Traefik, [see this thread](https://community.silverbullet.md/t/traefik-proxied-setups-block-page-names-with-fix/3724/2) on how to work around this.
# Naming conventions
Beyond the hard rules above, there is a widely-used stylistic convention for everyday content pages:
* **Use Title Case with spaces**: name a page as you’d write it in prose: `Customer Persona`, `Release Process` — not `customer-persona`, `release_process`, or `CustomerPersona`. The page name doubles as its title and as inline link text, so a readable name reads well in context: `see [[Release Process]]`.
* **Keep the namespace flat** by default: place most pages at the top level and reach for folders only once a clear grouping earns it. See [[Best Practices#Flat name space]] for the rationale.
These are conventions, not enforced rules — your space is yours to organise however suits you.
+37 -1
View File
@@ -18,7 +18,10 @@ When you launch a fresh client for the first time, the object index will be buil
After the initial index process, the index will be kept up-to-date incrementally.
To forcefully reindex your space, run the `Space: Reindex` command.
> **note** Asynchronous indexing lag
> After saving a page (or external tools making updates to pages in your space) the index updates in the background and can lag a few seconds. A query run immediately after an edit _may_ return stale results for a bit. When this happens, wait briefly and re-run before drawing conclusions.
To forcefully reindex your entire space, run the `Space: Reindex` command. Depending on the size of your space, this can take anywhere from a second to minutes or longer.
## Indexing process
Objects are stored in your browser’s IndexedDB, implemented as a thin abstraction layer on top of [[API/datastore]]. Objects are always attached to a particular page.
@@ -43,6 +46,39 @@ The Object Index is generally queried using [[Space Lua/Integrated Query]].
Entry points are:
* [[API/index#index.objects(tag)]], e.g.: `index.objects("page")` (or the convenience wrappers like `index.pages()`, `index.tasks()`, etc.)
* `index.contentPages(tag?)` — returns only content pages, **excluding [[Meta Page|Meta Pages]]** (pages tagged `meta` or `meta/*`).
* `index.aspiringPages()` — returns all [[Aspiring Pages]] (pages linked to but not yet created). Useful for finding broken or forward-references.
* `tags.*`: as a convenience — `tags.page` is equivalent to `index.objects("page")`
If a `metatable` is defined for a particular tag with [[API/tag#tag.define(spec)]], the metatable is set for each object for the tag.
## Common patterns
A few idiomatic recipes to get started — see [[Space Lua/Integrated Query]] for the full clause reference:
**Content pages by last modified** (most recently touched first):
```lua
${query[[
from p = index.contentPages()
order by p.lastModified desc
limit 10
select { name = p.name, modified = p.lastModified }
]]}
```
**Inbound links to a page** (everything that links to `"My Page"`):
```lua
${query[[
from l = index.links()
where l.toPage == "My Page"
select { from = l.page, text = l.description }
]]}
```
**Dangling links in your own content**:
```lua
${query[[
from t = index.aspiringPages()
where not string.startsWith(t.page, "Library/")
select { target = t.name, linkedFrom = t.page }
]]}
```
+11
View File
@@ -63,6 +63,17 @@ Here are the conventions used by the [[Library/Std]] library:
> **note** Tip
> All your space-lua scripts are loaded on boot, to reload them without reloading the page, simply run the ${widgets.commandButton("System: Reload")} (Ctrl-Alt-r) command.
## Authoring loop
When iterating on a `space-lua` block, follow this loop:
1. **Edit** the script in the editor.
2. **Reload**: run `System: Reload` (Ctrl-Alt-r) to re-execute all `space-lua` definitions without a full page reload (you can reload the browser also if you prefer). Run `Space: Reindex` (via the command palette) if you also need the [[Object Index]] rebuilt with fresh data (e.g. if you use [[API/tag#tag.define(spec)]]).
3. **Check the brower’s logs**: a reload completing without a visible error **does not mean your script is healthy**. Lua syntax errors, load-time failures, and runtime exceptions during indexing or widget rendering all surface in the browser console logs, not always as a user-visible reload failure.
4. **Verify** the behaviour in the editor.
> **note** Note
> Lua examples in the docs use `lua` fenced blocks (not `space-lua`) so they are not activated on the docs site itself; when using snippets in your own space, change `lua` to `space-lua`. See the note at the top of this page.
# Expressions
One SilverBullet specific [[Markdown]] [[Markdown/Extensions]] is the `${lua expression}` syntax that you can use in your pages. This syntax will [[Live Preview]] to the evaluation of that Lua expression.