| name | static-markdown-site |
| description | Build a minimal static site to display Markdown files โ collapsible grouped sidebar, auto-sync from source directories, static HTML build, and GitHub deployment |
Static Markdown Site
Build a zero-framework static site that renders Markdown content with a collapsible grouped sidebar. Supports auto-syncing .md files from any source directory, static HTML build, and one-command GitHub deployment.
When to Use
- You have a directory of
.md files (reports, docs, notes) you want to browse as a website
- You want grouped sidebar navigation with collapsible sections
- You need a one-command workflow: sync โ build โ deploy
Project Structure
md-site/
โโโ index.html # entry point (loads marked.js from CDN)
โโโ build.py # static HTML build (supports grouped manifest)
โโโ sync-reports.sh # one-command: sync + build + git push
โโโ assets/
โ โโโ style.css # theme with CSS variables
โ โโโ app.js # markdown loader + grouped sidebar logic
โโโ content/
โ โโโ manifest.json # grouped: [{group, pages: [{file, title}]}]
โ โโโ ... # synced .md files (preserves subdirectory structure)
โโโ docs/ # static build output (use docs/ for GitHub Pages, dist/ otherwise)
Key Design Decisions
Grouped Manifest Format
The manifest supports two formats:
Flat (simple sites):
[{ "file": "hello.md", "title": "Hello" }]
Grouped (auto-synced sites with directory structure):
[
{ "group": "Getting Started", "pages": [{ "file": "hello.md", "title": "Hello" }] },
{ "group": "Daily", "pages": [{ "file": "daily/2026-04-15.md", "title": "2026-04-15" }] }
]
Both app.js and build.py detect the format automatically.
Collapsible Sidebar Groups
- Click group label to expand/collapse (animated with
max-height transition)
- State persisted in
localStorage key sidebar-groups
- Visual indicator:
โพ arrow rotates 90ยฐ when collapsed
- Global sidebar collapse:
Ctrl+B or < button, state in sidebar-collapsed
CSS Variables for Easy Theming
:root {
--sidebar-width: 260px;
--sidebar-gap: 100px;
--accent: #0969da;
--bg-sidebar: #f8f9fb;
--radius: 8px;
}
Sync Script Pattern
sync-reports.sh does three things in sequence:
- Copy .md files โ
find + cp, only if changed (diff -q check)
- Generate manifest โ Python heredoc walks
content/ directory, groups by path, skips demo pages
- Build + Deploy โ
python3 build.py, then git add -A && commit && push
Filtering Source Directories
To sync only specific directories and ignore certain files, define arrays at the top of the script:
SYNC_DIRS=("daily" "us-stock/daily" "weekly")
IGNORE_FILES=("readme.md" "hello.md")
for dir in "${SYNC_DIRS[@]}"; do
rm -rf "$CONTENT_DIR/$dir"
done
for dir in "${SYNC_DIRS[@]}"; do
[ -d "$dir" ] || continue
find "$dir" -name "*.md" -type f | while read -r src; do
base=$(basename "$src" | tr '[:upper:]' '[:lower:]')
skip=false
for ign in "${IGNORE_FILES[@]}"; do
[ "$base" = "$ign" ] && skip=true && break
done
$skip && continue
done
done
Pass the dirs to the Python manifest generator so it only walks those subdirectories:
python3 - "$CONTENT_DIR" "${SYNC_DIRS[@]}" <<'PYEOF'
import os, sys
content_dir = sys.argv[1]
sync_dirs = sys.argv[2:]
for sdir in sync_dirs:
walk_root = os.path.join(content_dir, sdir)
PYEOF
Python Pitfalls
- Python heredoc variable passing: use
python3 - \"$VAR1\" \"$VAR2\" <<'PYEOF' with sys.argv โ do NOT use shell variable expansion inside heredoc (causes string indices must be integers errors)
- Token handling: read from
~/.hermes/.env via grep, embed in remote URL for passwordless push: https://user:${TOKEN}@github.com/user/repo.git
- The sync script replaces content โ demo pages (hello.md, guide.md) must be manually added to the manifest's first group or they get lost on next sync
GitHub Push with Embedded Token
TOKEN=$(grep "^GITHUB_TOKEN=" ~/.hermes/.env | head -1 | cut -d= -f2 | tr -d '\n\r')
git remote add origin "https://user:${TOKEN}@github.com/user/repo.git"
git push -u origin main
build.py Key Logic
load_manifest() โ reads manifest.json, auto-detects grouped vs flat format
flat_pages(groups) โ flattens groups into single page list
- Builds each page with grouped sidebar nav HTML
- Creates subdirectory structure in
dist/ matching source paths
Deploy
Static site โ deploy dist/ or root to GitHub Pages, Netlify, Vercel, or any web server.
GitHub Pages Path Restriction
GitHub Pages API only accepts / or /docs as source paths โ /dist is NOT valid. If deploying via API:
curl -X POST .../pages -d '{"source":{"branch":"main","path":"/dist"}}'
curl -X POST .../pages -d '{"source":{"branch":"main","path":"/docs"}}'
So if using GitHub Pages, output builds to docs/ instead of dist/. Update build.py:
DIST = ROOT / "docs"
Then enable Pages:
curl -s -X POST \
-H "Authorization: token $TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/$OWNER/$REPO/pages \
-d '{"build_type":"legacy","source":{"branch":"main","path":"/docs"}}'