Architecture

Architecture

This document provides a technical overview of mbr’s architecture, design decisions, and implementation details.

High-Level Overview

flowchart TD
    subgraph Input
        FILES[Markdown Files]
        CONFIG[.mbr/ Config]
    end

    subgraph Core
        SCANNER[Repository Scanner]
        PARSER[Markdown Parser]
        RESOLVER[Path Resolver]
        TMPL[Template Engine]
    end

    subgraph Output
        SERVER[Axum Server]
        BUILD[Static Builder]
        GUI[Native Window]
    end

    FILES --> SCANNER
    CONFIG --> TMPL
    SCANNER --> RESOLVER
    RESOLVER --> PARSER
    PARSER --> TMPL
    TMPL --> SERVER
    TMPL --> BUILD
    SERVER --> GUI

Rust Modules

ModulePurpose
main.rsEntry point, CLI mode selection
cli.rsCommand-line argument parsing (clap)
config.rsConfiguration loading (figment)
server.rsHTTP server (axum)
build.rsStatic site generator
browser.rsNative GUI window (wry/tao)
vault/Storage seam: every repository read and write goes through a Vault (LocalVault on the desktop)
path_resolver.rsURL to file path resolution
markdown.rsMarkdown parsing (pulldown-cmark)
templates.rsTemplate rendering (tera)
repo.rsRepository scanning
vid.rsVideo shortcode handling
oembed.rsURL metadata extraction
quicklook.rsmacOS QuickLook extension - markdown rendering plus verbatim plain-text/source previews
errors.rsError type definitions

Request Flow

flowchart TD
    REQ["HTTP Request<br/>/docs/guide/"] --> HANDLER[Request Handler]
    HANDLER --> RESOLVER["Path Resolver<br/>resolve_request_path()"]

    RESOLVER --> |MarkdownFile| PARSE["Parse Markdown<br/>+ Extract Frontmatter"]
    RESOLVER --> |StaticFile| SERVE[Serve File]
    RESOLVER --> |DirectoryListing| LIST[Generate Listing]
    RESOLVER --> |NotFound| E404[404 Response]

    PARSE --> RENDER[Render Template]
    LIST --> RENDER
    RENDER --> CACHE["Add Cache Headers<br/>ETag, Last-Modified"]
    CACHE --> RESP[HTTP Response]
    SERVE --> RESP

Path Resolution

The ResolvedPath enum represents resolution outcomes:

enum ResolvedPath {
    MarkdownFile(PathBuf),
    StaticFile(PathBuf),
    DirectoryListing(PathBuf),
    NotFound,
}

Resolution order:

  1. Direct file match → StaticFile
  2. Directory + index file → MarkdownFile
  3. Path with / suffix matching .md file → MarkdownFile
  4. File in static folder → StaticFile
  5. Directory without index → DirectoryListing
  6. Nothing matches → NotFound

One canonical URL per page

A markdown page is served at exactly one URL — the directory-style one:

FileCanonical URL
docs/guide.md/docs/guide/
docs/index.md/docs/
README.md/README/

The trailing slash is load-bearing, not cosmetic. It decides the base a browser uses for the page’s own relative links: from /docs/guide/ a ../other/ href resolves to /docs/other/, but from /docs/guide it resolves to /other/. Serving a page at the slashless URL therefore breaks every relative link on that page — one click after the wrong URL, which is what makes the symptom so hard to trace.

Server and GUI mode answer any non-canonical spelling — /docs/guide, /docs/guide.md, /docs, /docs/index/ — with a 301 to the canonical URL, preserving the query string. Fragments are not echoed in Location, because per RFC 9110 §10.2.2 the client reapplies the original one. Static files and directory listings are never redirected.

Static builds have no server, so the redirect cannot save them: there the correct href has to be emitted at render time, and the build’s link checker reports any that are not (see Build Mode).

link_transform::transform_link rewrites each authored href for the trailing-slash convention:

Authored in docs/guide.mdEmitted hrefLands on
other.md../other//docs/other/
other../other//docs/other/
other/../other//docs/other/
subfolder/index.md../subfolder//docs/subfolder/
../folder/file.md../../folder/file//folder/file/
photo.png../photo.png/docs/photo.png
Makefile../Makefile/docs/Makefile
/docs/other/unchanged (server)/docs/other/
https://…, mailto:…unchangedoff-site

The extra ../ on non-index pages compensates for the trailing slash; index pages already sit at a directory URL and get none.

An extension-less target is ambiguous — ../folder/file could be a markdown page or a file literally named file. Guessing either way corrupts the other (LICENSE, Makefile, Dockerfile are real, common link targets), so mbr asks the repository through the same path resolver a live request uses. Contexts with no repository — CLI and QuickLook rendering — treat an extension-less target as a static file.

ModeWhereWhat it reads
Server / GUIGET /{page}/errors.json (page_errors.rs)Every <a href> in the rendered HTML
Buildafter rendering (build.rs::validate_links)Every <a href> in the generated HTML

Both read the href that was actually emitted rather than re-deriving one from the markdown source: a checker that re-derives re-applies the same rules the transform used, so a transform defect is invisible to it by construction. Both report three kinds of problem — a target that does not exist, a page link missing its trailing slash, and a ../ chain that escapes the repository root (which browsers silently clamp).

Links into mbr’s own /.mbr/ namespace are skipped in server/GUI mode: the media viewers (/.mbr/videos/?path=… and friends) and the JSON endpoints are axum routes with no file behind them, and /.mbr/theme.css-style assets fall back to the compiled-in defaults when the repository has no .mbr/ folder. The path resolver never sees any of it, so its verdict would be a 404 claim about a URL that serves 200. A static build has no such gap — it writes that whole tree itself, so those files are checked like any other — with one exception shared with the server: links to the media viewers carrying a path= query (url_path::is_mbr_query_route) are skipped, since they are addressed by query rather than by a file and the slashless spelling (/.mbr/videos?path=…) would otherwise be reported as missing its trailing slash.

Design Decisions

On-the-Fly Rendering

mbr renders markdown on every request rather than using caches:

Rationale:

Performance:

No Temp Files

mbr never writes to the filesystem during normal operation:

Rationale:

Exception: Static build mode writes to output directory.

Static builds use symlinks instead of copying assets:

Rationale:

Limitation: Requires Unix-like OS (macOS, Linux).

Parallel Scanning

Repository scanning uses rayon for parallelism:

// Parallel directory traversal
files.par_iter().for_each(|file| {
    // Process each file concurrently
});

Benefits:

Storage Through a Vault

The core never assumes std::fs for the repository. Scanning, URL resolution, content search, the task index, page rendering and every write the server makes go through a Vault — on the desktop a LocalVault, which is plain std::fs with the repository root canonicalized once. Listing is one call per directory (with sizes and times already attached), so the desktop pays one stat per entry, and a mobile or sync-library backend is called per directory rather than per file. Where a real path exists, the hot paths (grep, static file serving, media probes) use it directly.

Paths inside a vault are VaultPaths: relative, /-separated and normalized, so a request can never name anything above the repository root, on any platform. Symlinks are followed like they always were. A link that leads out of the repository (or out of an external static folder) is judged where it is first resolved — a request, a write target, a folder the scanner descends into: either it is accepted as a read-only mount, addressed at the link’s own location so every index key and URL is the one a real folder there would have, or it is refused and stays outside, exactly as before. Inside a mount, .. and nested links can only reach the mount, the repository, or another accepted mount; every write into one is refused by the vault itself. The rules are in Symlinked folders.

Hidden paths (a segment starting with ., other than .well-known) are never resolved, from the repository, the static folder or a mount, and the static build never places them — see Hidden files.

Template Fallback Chain

Templates resolve through a layered system:

1. --template-folder flag
2. .mbr/ folder in repo
3. Compiled-in defaults

Each layer can override specific files while inheriting others.

Performance Goals

mbr prioritizes speed in these areas:

AreaGoalApproach
Server startup< 1 secondLazy initialization
Page render< 50msSIMD markdown, in-memory template caching
Site build< 1 file/msParallel rendering
Static page load< 100msMinimal JS, client caching

Optimization Techniques

Lazy Loading:

Concurrent Processing:

Efficient Data Structures:

Key Dependencies

CratePurpose
axumHTTP server framework
tokioAsync runtime
pulldown-cmarkMarkdown parsing
teraTemplate engine
figmentConfiguration management
wryWebView wrapper
taoWindow management
mudaNative menu bar
rayonParallel iteration
papayaConcurrent hash maps
proptestProperty-based testing

Error Handling

mbr uses custom error types with thiserror:

#[derive(thiserror::Error, Debug)]
pub enum MbrError {
    #[error("Configuration error: {0}")]
    Config(#[from] ConfigError),

    #[error("Build error: {0}")]
    Build(#[from] BuildError),

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

Errors propagate with context.

Testing Strategy

TypeLocationPurpose
Unit testssrc/*/testsModule behavior
Property testssrc/*/proptestsInvariant verification
Integration teststests/HTTP endpoint testing
Doc testsInlineExample correctness

Property-Based Testing

Key invariants verified with proptest:

Future Considerations

Potential Optimizations

Extensibility Points