Architecture
Contents
OnyxFolio is a Flask app split across seven modules with a strict one-way import chain.
Module structure
| Module | Responsibility |
|---|---|
config.py |
Loads .env, sets VAULT_PATH, ATTACHMENTS_PATH |
obsidian_syntax.py |
Wiki-links, embeds, callouts, checkboxes, highlights, math, block IDs, transclusion, slugify |
dataview.py |
Server-side Dataview engine: TABLE/LIST/GROUP BY/WHERE/SORT/LIMIT |
converters.py |
Markdown pipeline coordinator: wires render_markdown() |
posts.py |
Two-pass load_posts(), maybe_reload(), ALL_POSTS, SECTION_ROUTES |
view_helpers.py |
Pure view utilities: breadcrumbs, adjacent posts, related posts, search highlight |
app.py |
Flask app init, context processor, all routes |
New features follow this chain — app.py imports from posts.py and view_helpers.py, never the reverse.
Data flow
Two-pass vault loading
Pass 1 — load_posts() scans all .md files, parses frontmatter, computes url_path and section from folder location, builds url_index for wiki-link resolution. Each post is indexed three ways: slugify(title), slugify(filename), and frontmatter slug. This means [[Filename]], [[Title]], and [[slug]] all resolve correctly even when the three differ.
Pass 2 — renders markdown for each file using url_index so [[Wiki Links]] resolve to correct cross-section URLs.
- Files with
type: listingortype: homepage→ registered inSECTION_ROUTES; not inALL_POSTS maybe_reload()checks file modification times on each request and reloads if anything changed
Routing
| URL | Source |
|---|---|
/ |
SECTION_ROUTES["/"] — file with type: homepage in vault root |
/blog |
SECTION_ROUTES["/blog"] — file with type: listing in blog/ folder |
/blog/my-post |
ALL_POSTS["/blog/my-post"] — regular post in blog/ folder |
/search |
Full-text search across ALL_POSTS |
/feed.xml |
RSS — latest 20 posts |
/sitemap.xml |
Auto-generated sitemap |
/attachments/<path> |
Media served from vault |
A single /<path:path> Flask route checks SECTION_ROUTES first, then ALL_POSTS, then 404.
Nav links are auto-generated from top-level SECTION_ROUTES keys (direct children of /). Posts with menu_order frontmatter are additionally pinned to the nav.
Markdown pipeline
render_markdown(md, path, url_index) in converters.py — order matters:
strip_leading_h1()— removes# Titlesince the template renders it from frontmatterconvert_media()—![[file.ext]]→ lightbox image / slider / audioconvert_links(url_index)—[[Title]]→ resolved URLconvert_callouts()—> [!type] Title→<div class="callout callout-{type}">convert_checkboxes()—- [ ]/- [x]→ HTML checkbox listsconvert_dataview()—```dataview ```blocks executed server-sidemarkdown.markdown()— standard extensions:fenced_code,tables,toc,md_in_html,codehilite
Media resolution
Images/videos must be in an _attachments/ subfolder relative to the .md file's folder. A post in blog/ uses blog/_attachments/. Resolution order:
<section>/_attachments/<filename>_attachments/<filename>(vault root)ATTACHMENTS_PATH/<filename>(from.env)
Multiple ![[...]] on one line → slider; separate lines → individual lightbox images.
Frontend
| File | Purpose |
|---|---|
frontend/templates/base.html |
Master template; JS (lightbox, sliders, copy); dynamic nav |
frontend/templates/index.html |
Homepage (type: homepage content) |
frontend/templates/listing.html |
Section listing (featured + regular posts) |
frontend/templates/post.html |
Individual post |
frontend/static/obsidian.css |
Dark theme (Catppuccin-inspired) + layout |
frontend/static/callouts.css |
Per-type callout styles |
frontend/static/code.css |
Code block styling |