A Julia package that bridges Obsidian vaults with Xranklin.jl static sites. It converts Obsidian-flavored Markdown — wikilinks, callouts, frontmatter, .base table views — into pages that Xranklin can render and deploy.
Example: BonhamLab/LabWebsite uses this package to publish a subset of its private Obsidian vault as a public lab wiki at lab.bonham.ch.
- Converts
[[wikilinks]]to standard Markdown links, with graph edge tracking - Converts
> [!TYPE] Titlecallout blocks to styled HTML - Converts YAML frontmatter to Xranklin's TOML format
- Renders embedded
.basefile table views (Obsidian Bases) as HTML - Copies media assets from the vault to
_assets/vault/ - Generates
_assets/graph_data.jsonfor an interactive D3.js knowledge graph - Live-reloading watcher for local development via
watch_vault
ObsidianXranklin is not in the General registry. Add it to your Xranklin site's project by URL:
using Pkg
Pkg.add(url="https://github.com/BonhamLab/ObsidianXranklin.jl")Commit the resulting Manifest.toml so CI can reproduce the exact environment with Pkg.instantiate().
Note: If your site's Julia environment uses SSH remotes (e.g.
git@github.com:), Julia's built-in LibGit2 cannot use the system SSH agent. SetENV["JULIA_PKG_USE_CLI_GIT"] = true(e.g. in your shell profile orstartup.jl) to makePkguse the systemgitbinary instead, which respects your SSH agent and~/.ssh/config.For CI, the simplest fix is to ensure the
repo-urlinManifest.tomluses an HTTPS URL for any public packages so no authentication is needed at all.
Your Obsidian vault is typically a git submodule inside the Xranklin site:
my-site/
├── vault/ ← git submodule (your Obsidian vault)
├── _assets/
├── _layout/
├── config.jl
├── make.jl ← runs sync_vault before build
├── utils.jl ← imports ObsidianXranklin for live dev
└── Project.toml
To mark a note for publishing, add publish: true to its YAML frontmatter:
---
title: My Protocol
publish: true
tags:
- protocol
- wetlab
---Alternatively, pass publish_folders to auto-publish all notes in specific vault folders without requiring per-note frontmatter.
Create a make.jl at your site root that runs before Xranklin builds the site:
using Pkg
Pkg.activate(@__DIR__)
using ObsidianXranklin
ObsidianXranklin.sync_vault("vault", ".";
output_dir = "notes", # published notes appear at /notes/<slug>/
index_note = "my-wiki-home", # this note's slug becomes /notes/index.md
)Run it before serving or building:
julia --project make.jl
julia --project -e 'using Xranklin; serve()'| Keyword | Default | Description |
|---|---|---|
output_dir |
"notes" |
Site subdirectory for published notes |
index_note |
nothing |
Slug of the note to copy to <output_dir>/index.md (the section homepage) |
publish_folders |
[] |
Vault-relative folder prefixes whose notes are auto-published without publish: true |
skip_folders |
nothing |
Folder prefixes to exclude. Falls back to vault_skip_folders in config.jl, then ["Templates/", "Attachments/"] |
Add watch_vault to your site's utils.jl so that vault changes trigger re-syncs while Xranklin's live server is running:
using ObsidianXranklin
# Re-sync whenever a vault file changes (polls every 2 seconds)
watch_vault("vault", ".";
output_dir = "notes",
index_note = "my-wiki-home",
)
# Expose the interactive graph view as {{obsidian_graph}} in templates
# (hfun_obsidian_graph is exported by ObsidianXranklin)The watcher runs as a background Task and is safe to re-evaluate — if utils.jl is re-loaded by Xranklin's live server, the old watcher stops cooperatively before the new one starts.
# Folders to exclude from vault discovery (in addition to the built-in defaults)
vault_skip_folders = ["Templates/", "Attachments/", "Private/"]
# Slug of the note used as the wiki section homepage
obsidian_home = "my-wiki-home"Add a page at e.g. notes/graph.md:
+++
title = "Note Graph"
+++
{{obsidian_graph}}
This renders an interactive D3.js force-directed graph of your published notes and their links, reading from _assets/graph_data.json generated by sync_vault.
Obsidian Bases files (.base) embedded in notes with ![[file.base]] are rendered as HTML tables. The table respects filters, order, and sort from the .base YAML config. Supported column properties:
file.name— note title, linked to its published URLfile.folder— vault-relative folder pathfile.mtime— last modified time, formatted in local timezonefile.tags/tags— rendered as styled pill badges- Any frontmatter key (e.g.
type,status)
If your vault is a private repository, the default GITHUB_TOKEN cannot clone it. Use a deploy key:
- Generate a key pair:
ssh-keygen -t ed25519 -C "github-actions" -f /tmp/deploy_key -N ""
- Add the public key (
/tmp/deploy_key.pub) as a read-only deploy key on the vault repo (Settings → Deploy keys). - Add the private key (
/tmp/deploy_key) as a repository secret (e.g.VAULT_DEPLOY_KEY) on the site repo. - Pass it to
actions/checkout— this makes checkout use SSH for submodule cloning instead of the HTTPS token path, which only covers the current repo:- uses: actions/checkout@v4 with: ssh-key: ${{ secrets.VAULT_DEPLOY_KEY }} submodules: true fetch-depth: 0
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize]
env:
GH_USERNAME: "your-github-username"
PREVIEWS_PREFIX: "previews/PR"
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions: write-all
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.VAULT_DEPLOY_KEY }} # omit if vault is public
submodules: true
fetch-depth: 0
- name: Set preview path for PRs
run: |
if ${{ github.event_name == 'pull_request' }}
then
echo 'PRID=${{ env.PREVIEWS_PREFIX }}${{ github.event.number }}' >> $GITHUB_ENV
else
echo 'PRID=' >> $GITHUB_ENV
fi
shell: bash
- name: Build and Deploy
uses: kescobo/xranklin-build-action@main
with:
SITE_FOLDER: "./"
PREVIEW: ${{ env.PRID }}
- name: Post preview URL on PRs
uses: thollander/actions-comment-pull-request@v2
with:
message: |
Preview available at https://your-domain.com/${{ env.PRID }}
if: github.event_name == 'pull_request'Note on vault syncing in CI:
kescobo/xranklin-build-actionruns the Xranklin build, which loads your site'sutils.jl. If yourutils.jlcallswatch_vault, the vault sync runs automatically as part of the build — no separate step needed. If you usemake.jlinstead, add an explicit step before the build action to run it.
A note is published if (in priority order):
publish: falsein frontmatter → never publishedpublish: truein frontmatter → always published- Note path matches a prefix in
publish_folders→ published - Otherwise → not published
Unpublished notes still appear in .base table views (so you can list all notes including private ones in a database view), but they are not written to the site and their wikilinks resolve to #wikilink-missing.
YAML frontmatter is converted to Xranklin's TOML format. Obsidian-internal keys (cssclasses, aliases, position, file, publish) are stripped. Dates are parsed and emitted as Date(year, month, day) with a using Dates import prepended automatically.
- Julia 1.12 world-age warning: Tag page generation in Xranklin triggers a world-age warning (
Detected access to binding in a world prior to its definition world) due to a Xranklin bug. As a workaround, avoid calling customhfun_*functions from_layout/tag.html— use static HTML for the nav in that layout instead. - Symlinks in vault: If your vault contains symlinks to files outside the vault directory (e.g. from a Zotero integration), those assets are silently skipped during copying rather than causing an error.