SilverBullet

SilverBullet

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.

Setting up SilverBullet

For complete transparency of the version I am using, I am using version 2.3.0. I moved from using the :latest tag

docker-compose.yml
Copy
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

Useful components

breadcrumbs

Generates breadcrumbs for page navigation. For example:
${breadcrumbs()} >>> 🏠 / silverbullet / components

breadcrumbs
Copy
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

frontmatterTable

Generates a table from frontmatter content. For example:

frontmatterTable
Copy
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

toc

Generates a hierarchical table of contents of pages below the current page. For example:
${toc(2)}

Copy
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

Useful methods

has

Checks if an array contains a value. For example:
${has({1, 2, 3, 4, 5}, 4)} >>> True

has
Copy
function has (tab, val) for index, value in ipairs(tab) do if value == val then return true end end return false end

humanTimeAgo

Converts ISO time to human time ago. For example:
${humanTimeAgo("2026-2-1T14:30:15Z")} >>> 10 days ago.

humanTimeAgo
Copy
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