mdcms/CLAUDE.md
Claude 9b7639cc62
v0.4 Phase 6: fetch-deps command for offline/local dependency mode
- 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
2026-05-17 19:06:31 +00:00

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 branch mdcms_claude (create from main if it doesn't exist), PR to main.
  • Documentation only (CLAUDE.md, docs/) — push directly to main.

What this project is

MD-CMS is a markdown-based static site system with two distinct parts:

  1. mdcms.py — a Python 3 CLI tool (click + PyYAML + certifi). Manages a registry of sites, scans content, generates nav.yml and search.json, and is designed for both local use and GitHub Actions pipelines.
  2. 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:

  1. Version helpersread_site_version() reads the mdcms v0.3 marker from the first line of config.yml. version_status() classifies sites as ok, outdated, newer, or unsupported against MIN_SUPPORTED_VERSION.
  2. Registry~/.config/mdcms/sites.json stores {name: {path, version}}. load_registry() / save_registry() / resolve_site_path().
  3. Config readingread_config() reads config.yml with yaml.safe_load(). get_category_info() extracts category settings from the parsed dict.
  4. Frontmatter parser (parse_frontmatter) — reads --- YAML blocks using yaml.safe_load(). Returns (meta_dict, body_text).
  5. Category systemidentify_variant() splits .md paths into (base, category_code). A suffix is only treated as a category code if it appears in the declared code list.
  6. Scanner (scan_and_categorize) — walks a directory, skips drafts, returns records with the first 5000 chars of body for search indexing. Paths are relative to site_root.
  7. Nav/search generatorsgenerate_nav_yml() emits a fixed-format YAML subset. generate_search_json() emits a JSON array. merge_sections() preserves existing section metadata on rebuild.
  8. Core build (run_build) — orchestrates the full build: version check → config read → scan → merge → write nav.yml and search.json.
  9. Template download (download_template) — fetches app/ from GitHub via the Contents API using urllib + certifi for SSL. Recursively downloads files and directories.
  10. CLI commands (register, delete, view, build) — implemented with click. Entry point: main()cli().

Version markers

Every mdcms site has a version marker on the first line of two files:

  • config.yml line 1: # mdcms v0.3 | DO NOT REMOVE THIS COMMENT
  • index.html line 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_VERSION in mdcms.py, version in pyproject.toml) — bumped with every release.
  • Site format version (markers in config.yml and index.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: yes in config.yml enables categories
  • default-category.code is 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-category requires each section in nav.yml to have a categorynames block 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:

  1. Update CLI_VERSION in mdcms.py
  2. Update version in pyproject.toml
  3. Update site format markers in app/config.yml and app/index.html only 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. Only posts-datetime-chronological-byyearmonth and posts-datetime-reversechronological reliably 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 both directory and site_root — paths in records are always relative to site_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) with certifi for SSL certificate verification — required for PyInstaller binaries on Linux/macOS where the bundled Python cannot find system CA certificates.