mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 15:24:32 +00:00
286 lines
17 KiB
Markdown
286 lines
17 KiB
Markdown
# 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
|
||
|
||
Only two branches exist in this repository: **`main`** and **`development`**. No other branches should be created or left alive.
|
||
|
||
- **`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. Always commit to `development` — never create a new branch per conversation or feature.
|
||
- **Documentation only** (`CLAUDE.md`, `docs/`) — may be pushed directly to `main`.
|
||
- **If a non-canonical branch is created** (e.g. for a large staged feature), it must be deleted immediately after it is merged. The repo returns to `main` + `development` only.
|
||
|
||
In practice: check out `development`, do the work, push to `development`, PR `development` → `main` when ready to release.
|
||
|
||
**When a branch isn't visible locally:** always run `git fetch origin <branch-name>` before concluding a branch doesn't exist. Never create a new branch if the user names one — fetch it from the remote first.
|
||
|
||
## Unreleased changelog
|
||
|
||
`docs/unreleased.md` is a living document that tracks every fix or feature on `development` that has not yet been merged to `main`. Keep it current: whenever a change lands on `development`, add or update an entry in `unreleased.md` in the same commit (or a follow-up commit to `development`). When a batch of changes is merged to `main` and released, clear the entries that were released and leave the file in place for the next round of work.
|
||
|
||
## 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 → patch `<title>` in `index.html` with `sitename` → generate PWA files if enabled. The `<title>` patch ensures crawlers and link-preview scrapers (WhatsApp, Slack, etc.) see the correct site name in the static HTML before any JavaScript runs.
|
||
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`
|
||
- Line height is set per category via `line-height: 2.8` (useful for scripts like Nastaliq that need extra vertical space). Restores to theme default when switching to a category without this key.
|
||
|
||
## 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.
|
||
|
||
**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`.
|
||
|
||
**Nav section toggle icons** (top-level keys, not inside `light:`/`dark:`):
|
||
|
||
| Key | Default | Purpose |
|
||
|---|---|---|
|
||
| `nav-section-expand-icon` | `arrow_right` | icon shown when section is collapsed |
|
||
| `nav-section-collapse-icon` | `arrow_drop_down` | icon shown when section is expanded |
|
||
|
||
Available icon names: `arrow_right`, `arrow_drop_down`, `keyboard_arrow_right`, `keyboard_arrow_down`, `keyboard_double_arrow_right`, `keyboard_double_arrow_down`, `expand_content`, `collapse_content`, `add`, `minimize`.
|
||
|
||
These only apply to nav sections with `pagesvisibility: hidden` (collapsible sections).
|
||
|
||
### 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.
|