Markdown utility for Obsidian vaults and plain markdown docs: builds a vault into one HTML or markdown file, serves it rebuilding on change, checks it, counts it, links mentions, rewraps lines, moves pages rewriting links, and is a language server for editors.
Python 3.11 or later, with mise or uv:
mise use -g pipx:hiqsol/uwiki
uv tool install git+https://github.com/hiqsol/uwiki
Vault: folder of markdown pages, rooted at the nearest folder with .obsidian, else the one given paths share.
Commands take its folders and files, skipping hidden ones, node_modules and vendor,
--exclude GLOB skips more, --include GLOB takes back, globs as in .gitignore,
{a,b} matching either.
Folder page: README.md, index.md or page named as its folder, the contents of the folder.
Title of a page: front matter title, else the heading it starts with, else its name;
AGENTS.md is always titled AGENTS.md, the name tools look for.
Reading order of build, llms and the HTML sidebar: folder page, then pages in the order it links them,
every other page right after the first page linking it, unlinked ones last.
Links resolve the same way in every command:
[[Page]]by name, then by front matteraliases, nearest first;[[Page|text]],[[folder/Page]][[Page#Heading]],[[Page#3]]item 3 of the first numbered list of Page,[[Page#Heading#3]]under Heading[text](../Page.md#heading)by path relative to the page, heading by its GitHub anchor- embeds
![[Page]],![[Page#Heading]],![[image.png|300]], callouts> [!note] Title
Orphan: page no chain of links leads to from the root folder page.
uwiki build <paths> [-o OUTPUT] [--title TITLE] [--html | --md] [--no-backlinks] [--no-front-matter]
uwiki watch <paths> [build options] [-q] [--interval 0.5]
uwiki serve <paths> [watch options] [-p 8000] [--host 127.0.0.1] [--open]
uwiki check <paths> [--orphans] [--unlisted] [--mentions] [--mentions-exclude GLOB] [--no-folder-links] [--max-lines N]
uwiki link <paths> [--dry-run] [-w 70]
uwiki llms <paths> [-o FILE] [--title TITLE] [--check]
uwiki index <paths> [--check]
uwiki ls <paths> [--external] [-z]
uwiki stats <paths> [--by folder|KEY] [--top N] [--json]
uwiki wrap <paths> [-w 70] [--check]
uwiki mv <from> <to> [--dry-run]
uwiki lsp
uwiki <command> --help lists all options.
buildwrites<name>.htmland<name>.md, named by--titleor the folder,-o out/Wiki.htmlonly one; the wiki is titled as its index page is (README.md,index.md), unless--titleis given; a page shows its folders, metadata and front matter; a link to a page left out fails the buildwatchbuilds again on change of a page, attachment or the config,servealso serves the HTML onhttp://127.0.0.1:8000, reloading the page, its blocks withdata-lineof the source line they start at; both restart on change of uwiki's own code, as on upgrade, unless it fails to import, and of options the config gives themcheckreports unresolved links, headings and items, missing images, front matter out of[schema]and list numbering aspath:line: message, exits 1 if any;--orphansreports orphans,--unlistedand--mentionswarn about pages their folder page does not link and plain text mentions of page titles, outsideindexblocks and pages--mentions-excludematches,--max-linesreports longer pages,--acyclic KEYpages a chain of pages that front matter key names leads back to,[check] inversepage valued keys that do not name each other back;--stale-days Nwarns about pages whose latest--stale-date KEYis older,--stale-until KEYabout pages whose date in that key has come;[check.code-paths]reports code spans ending in.mdor holding a/in matching pages that name no file or folder in the folder it giveslinklinks the plain text mentionscheck --mentions-everywarns about:[[Star]]sllmswrites llms.txt: a section per folder of- [title](path): descriptionindexfills<!-- generated: NAME -->...<!-- /generated -->blocks of pages with the other pages of their folder,- [title](path): descriptioneach, keeping their order and the lines linking subfolders,--checkreports blocks out of datelslists the files commands take,--externalexternal links, for other tools:uwiki ls -z docs | xargs -0 typos,uwiki ls --external docs | lychee -statscounts pages, words, lines, headings, items, links and images by folder or front matter keywraprewraps paragraphs and list items longer than-w 70,--checkreports themmvmoves a page, folder or attachment rewriting every link to it, withgit mvin a git work treelspserves definition, hover, references, outline, completion and check diagnostics over stdio; Neovim 0.11+:
vim.lsp.config('uwiki', { cmd = { 'uwiki', 'lsp' }, filetypes = { 'markdown' }, root_markers = { 'uwiki.toml' } })
vim.lsp.enable('uwiki')uwiki.toml, or [tool.uwiki] in pyproject.toml, in the vault root or the nearest folder above it
up to the git work tree root, holds option defaults:
top level ones for every command, a table for a command, [build] also for watch and serve.
[schema] gives front matter keys check takes, dotted ones nested in a map or in every map of a list,
_required the ones a page has, a map key every key nested in it,
[schema."glob"] adds rules for pages the glob matches from the config folder, _inherit = false drops the ones before.
[check] layers makes values of a key layers:
a page links only to pages of its layer or earlier ones, a folder page comes no later than its pages.
exclude = ["drafts"]
[check]
orphans = true
mentions = true
mentions-ignore = ["Go"] # terms
mentions-exclude = ["prompts/**"] # pages, globs from the config folder
max-lines = 1500 # a page an agent reads in one go
max-lines-ignore = ["log.md"] # globs from the config folder
acyclic = ["supersedes", "superseded_by"] # no chain of these page valued keys leads back
inverse.supersedes = "superseded_by" # a page B supersedes names B in superseded_by, and back
stale-days = 183 # warn when the latest of these dates is older
stale-date = ["verified.at", "generated.at"]
stale-until = "stale_after" # warn once this date has come
code-paths."skills/**/map.md" = "docs" # `a/b.md` in such pages names docs/a/b.md
layers.scope = ["core", "duel", "advanced"]
[index.adrs] # <!-- generated: adrs --> blocks
line = "* [{title}]({path}), {date}: {description}" # {front matter key}, {title}, {stem}, {path}
sort = "-date" # by key, descending; else lines keep their order
empty = "None yet."
exclude = ["log.md"]
[schema] # keys in the order HTML shows them
_required = ["status"]
status = ["stub", "draft", "stable"] # allowed values
date = "date" # or a type: string, number, bool, date, datetime, page, list, any
description = "string(250)" # string of at most 250 characters
supersedes = "page" # names a page as links do: Page, [[Page]], ../Page.md
generated.at = "datetime" # generated: { by: me, at: 2026-01-02T03:04:05Z }
[schema."docs/**"] # folder rules add to the top level ones
owner = "string"
[schema."**/index.md"]
_inherit = false # folder pages: no keys required, none allowedOne self-contained file, light or dark by system preference, works from file://:
- pages render as GitHub and Obsidian show them: CommonMark with tables, task lists and strikethrough; a page is laid out and drawn as it nears the window, so a long vault opens fast
- sidebar of contents following the page being read, its edge drags its width,
a click on a branch title folds or unfolds it too; guide lines show nesting, the one of the page
being read stands out; share of tokens of a branch with all pages under it right of its title, of a page
on hover, tokens beside it on hover; bars from the right edge as
dustdraws them: the share of an entry over the lighter one of its branch; when filtering, shares are of pages found, beside counts of hits - hover preview of a link target under the folders it is in, links in it preview too, sidebar entries beside the sidebar
- metadata and front matter top right of page header, problems marked at their lines;
metadata folds to problems, tokens, backlinks and file name, unfolds to what
checkreports, words, tokens and reading time, backlinks, file name and folder; file name and folder only where title and folders don't tell them; nested front matter keys beside the key above them in the same table - tokens estimated as 4 bytes of UTF-8 each, so text in other scripts than Latin counts more
/focuses the box filtering pages: words,"exact text",key:valuefront matter,in:folder,is:problem,-term, e.g.scope:core -status:stub deck;Entergoes to the next match, the box goes to the URL as?q=,×or thexkey clears itCtrl Kgoes to a page or folder by letters of its name in order, e.g.dkbldfor Deck Building: title matches first, then aliases, then paths; whole words and starts of words before scattered letters- chips of
[schema]values with page counts, a click adds or removes one - pages, words and tokens shown out of all under the chips, after a triangle folding or unfolding the whole tree; a click on the count of problems shows only pages with problems
−and+by the title or the-and+keys make the text of pages and previews smaller or larger, the line buttons beside them or the<and>keys put their lines closer or wider apart,0restores both, remembered per browserjandkgo to the next and previous page,tto the top,[and]fold and unfold the whole tree?by the title or the?key opens help: what the page is, uWiki version, build time and the git commit built from, keys, search terms and mouse gestures; underservewhether the server answers; its config link shows build options and the vault config file as they were- code blocks highlighted by Prism and
mermaidblocks drawn as they near the window, the libraries loaded from CDNs on first need; offline, code stays plain
make test # run tests
make patch|minor|major # test, bump version, commit and tag it
make release # push master and the tag, the release workflow publishes it
make up # install the tagged version with mise
MIT, see LICENSE.