# Unreleased changes Changes merged into `development` that have not yet been released to `main`. --- ## Tabs & Accordions (`app/index.html`) Four new `mdcms` fenced-block types for rich content layout. All four variants read from the active theme automatically — no new config keys, no per-theme overrides needed. ### Block types | Language tag | Alias for | Renders as | |---|---|---| | `tab-underline` | — | Tab strip, active tab marked with underline | | `tab` | `tab-underline` | (same) | | `tab-filled` | — | Tab strip, tabs as filled chips | | `accordion-underline` | — | Stacked accordion, header underline style | | `accordion` | `accordion-underline` | (same) | | `accordion-filled` | — | Stacked accordion, filled card style | ### Authoring syntax Open a fenced block with the language tag `mdcms `. The body is YAML with a single top-level key `items:`, whose value is a list of item objects. ~~~markdown ```mdcms tab-underline items: - title: Install default: selected content: | Install with `npm i mdcms` or `pnpm add mdcms`. - title: Configure content: | Drop a `mdcms.config.yaml` next to your content folder. - title: Deploy content: | Any static host. The build emits plain HTML. ``` ~~~ ### Per-item keys | Key | Required | Type | Notes | |---|---|---|---| | `title` | yes | plain string | Label shown on the tab button or accordion header. Plain text only — no Markdown. | | `content` | yes | Markdown block | Body content. Use the YAML literal block scalar (`\|`) for multi-line Markdown. Rendered with the same pipeline as the surrounding page (GFM, syntax highlighting, internal links). | | `default` | no | string | **Tabs:** `selected` marks the tab that is open on load; if no item has `selected`, the first item is used. `notselected` (or omitting the key) leaves the tab inactive. Exactly one tab should be `selected`. **Accordions:** `open` makes the item expanded on load; `closed` (or omitting) leaves it collapsed. Any number of accordion items may be `open`. | | `title-style` | no | string | Heading level for screen readers and external TOC tools. One of `"#"`, `"##"`, `"###"`, `"####"`, `"#####"`, `"######"`, or `""` (default). Visual size is always fixed by the component — this only changes the underlying ARIA role and level. Use a value when you want the item to be picked up as a heading by assistive technology. | ### Examples **Tabs — underline (default)** ~~~markdown ```mdcms tab items: - title: npm default: selected content: | ```bash npm install mdcms ``` - title: pnpm content: | ```bash pnpm add mdcms ``` - title: yarn content: | ```bash yarn add mdcms ``` ``` ~~~ **Tabs — filled chips** ~~~markdown ```mdcms tab-filled items: - title: Overview default: selected content: | MD-CMS is a markdown-based static site system with no build step. - title: Features content: | - Sidebar navigation - Full-text search - PWA + offline support - Dark / light theme ``` ~~~ **Accordion — underline (default)** ~~~markdown ```mdcms accordion items: - title: What is MD-CMS? default: open content: | A single-file browser renderer. No build pipeline, no compilation, no server required. - title: How do I install it? content: | Run `pip install mdcms` or download a binary from the GitHub releases page. - title: Does it work offline? content: | Yes — run `mdcms fetch-deps` to bundle vendor assets locally, then enable `pwa: yes` in `config.yml` for full offline support. ``` ~~~ **Accordion — filled cards** ~~~markdown ```mdcms accordion-filled items: - title: Can I use custom themes? default: open content: | Yes. Create a `theme.yml` and reference it with `theme: theme.yml` in `config.yml`. The theme controls colours, fonts, and layout. - title: title-style example title-style: "##" content: | This header is announced as an `

` to screen readers, even though its visual size is set by the accordion component. ``` ~~~ ### How the appearance adapts to themes The components derive their fill colours and bar/border colours from the active theme at runtime. No new keys in `config.yml` or `theme.yml` are needed. **Bold themes** (nav background is visually distinct from the page — e.g. a dark sidebar on a light page, or a coloured nav like red or navy): filled tabs and accordion headers use the nav background colour as their fill; the bar/border uses the nav colour. This makes the components look like an extension of the sidebar chrome. **Subtle themes** (nav background is almost identical to the page — e.g. both near-white or both near-dark): filled tabs use a light tint of the accent colour; the bar and border use the accent colour directly. This keeps the components visible without a strong nav background to borrow from. The switch between bold and subtle is automatic. The algorithm uses HSL chroma (`S × (1−|2L−1|)`) rather than raw HSL saturation, which would give false "bold" readings for near-white or near-black nav backgrounds. --- ## `mdcms build` patches `` with sitename `mdcms build` now rewrites the `<title>` tag in `index.html` with the value of `sitename` from `config.yml`. Previously the tag was hardcoded (`MD-CMS`) in older templates, or blank in the starter template, so link previews in WhatsApp, Slack, and other crawlers that read static HTML showed the wrong name. --- ## Untranslated posts now visible in all categories **Status:** On `development`, pending release. ### What was broken When the category system is enabled, a post file without a category suffix (e.g. `posts/my-post.md`) was silently assigned to the default category only. Switching to any other category caused those posts to disappear from the nav and from `posts-*` tag listings — even though no translated version existed. If you wrote posts without a language suffix, they simply vanished the moment a visitor switched category. Pages without a category suffix are unaffected: they continue to be assigned to the default category, which is the correct behaviour for pages. ### What it does now Posts without a category suffix are treated as uncategorised — meaning they appear in every category. A post called `my-post.md` now shows up regardless of which category is active. A post called `my-post.en.md` still appears only in the `en` category as before. Mixed situations work as expected: if you have both `my-post.md` and `my-post.nb.md`, the Norwegian variant is shown when the `nb` category is active, and the bare `my-post.md` is shown for every other category. ### What changes in the build output After rebuilding a site with `mdcms build`, affected post entries in `nav.yml` gain an `uncategorized: true` field: ```yaml - file: posts/my-post.md title: My Post sort: 100 uncategorized: true ``` In `search.json`, these entries carry `"category": null` instead of the default category code. This is what tells the renderer to include them universally. A rebuild is required for existing sites to pick up the change. --- ## Fix: category-variant pages fail to load on servers with SPA routing (e.g. Cloudflare Pages) When a site uses category-suffixed page files (e.g. `page.current.md`) and is hosted on a server configured with SPA fallback routing (serving `index.html` with HTTP 200 for any unknown path), the renderer's `fetchPageFile` mistook the HTML fallback for a found markdown file. It returned `index.html` content instead of falling through to try the `.current.md` variant. The page rendered the raw HTML of `index.html` as markdown, showing the `<title>` text (`sitename`) in the content area. `fetchPageFile` now checks the `Content-Type` response header and rejects any response with `text/html`, continuing to the next candidate URL instead. --- ## Fix: stale service worker not removed when `pwa: no` `index.html` unconditionally registers `service-worker.js` on every page load. When a site switched from `pwa: yes` to `pwa: no`, `mdcms build` stopped generating a new service worker, but the old one remained active in browsers that had visited the site before. The stale worker continued to serve cached responses from the old build. `mdcms build` now writes a self-unregistering `service-worker.js` when `pwa: no`. On the visitor's next page load, the browser installs this stub worker, which immediately unregisters itself and evicts any previously cached content. `manifest.json` is also removed if present. --- ## Fix: `config.yml` YAML parse errors now abort the build with a clear message A malformed `config.yml` (e.g. a stray tab character, which YAML forbids as a token starter) previously caused `read_config` to silently return an empty dict. The build would proceed with no config — categories disabled, no default category code — producing a broken `nav.yml` with wrong filenames and missing `variants` fields, so category-variant pages would not appear in the sidebar. `read_config` now raises `ClickException` on both `OSError` and `yaml.YAMLError`, aborting the build with a descriptive error message instead of continuing silently with an empty config.