- Add _http_get() general HTTP helper (SSL via certifi, 30s timeout) - Add CDN_DEPS table: 6 jsDelivr assets (js-yaml, marked, fuse.js, highlight.js, 2x highlight CSS) - Add _fetch_bunny_fonts(): reads theme.yml font-body/heading/code keys, fetches CSS from fonts.bunny.net, downloads woff2 files to assets/fonts/, rewrites CSS to use relative local paths, writes per-font CSS file - Add _patch_index_html(): replaces CDN URLs with local vendor paths, injects <link data-mdcms-fonts> tags for locally downloaded fonts - Add fetch-deps CLI command: downloads vendors, fetches fonts if theme.yml present, patches index.html — site makes no external network requests - index.html loadFonts(): skip if data-mdcms-fonts link already present (set by patched index.html after fetch-deps) - Update CLAUDE.md CLI command table with fetch-deps entries https://claude.ai/code/session_015XtsgTMi8UtmgxEgb5Qt2c
10 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Versioning rule
Every merge into main is a release. Before committing any change to mdcms.py, ask: "Is this intended to be merged to main immediately?" If yes, bump CLI_VERSION and CLI_RELEASE_DATE in mdcms.py and version in pyproject.toml before committing. If the work is exploratory or not yet ready to merge, leave the version unchanged and ask again when the merge is imminent.
Branching convention
- Code changes (
mdcms.py,pyproject.toml,app/,.github/) — use branchmdcms_claude(create frommainif it doesn't exist), PR tomain. - Documentation only (
CLAUDE.md,docs/) — push directly tomain.
What this project is
MD-CMS is a markdown-based static site system with two distinct parts:
mdcms.py— a Python 3 CLI tool (click+PyYAML+certifi). Manages a registry of sites, scans content, generatesnav.ymlandsearch.json, and is designed for both local use and GitHub Actions pipelines.app/index.html— a single-file browser renderer that reads markdown, config, and nav at runtime entirely client-side. There is no build pipeline, no compilation, no server.
The app/ folder is the deployable artifact and the starter template downloaded when registering a new site. mdcms.py lives outside it.
Repository layout
mdcms.py ← CLI tool
pyproject.toml ← packaging (entry point, dependencies)
app/
index.html ← renderer + v0.3 version marker
config.yml ← starter config + v0.3 version marker
nav.yml ← generated
search.json ← generated
pages/
posts/
assets/
docs/
banner/
documentation.md
knownlimitations.md
quickstart.md
install.md
release.md
.github/workflows/release.yml ← cross-platform release builds
samplesite/ ← reference implementation (not deployed)
CLI commands
Install: pip install mdcms / pipx install mdcms — or use the standalone binary from a GitHub release.
During development, run directly: python3 mdcms.py <command>
| Command | Description |
|---|---|
mdcms register <name> [path] |
Register a site. Downloads starter template from GitHub if no mdcms site is found at the path. Defaults to current directory. |
mdcms delete <name> |
Remove a site from the registry. Does not delete files. Prompts for confirmation. |
mdcms view |
List all registered sites with version and status. |
mdcms view <name> |
Show details: path, version, sitename, pages/posts count, sections, categories. |
mdcms build <name> |
Build nav.yml and search.json for a registered site. |
mdcms build --path <path> |
Build using an explicit path — no registry needed. Intended for CI/CD. |
mdcms build |
Build using current working directory. Simplest form for GitHub Actions. |
mdcms fetch-deps [name] |
Download all external JS/CSS deps to assets/required/vendors/ and Bunny Fonts to assets/fonts/. Patches index.html to use local paths — no CDN requests after this. |
mdcms fetch-deps --path <path> |
Same, using an explicit path. |
Local preview: Run python3 -m http.server 8800 in the site directory and open http://localhost:8800. Do not open index.html directly — browsers block local file access due to CORS.
Architecture of mdcms.py
Single-module Python script. Logical layers in order:
- Version helpers —
read_site_version()reads themdcms v0.3marker from the first line ofconfig.yml.version_status()classifies sites asok,outdated,newer, orunsupportedagainstMIN_SUPPORTED_VERSION. - Registry —
~/.config/mdcms/sites.jsonstores{name: {path, version}}.load_registry()/save_registry()/resolve_site_path(). - Config reading —
read_config()readsconfig.ymlwithyaml.safe_load().get_category_info()extracts category settings from the parsed dict. - Frontmatter parser (
parse_frontmatter) — reads---YAML blocks usingyaml.safe_load(). Returns(meta_dict, body_text). - Category system —
identify_variant()splits.mdpaths into(base, category_code). A suffix is only treated as a category code if it appears in the declared code list. - Scanner (
scan_and_categorize) — walks a directory, skips drafts, returns records with the first 5000 chars of body for search indexing. Paths are relative tosite_root. - Nav/search generators —
generate_nav_yml()emits a fixed-format YAML subset.generate_search_json()emits a JSON array.merge_sections()preserves existing section metadata on rebuild. - Core build (
run_build) — orchestrates the full build: version check → config read → scan → merge → write nav.yml and search.json. - Template download (
download_template) — fetchesapp/from GitHub via the Contents API usingurllib+certififor SSL. Recursively downloads files and directories. - CLI commands (
register,delete,view,build) — implemented withclick. Entry point:main()→cli().
Version markers
Every mdcms site has a version marker on the first line of two files:
config.ymlline 1:# mdcms v0.3 | DO NOT REMOVE THIS COMMENTindex.htmlline 1:<!-- mdcms v0.3 | DO NOT REMOVE THIS COMMENT -->
register and build both read the marker from config.yml to detect and validate the site. Sites with no marker are not recognised as mdcms sites. Sites below MIN_SUPPORTED_VERSION are rejected.
There are two distinct version numbers:
- CLI version (
CLI_VERSIONinmdcms.py,versioninpyproject.toml) — bumped with every release. - Site format version (markers in
config.ymlandindex.html) — only bumped when the site file format has a breaking change. Many CLI releases may share the same site format version.
Site structure
The registered path points directly to the directory containing index.html (the site root). There is no website/ subdirectory.
<site-root>/
index.html ← renderer
config.yml ← required: sitename, navigation; rest optional
nav.yml ← generated; manual edits to section metadata are preserved
search.json ← generated
pages/
home.md ← default landing page
about.md
about.nb.md ← Norwegian variant (category suffix = nb)
posts/
2025-01-01-my-first-post.md
assets/
fonts/
images/
Page frontmatter fields
All optional except title:
---
title: Page Title
sort: 100 # controls nav ordering (lower = higher)
section-id: blog # assigns page to a nav section
draft: true # exclude from nav and search
author: Name
created: 2025-01-01 13:00
modified: 2025-01-15 09:00
keywords: foo, bar
description: Short description for search
language: en
---
nav.yml structure
Sections and pages are separate lists. mdcms.py preserves manual edits to section fields (defaultname, sort, parent, parent-sort, pagesvisibility, categorynames) on each rebuild. New sections are auto-created from section-id values found in frontmatter.
pagesvisibility can be visible, hidden, or draft (draft excludes pages from search.json).
For nested navigation, set parent: <parent-section-code> and parent-sort on a section.
Category system
categories-use: yesinconfig.ymlenables categoriesdefault-category.codeis required when categories are enabled- Variant files:
<base>.<code>.md— the suffix is only treated as a category if the code is declared in config categories-sectionnames: per-categoryrequires each section innav.ymlto have acategorynamesblock with an entry per category code- RTL is set per category via
direction: rtl
Dynamic post tags (mdcms code blocks)
Embed post lists in pages using fenced blocks:
```mdcms
posts-created-reversechronological
limit: 10
paginate: yes
```
Reliable tags (others are known-broken): posts-created-chronological-byyearmonth, posts-created-reversechronological. Use created frontmatter (format: YYYY-MM-DD HH:MM) for posts.
Release workflow
.github/workflows/release.yml triggers on version tags (v*). Uses a matrix of three runners:
| Runner | Output |
|---|---|
ubuntu-latest |
mdcms-linux-amd64 binary + mdcms_<version>_amd64.deb (via PyInstaller + fpm) |
macos-latest |
mdcms-macos-arm64 binary |
windows-latest |
mdcms-windows-amd64.exe |
All artifacts are attached to the GitHub release using gh release create. The workflow sets FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true to opt into the Node.js 24 runner ahead of the June 2026 forced migration.
Release checklist — before tagging:
- Update
CLI_VERSIONinmdcms.py - Update
versioninpyproject.toml - Update site format markers in
app/config.ymlandapp/index.htmlonly if the site format changed
Then: git tag v0.3.1 && git push origin v0.3.1
Known limitations
- Most
posts-*tag variants are broken. Onlyposts-datetime-chronological-byyearmonthandposts-datetime-reversechronologicalreliably work. - Section headings in the nav are non-clickable (sections-sitemap is not yet implemented).
Key implementation details
generate_nav_yml()emits a fixed-format YAML subset. It is not a general YAML emitter — do not assume it handles arbitrary structures.yaml.safe_load()is used for all YAML reading (config.yml, nav.yml, frontmatter). The nav.yml parser depends on PyYAML, not a hand-rolled parser.- Category code validation uses
CATEGORY_CODE_RE = re.compile(r"^[a-zA-Z0-9\-]+$")— codes must match this. scan_and_categorize()takes bothdirectoryandsite_root— paths in records are always relative tosite_root.- The
samplesite/directory is a reference implementation with multi-language categories (English, Norwegian, Arabic including RTL). It is not deployed; it exists for reference and testing. - Template download uses
urllib(stdlib) withcertififor SSL certificate verification — required for PyInstaller binaries on Linux/macOS where the bundled Python cannot find system CA certificates.