Write a synopsis of this article that is appealing
For my server, I was looking for a light-weight knowledge base. Since I am to the only one maintaining the server, I needed very few features, but primarily I needed something that could not break, is light-weight, and easy to use.
I considered, and tried several options including Obsidian, HedgeDoc, SeaFile Wiki. In the end, I stuck with SilverBullet (https://github.com/silverbulletmd/silverbullet).
In it’s essence, SilverBullet is a stack of markdown files, served through the browser. The browser interface already provides nice features, such as shortcuts so you can write without leaving your keyboard, but also customization options.
Where SilverBullet really starts to shine is in its use of Lua, or actually a vernacular of Lua called SpaceLua.
SpaceLua can be used to add functionalities to your SilverBullet space. In this blog post, I wanted to share a few snippets that I’ve written. Because, as much as I love using SilverBullet, the documentation can sometimes be confusing. Especially since SilverBullet has been under active development, going from v1 to v2, many solutions presented online for v1, cannot be used for v2 (the version I am using) anymore.
To enable file versioning you could set SilverBullet up with git. However, I had already set up daily snapshots of the location so haven’t done so yet.
For complete transparency of the version I am using, I am using version 2.3.0. I moved from using the :latest tag
version: '3'
services:
silverbullet:
# image: ghcr.io/silverbulletmd/silverbullet:v2
image: ghcr.io/silverbulletmd/silverbullet:2.3.0
dns:
- 1.1.1.1
- 8.8.8.8
environment:
- SB_USER=admin:silver_bullet_password
ports:
- "3000:3000"
volumes:
- /opt/silverbullet:/space
restart: always
Generates breadcrumbs for page navigation. For example:
${breadcrumbs()} >>> 🏠 / silverbullet / components
breadcrumbs=breadcrumbs or {}
local function breadcrumb(path)
local mypath = path or editor.getCurrentPage()
local parts = string.split(mypath,"/")
local breadcrumbs = {}
for i,part in ipairs(parts) do
local current = ""
for j=1,i do
if current != "" then current=current.."/" end
current = current..parts[j]
end
table.insert(breadcrumbs, {name = current})
end
return breadcrumbs
end
breadcrust = template.new([==[/[[${name}]]]==])
function breadcrumbs(path)
return "[[index|🏠]]"..(template.each(breadcrumb(path),breadcrust))
end
Generates a table from frontmatter content. For example:
function frontmatterTable(key, columns)
-- Build markdown table header
local head = "|"
local separator = "|"
for _, col in ipairs(columns) do
head = head .. " " .. col .. " |"
separator = separator .. "--------|"
end
head = head .. "\n" .. separator .. "\n"
-- Build the row template
local row_cells = {}
for i, column in ipairs(columns) do
table.insert(row_cells, "${" .. column .. "}")
end
local function row_template(data)
local row_cells = {}
for i, column in ipairs(columns) do
local value = data[column] or ""
local ref = nil
if i == 1 then
ref = data.ref
elseif string.sub(value, -4) == ".com" then
ref = "https://" .. value
end
if ref ~= nil then
value = "[" .. value .. "](" .. "/" .. ref .. ")"
end
table.insert(row_cells, value)
end
return "| " .. table.concat(row_cells, " | ") .. " |\n"
end
-- Run query and assemble body
local body = template.each(query[[
from index.tag "page"
where table.includes(tags, key)
]], row_template)
return head .. body
end
Generates a hierarchical table of contents of pages below the current page. For example:
${toc(2)}
toc = toc or {}
function getDepth(pageName)
return #string.split(pageName, "/")
end
function row(depth)
local depth = depth or 1
local mypath = editor.getCurrentPage()
local minDepth = getDepth(mypath)
local tocIgnore = {}
if mypath == "index" then
mypath = ""
minDepth = 0
tocIgnore = {"CONFIG", "index", "Library/Std"}
else
mypath = mypath .. "/"
end
local pages = query[[
from index.tag "page"
]] or {}
local rows = {}
for page in pages do
local pageName = page.name
if string.startsWith(pageName, mypath) and not has(tocIgnore, pageName) then
local pageDepth = getDepth(pageName)
local relDepth = pageDepth - minDepth
if relDepth <= depth then
local indent = ""
for i = 1, relDepth - 1 do
indent = indent .. " "
end
table.insert(rows, { name = pageName, indent = indent})
end
end
end
return rows
end
rowTemplate = template.new([==[
${indent}../[[${name}]]
]==])
function toc(depth)
return template.each(row(depth), rowTemplate)
end
Checks if an array contains a value. For example:
${has({1, 2, 3, 4, 5}, 4)} >>> True
function has (tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
Converts ISO time to human time ago. For example:
${humanTimeAgo("2026-2-1T14:30:15Z")} >>> 10 days ago.
function parseIsoTime(iso)
local year, month, day, hour, min, sec = iso:match("(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)")
return os.time({
year = tonumber(year),
month = tonumber(month),
day = tonumber(day),
hour = tonumber(hour),
min = tonumber(min),
sec = tonumber(sec)
})
end
-- Convert ISO timestamp to "x ago"
function humanTimeAgo(iso)
local now = os.time()
local past = parseIsoTime(iso)
local delta = now - past
local minutes = delta / 60
local hours = minutes / 60
local days = hours / 24
local weeks = days / 7
local months = days / 30
local years = days / 365
if minutes < 1 then
return 'Now'
elseif minutes < 60 then
return string.format("%d minutes ago", math.floor(minutes + 0.5))
elseif hours < 48 then
return string.format("%d hours ago", math.floor(hours + 0.5))
elseif days < 15 then
return string.format("%d days ago", math.floor(days + 0.5))
elseif weeks < 9 then
return string.format("%d weeks ago", math.floor(weeks + 0.5))
elseif months < 49 then
return string.format("%d months ago", math.floor(months + 0.5))
else
return string.format("%d years ago", math.floor(years + 0.5))
end
end