Skip to content

MkDocs Material, Forge Markdown Viewer

Static site that renders the forge repo as a browsable, searchable docs site. Built with MkDocs + Material theme + awesome-pages auto-nav. Architecture redone 2026-04-29: split into a static HTTP server + a periodic-rebuild timer, replacing mkdocs serve (which had a flaky file watcher that silently dropped new files when render errors occurred upstream).

Architecture (post 2026-04-29)

Three systemd units, separation of concerns:

Unit Type Role
forge-mkdocs.service simple python3 -m http.server 8000 from /home/justinwieb/forge-mkdocs-site; static files only
forge-mkdocs-build.service oneshot mkdocs build --quiet; rebuilds the static site dir
forge-mkdocs-build.timer timer fires forge-mkdocs-build.service every 2 minutes

Effect: any new or edited .md is visible at the URL within ~2 minutes. No watcher state to corrupt, no in-memory rebuild crashes, and a single bad markdown file no longer takes down siblings (the static site keeps the prior good build for any file that fails to render).

Quick reference

Thing Path / value
Config infra/mkdocs/mkdocs.yml
Venv ~/.forge-venvs/mkdocs/
Build output /home/justinwieb/forge-mkdocs-site/ (this is what's served)
Service files /etc/systemd/system/forge-mkdocs.service, forge-mkdocs-build.service, forge-mkdocs-build.timer
Local URL http://192.168.86.50:8000/ (LAN), also Tailscale-reachable
Public URL https://mkdocs.justinsforge.com/ (behind CF Access, Justin only)
Tunnel media-server (id a55b7768-b42a-4d78-82b5-4a427bdbb9b7), via /cf-add

How it works

  • HTTP server is dumb-static: python3 -m http.server from the build dir. Doesn't crash on doc errors; just serves whatever is on disk.
  • Build timer fires mkdocs build every 2 min. If a doc has a render error, the build keeps the prior good output for that file.
  • docs_dir: ../../ in mkdocs.yml points at the forge repo root, so paths in nav match git paths.
  • exclude_docs: (gitignore-style, MkDocs ≥ 1.6) keeps secrets, scripts, logs, node_modules, etc. out.
  • mkdocs-awesome-pages-plugin auto-generates the nav from the filesystem.

Why we migrated off mkdocs serve (2026-04-29 incident)

mkdocs serve ran continuously and was supposed to rebuild on file changes. In practice its watcher silently dropped specific files when their markdown had unbalanced markers (e.g. unmatched ** or stray quote), creating an empty target directory with no index.html, returning 404 with no error in the logs. Long-standing intermittent. Replacing serve with build on a timer + python -m http.server removed the watcher entirely.

Force a rebuild now

sudo systemctl start forge-mkdocs-build.service     # one-shot rebuild (sync)
sudo systemctl status forge-mkdocs-build.service --no-pager
journalctl -u forge-mkdocs-build --no-pager -n 20   # see warnings/errors from the build

Restart the HTTP server

sudo systemctl restart forge-mkdocs
sudo systemctl status forge-mkdocs --no-pager

See timer schedule

sudo systemctl list-timers forge-mkdocs-build.timer --no-pager

Backup of old service unit

/etc/systemd/system/forge-mkdocs.service.bak-2026-04-29 keeps the prior mkdocs serve config in case rollback is ever needed.

Excluded paths

logs/ tasks/ data/ comms/ dashboard/ assets/ sites/ scripts/ personal/ clients/ notifications/ skills/ agents/ .claude/ .git/ .openclaw/ .vscode/ infra/mkdocs/ node_modules/ __pycache__/ .venv/ venv/

Plus ._* (macOS metadata), *.bak*, and most non-markdown extensions.

Cloudflare Access

The site MUST stay behind Cloudflare Access — it contains daily logs, infra docs, and references to credentials. Pattern matches n8n.justinkrystal.com:

  • Zero Trust → Access → Applications → Self-hosted
  • Application domain: mkdocs.justinsforge.com
  • Policy: email = [email protected] (Justin's hardened account)
  • Session duration: 24h is fine

If the Access app is missing, remove the tunnel ingress with /home/justinwieb/forge/scripts/cloudflare/cf.py rm forge justinkrystal.com until it's back in place.

The CF token at ~/.forge-secrets/cloudflare.env does NOT have Access:Apps scope as of 2026-04-28 — Access apps must be created manually via dashboard, or the token must be rescoped first.

Adding new content

Just write markdown anywhere in the forge repo (memory/, nexus/, docs/, brands/, infra/, etc.). The 2-min rebuild timer picks it up automatically, or run sudo systemctl start forge-mkdocs-build.service to rebuild immediately.

Adjusting nav order

Drop a .pages file in any directory:

nav:
  - intro.md
  - other-page.md
  - subdir

See awesome-pages docs for full syntax.

Encoding gotchas

MkDocs requires strict UTF-8. macOS AppleDouble ._* files and stray byte sequences will crash the build. The exclude list handles ._* already; if you hit UnicodeDecodeError, run:

python3 -c "
import sys; p = sys.argv[1]
with open(p,'rb') as f: data = f.read()
with open(p,'w',encoding='utf-8') as f: f.write(data.decode('utf-8', errors='replace').replace('…','\n'))
" path/to/bad.md

Why MkDocs Material

  • Static, no DB, no live process risk
  • Beautiful + mobile-responsive Material theme
  • Built-in lunr.js full-text search (ranked, instant)
  • Free and OSS (mkdocs-material 9.x)
  • Periodic rebuild via systemd timer keeps content fresh without watcher fragility

Adding more zones / hostnames

To expose forge under a different host, use /cf-add <host> <zone> http://192.168.86.50:8000. The systemd service binds 0.0.0.0:8000 so any tunnel ingress pointing at that origin works.