mdcms/CLAUDE.md
Claude f4a41ed3ae
Add nav-sitename, nav-description, nav-toggle colour keys
Separates sidebar header colours from nav link colours. The three new keys
control the site name, site description, and dark/light toggle independently,
each cascading from the nearest nav variable (nav-sitename → nav-link,
nav-description and nav-toggle → nav-section-heading) so existing themes
are unaffected.

Enables Claude Design to fine-tune sidebar header legibility on saturated or
bold nav backgrounds without having to override the nav link colours, and vice
versa. CSS selectors and applyThemeYml() updated; app/theme.yml, CLAUDE.md,
and docs/claude-design.md updated with full key reference and pattern examples.

https://claude.ai/code/session_01NQKywehSj8Ku4yKhwB4VNB
2026-05-18 14:47:28 +00:00

15 KiB
Raw Blame History

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

  • main is the release branch. Every merge to main is a release. Never commit work-in-progress directly to main.
  • development is the default branch for all development, including all Claude-driven work. Create it from main if it doesn't exist. Do not create a new branch per conversation.
  • Phased branches (claude/<feature>) are allowed when a large feature needs staged review, but the final merge target is always main via development.
  • Documentation only (CLAUDE.md, docs/) — may be pushed directly to main.

In practice: check out development, do the work, push to development, PR developmentmain when ready to release.

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.4 version marker
  config.yml                    ← starter config + v0.4 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.

PWA config keys

Set in config.yml. mdcms build generates manifest.json and service-worker.js when pwa: yes.

pwa: yes
pwa-name: "My Documentation"   # mandatory if pwa: yes
pwa-shortname: "MyDocs"        # optional short name for home screen
pwa-colour: "#2563EB"          # optional browser chrome colour
offline-message:
  en: "You are offline and some content is unavailable."
  nb: "Du er frakoblet og noe innhold er utilgjengelig."

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.4 | DO NOT REMOVE THIS COMMENT
  • index.html line 1: <!-- mdcms v0.4 | DO NOT REMOVE THIS COMMENT -->
  • theme.yml line 1: # mdcms v0.4 | 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.4.1 && git push origin v0.4.1

Note: Git tag pushes must be done from a local machine — the cloud environment cannot push tags (HTTP 403). Use gh release create <tag> locally after pushing the tag.

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).
  • navigation: topbar is broken. Always use navigation: sidebar in config.yml for any test sites or starter templates.

v0.4 renderer features (index.html)

Features added in v0.4, all rendered client-side in app/index.html:

Callout tags

Fenced mdcms blocks with callout-info, callout-warning, callout-success, callout-error. Each has a coloured left border, low-opacity tinted background, optional icon + title row, and full markdown body. The JS sets --callout-primary and --callout-bg CSS variables on the container; the CSS must reference these (not hardcoded colours). Config-defined messages: message: <key> resolves title and body from the callouts: block in config.yml.

Table of contents tag

Fenced mdcms block with toc. Renders a section-grouped list of all visible, non-draft pages in the active category, excluding the TOC page itself. Groups by nav section.

Theme system (theme.yml)

Presentational config separate from config.yml. Controls accent colour, dark/light mode palette, fonts, and layout. index.html loads it at runtime.

Colour keys per mode (light: and dark: blocks):

Key CSS variable Default
accent --accent #2563EB / #60A5FA
background --bg-main #FFFFFF / #0F172A
nav-background --bg-nav #F8FAFC / #1E293B
text --font-colour #1E293B / #F1F5F9
text-muted --font-colour-muted #64748B / #94A3B8
nav-link --nav-link-colour falls back to text
nav-link-active --nav-link-active-colour falls back to accent
nav-section-heading --nav-section-heading-colour falls back to text-muted
nav-sitename --nav-sitename-colour falls back to nav-link
nav-description --nav-description-colour falls back to nav-section-heading
nav-toggle --nav-toggle-colour falls back to nav-section-heading
divider --divider color-mix(in srgb, background 85%, text)

When to use nav-link keys: When nav-background matches or is very close to accent, the default behaviour (active link coloured with accent) makes links invisible. Set nav-link, nav-link-active, and nav-section-heading explicitly so all three are legible against nav-background. Example: a red nav background needs white (#FFFFFF) for all three nav colour keys.

Semantic colours:

  • colours-semantic — applies to both light and dark modes. Use for colours that read on both backgrounds, or when you don't need per-mode control.
  • colours-semantic-dark — overrides semantic colours in dark mode only. Use lighter/more saturated variants here so callout borders and tinted backgrounds remain legible on dark page backgrounds.

Keys in both blocks: info, warning, success, error.

Icon system

All UI icons served as local SVGs from app/assets/icons/. No Google Fonts or external icon font. Icon names are normalised (lowercase, spaces → hyphens).

PWA

manifest.json and service-worker.js generated by mdcms build when pwa: yes. Cache-first SW precaches all pages, posts, and assets. Offline message from config.yml (offline-message key) stored in localStorage and shown when a page can't be fetched. Requires a favicon.png in assets/images/ for the install icon (192×192 recommended).

fetch-deps offline mode

mdcms fetch-deps downloads all CDN JS/CSS to assets/required/vendors/ and Bunny Fonts to assets/fonts/, then patches index.html CDN URLs to local paths. After this, the site makes no external network requests.

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.