Anton Bakulin

Architecture

Contents

OnyxFolio is a Flask app split across seven modules with a strict one-way import chain.


Module structure

graph LR config --> obsidian_syntax config --> dataview obsidian_syntax --> converters dataview --> converters converters --> posts posts --> app view_helpers --> app
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 1load_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.


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:

  1. strip_leading_h1() — removes # Title since the template renders it from frontmatter
  2. convert_media()![[file.ext]] → lightbox image / slider / audio
  3. convert_links(url_index)[[Title]] → resolved URL
  4. convert_callouts()> [!type] Title<div class="callout callout-{type}">
  5. convert_checkboxes()- [ ] / - [x] → HTML checkbox lists
  6. convert_dataview()```dataview ``` blocks executed server-side
  7. markdown.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:

  1. <section>/_attachments/<filename>
  2. _attachments/<filename> (vault root)
  3. 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