# 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. |

**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.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`:

```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
date: 2025-01-01
datetime: 2025-01-01 13:00   # use this for posts (not `date` alone — see known limitations)
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-datetime-reversechronological
limit: 10
paginate: yes
```
````

Reliable tags (others are known-broken): `posts-datetime-chronological-byyearmonth`, `posts-datetime-reversechronological`. Use `datetime` frontmatter (format: `YYYY-MM-DD HH:MM`) for posts — `date` alone does not work reliably.

## 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.
