mdcms/CLAUDE.md
Claude d72a7c0d7f
Update CLAUDE.md for v0.4: branching convention, renderer features, known limitations
- Branching: main=release, development=default dev branch, no per-conversation branches
- Add v0.4 renderer features section (callouts, TOC, theme, icons, PWA, fetch-deps)
- Note topbar nav is broken — always use sidebar
- Update version markers to v0.4 throughout
- Add note that tag pushes must be done locally (cloud env blocks them)

https://claude.ai/code/session_01UP8Wo2CKPNhvvTkzX48CWF
2026-05-18 07:09:07 +00:00

240 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `development``main` 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`.
```yaml
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 helpers**`read_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 reading**`read_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 system**`identify_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 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.
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`:
```yaml
---
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:
````markdown
```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.
### 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.