Sign inSign up

mooloco/mdviewer

By mooloco

•Updated 15 days ago

Pure static read-only Markdown viewer (midway-v2, nginx, amd64+arm64)

Image
0

471

mooloco/mdviewer repository overview

⁠mooloco/mdviewer:midway-v2

Staging branch build with dark mode, TOC, lightbox, KaTeX, search index, PWA.

Full docs in the MDViewer repo midway branch README.

⁠Markdown Viewer

中文⁠

Pure static, read-only Markdown viewer. No backend, no database. Single nginx:alpine serves the frontend (mooloco/mdviewer:midway-v2, multi-arch amd64/arm64); the docs directory is mounted read-only at runtime (local bind or NFS). Rendering is done in the browser with marked + DOMPurify + highlight.js, KaTeX loads on demand for math.

Deployed instances:

  • Production (main branch image mooloco/mdviewer:v1): docs from NFS, one shared web/ root.
  • Staging (midway branch, this doc): verify frontend changes on a copy of web/ with its own demo docs + own search-index.json before syncing to production.

⁠Features

  • Sidebar file tree (directories + .md only, dotfiles hidden) with breadcrumb
  • Sidebar search over a static /search-index.json (title/path/snippet, 250 ms debounce, <mark> highlight), plus recent-files folder (max 10, collapsed by default, independently scrollable)
  • Markdown rendering, code highlight, relative link/image rewrite (incl. CJK paths)
  • Code blocks: language tag, top-left copy button (clipboard API with fallback), auto-collapse for long blocks (>20 lines or >2000 chars) with expand toggle
  • Light / dark theme (GitHub + GitHub Dark highlight themes, persisted in localStorage), font size standard/large, settings dialog at sidebar bottom
  • Optional floating table of contents (h1–h3, off by default, slider switch in settings, active-section highlight, FAB on desktop + topbar on mobile)
  • Image lightbox (lazy loading, click to zoom, prev/next, pinch zoom, swipe navigation, 44 px touch targets)
  • Math via KaTeX, loaded on demand only when $…$ / $$…$$ is detected (code fences excluded, raw TeX kept on failure/offline)
  • Home button (back to docs root, auto-collapses sidebar on mobile)
  • Fixed page title Markdown Viewer, offline favicon + PWA (manifest.json, sw.js app-shell cache mdviewer-v2; /docs/* and /search-index.json always hit network)
  • Responsive desktop + mobile (drawer sidebar ≤768 px, safe-area insets, 28–44 px touch targets, horizontally scrollable tables/code)
  • File-route loading is directory-verified: the target directory JSON is fetched first and the real filename from the listing wins, so a stale search index cannot 404 a file that exists
  • GET/HEAD only, /docs/ read-only

⁠Layout

Browser --GET /---------> nginx (/usr/share/nginx/html)
        --GET /docs/----> autoindex JSON -> frontend file tree
        --GET /docs/x.md> static md -> marked render
        --GET /search-index.json -> static search index (per deployment!)
/docs <- runtime volume (local bind or nfs ro, pick one)

Only one docs source at a time, selected by DOCS_SOURCE. scripts/compose.sh forces COMPOSE_FILE from DOCS_SOURCE so volumes cannot be mixed by a stale .env.

Each deployment (production, staging) must have its own web/ root and its own search-index.json built from its own docs. Sharing one index across different docs roots makes search hits 404.

⁠Project tree

.
├── Dockerfile                # builds nginx + web image (multi-arch ready)
├── docker-compose.yml        # shared service (nginx + web + port)
├── docker-compose.local.yml  # local directory source
├── docker-compose.nfs.yml    # NFS volume source
├── .env.example              # config template (committed)
├── .env                      # local runtime config (gitignored)
├── nginx/default.conf        # read-only + autoindex json + .md MIME + index route
├── web/
│   ├── index.html
│   ├── app.js                # routing/search/toc/lightbox/math/pwa/recent
│   ├── style.css             # light/dark themes + responsive + touch
│   ├── favicon.png / apple-touch-icon.png / icon-192.png / icon-512.png
│   ├── manifest.json / sw.js # PWA (cache version mdviewer-v2)
│   └── vendor/               # 3rd-party static (gitignored, fetch it)
└── scripts/
    ├── compose.sh            # docker compose wrapper by DOCS_SOURCE
    ├── fetch-vendor.sh       # download vendor/*.js,*.css (incl. dark + katex)
    ├── build-index.py        # build search-index.json from a docs dir
    └── screenshot-demo.sh    # optional screenshots (needs playwright)

⁠Quick start

Requires Docker (compose plugin) and curl. NFS mode additionally needs nfs-common (Debian/Ubuntu) or nfs-utils (RHEL) on the host.

cp .env.example .env
# edit .env: pick DOCS_SOURCE=local|nfs, set paths below
sh scripts/fetch-vendor.sh
python3 scripts/build-index.py /path/to/docs web/search-index.json
sh scripts/compose.sh config
sh scripts/compose.sh up -d
curl -s http://127.0.0.1:8080/ | head

Open http://<host>:<port>/ in a browser.

⁠Docker image

The midway-v2 image below is multi-arch (linux/amd64, linux/arm64):

sh scripts/fetch-vendor.sh
python3 scripts/build-index.py /path/to/docs web/search-index.json
docker buildx build --platform linux/amd64,linux/arm64 \
  -t mooloco/mdviewer:midway-v2 --push .
docker pull mooloco/mdviewer:midway-v2
docker run -d --name mdweb -p 8080:80 \
  -v /path/to/your/docs:/usr/share/nginx/html/docs:ro \
  mooloco/mdviewer:midway-v2

/docs inside the image is an empty placeholder. Mount any directory of .md files read-only and it is served immediately. Rebuild search-index.json from the mounted docs if you use sidebar search.

⁠Configuration (.env)

VariableMeaningDefault
MDWEB_PORThost-side listen port8080
DOCS_SOURCEdocs source, local or nfs onlynfs
COMPOSE_FILEforced by compose.sh from DOCS_SOURCE, manual edit ignored—
LOCAL_DOCS_PATHlocal mode host absolute path/path/to/docs
NFS_SERVERNFS server host/IPnfs.example.com
NFS_EXPORTNFS export path (no colon; compose prepends :)/export/docs
NFS_VERSIONNFS version4.1
NFS_MOUNT_OPTIONSmount optionsro,hard,timeo=600

Compose assembles the NFS options as (example values):

addr=<NFS_SERVER>,nfsvers=<NFS_VERSION>,proto=tcp,port=2049,<NFS_MOUNT_OPTIONS>,retrans=3
device: :<NFS_EXPORT> -> /usr/share/nginx/html/docs (ro)

Change port:

# .env
MDWEB_PORT=8081
sh scripts/compose.sh up -d

⁠Search index

Sidebar search reads /search-index.json (static, built offline):

python3 scripts/build-index.py /path/to/docs web/search-index.json
# output: {"generated_at": "...", "files": [{"path": "/docs/a/b.md", "title": "...", "snippet": "..."}]}

Only .md files, dotfiles skipped, title = first H1 else filename, snippet = Markdown-noise-stripped prefix (600 chars). Rebuild after docs change (cron-friendly). Missing index degrades gracefully to directory browsing with a notice.

⁠Staging vs production

Both containers serve the same frontend code but must not share web/:

/opt/mdweb/web          -> mdweb:8080          -> NFS docs + NFS index
/opt/mdweb-staging/web  -> mdweb-staging:8089  -> demo docs + demo index

Workflow: scp changed frontend files to staging first, verify on :8089, then sync to production :8080. Rebuild each side's search-index.json from its own docs root.

⁠NFS notes

Server side (example), allow TCP 2049 for the viewer host:

# /etc/exports example:
# /export/docs 192.168.0.0/24(ro,sync,no_subtree_check)
exportfs -rav

Bare-metal mount test on the viewer host:

mount -t nfs -o vers=4.1,proto=tcp,port=2049,ro,hard,timeo=600,retrans=3 \
  <NFS_SERVER>:<NFS_EXPORT> /mnt/nfs_test \
  && ls /mnt/nfs_test | head && umount /mnt/nfs_test

⁠Read-only

  • /usr/share/nginx/html/docs is mounted ro; touch inside reports Read-only file system, which is expected
  • nginx/default.conf denies everything except GET/HEAD
  • .md is served as text/markdown, /docs/ and /search-index.json are no-cache so source edits appear immediately

⁠Troubleshooting

SymptomCheck
mount: connection refused/timed outserver export + 2049/tcp firewall, exportfs -v
wrong fs type / bad optionmissing nfs-common/nfs-utils on viewer host
403 / empty listweb/vendor fetched? docker exec mdweb ls -R /usr/share/nginx/html/docs
search hit 404sindex built from a different docs root; rebuild per deployment (see above)
port unreachabless -tlnp | grep <port>, change MDWEB_PORT then up -d
stale frontend / blank pagehard refresh (Ctrl+F5); SW cache versi

Tag summary

Content type

Image

Digest

sha256:6e4ceb2a3…

Size

41.2 MB

Last updated

15 days ago

docker pull mooloco/mdviewer:midway-v2.2