diff --git a/app/index.html b/app/index.html
index 1cb851f..9c1b403 100644
--- a/app/index.html
+++ b/app/index.html
@@ -785,6 +785,39 @@ body {
.main-content { padding: 1rem 1rem 3rem; }
}
+/* ═══════════════════════════════════════════
+ TAG SYSTEM: CALLOUTS
+ ═══════════════════════════════════════════ */
+.mdcms-callout {
+ border-left: 4px solid;
+ border-radius: 0 6px 6px 0;
+ padding: 0.85rem 1rem 0.85rem 1rem;
+ margin: 1.25rem 0;
+}
+.mdcms-callout-title {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-weight: 700;
+ font-size: 0.95rem;
+ margin-bottom: 0.45rem;
+}
+.mdcms-callout-title .mdcms-icon { font-size: 1.1em; }
+.mdcms-callout-body { font-size: 0.95rem; }
+.mdcms-callout-body > *:first-child { margin-top: 0; }
+.mdcms-callout-body > *:last-child { margin-bottom: 0; }
+
+/* ═══════════════════════════════════════════
+ TAG SYSTEM: TABLE OF CONTENTS
+ ═══════════════════════════════════════════ */
+.mdcms-toc { margin: 1rem 0; }
+.mdcms-toc-section { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 0.4rem; color: var(--font-colour-muted); border-bottom: 1px solid var(--divider); padding-bottom: 0.25rem; }
+.mdcms-toc-list { list-style: none; padding: 0; margin: 0 0 0.5rem; }
+.mdcms-toc-list li { padding: 0.2rem 0; border-bottom: 1px solid var(--divider); }
+.mdcms-toc-list li:last-child { border-bottom: none; }
+.mdcms-toc-list a { color: var(--accent); text-decoration: none; font-size: 0.95rem; }
+.mdcms-toc-list a:hover { text-decoration: underline; }
+
/* ═══════════════════════════════════════════
TAG SYSTEM: POST LISTINGS
═══════════════════════════════════════════ */
@@ -862,32 +895,6 @@ body {
}
.post-load-more:hover { background: var(--nav-hover-bg); }
-/* ── Callout tags ──────────────────────────────────────── */
-.mdcms-callout {
- border-left: 4px solid var(--callout-primary, var(--accent));
- background: var(--callout-bg, transparent);
- border-radius: 0 0.4rem 0.4rem 0;
- padding: 0.75rem 1rem;
- margin: 1rem 0;
-}
-.mdcms-callout-title {
- display: flex;
- align-items: center;
- gap: 0.45rem;
- font-weight: 700;
- color: var(--callout-primary, var(--accent));
- margin-bottom: 0.4rem;
-}
-.mdcms-callout-title .mdcms-icon svg {
- fill: var(--callout-primary, var(--accent));
- width: 1.2em;
- height: 1.2em;
- display: block;
-}
-.mdcms-callout-body { margin: 0; }
-.mdcms-callout-body > :first-child { margin-top: 0; }
-.mdcms-callout-body > :last-child { margin-bottom: 0; }
-
@media print {
.sidebar, .topbar, .scroll-top, .hamburger,
.mobile-header, .theme-toggle, .search-container { display: none !important; }
@@ -1889,12 +1896,70 @@ function fmtDatetime(dtStr) {
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
}
+ function renderTocTag(container) {
+ const byCode = {};
+ navSections.forEach(s => { byCode[s.code] = s; });
+
+ const sortedSections = navSections
+ .filter(s => !isDraftSection(s.code, byCode))
+ .sort((a, b) => ((a.sort ?? 999) - (b.sort ?? 999)) || (a.code || '').localeCompare(b.code || ''));
+
+ const visiblePages = navData.filter(p => {
+ if (p.file === currentPage) return false;
+ if (!pageShouldDisplay(p)) return false;
+ const sid = p['section-id'];
+ if (sid && isDraftSection(sid, byCode)) return false;
+ return true;
+ });
+
+ const bySection = {};
+ const unsectioned = [];
+ visiblePages.forEach(p => {
+ const sid = p['section-id'] || null;
+ if (sid) { (bySection[sid] = bySection[sid] || []).push(p); }
+ else unsectioned.push(p);
+ });
+
+ function sortPages(pages) {
+ return [...pages].sort((a, b) =>
+ ((a.sort ?? 999) - (b.sort ?? 999)) || a.file.localeCompare(b.file));
+ }
+
+ function makeList(pages) {
+ const ul = document.createElement('ul');
+ ul.className = 'mdcms-toc-list';
+ pages.forEach(p => {
+ const a = el('a', { href: '#' + p.file, textContent: pageDisplayTitle(p) });
+ a.addEventListener('click', e => { e.preventDefault(); navigateTo(p.file); });
+ ul.appendChild(el('li', {}, a));
+ });
+ return ul;
+ }
+
+ const div = el('div', { className: 'mdcms-toc' });
+
+ if (unsectioned.length) div.appendChild(makeList(sortPages(unsectioned)));
+
+ sortedSections.forEach(section => {
+ const pages = bySection[section.code];
+ if (!pages || !pages.length) return;
+ div.appendChild(el('h3', { className: 'mdcms-toc-section', textContent: sectionDisplayName(section) }));
+ div.appendChild(makeList(sortPages(pages)));
+ });
+
+ if (!div.children.length) div.textContent = 'No pages found.';
+
+ container.replaceWith(div);
+ }
+
function hydrateMdcmsTags() {
document.querySelectorAll('.mdcms-tag').forEach(function(tagEl) {
try {
var cfg = JSON.parse(tagEl.getAttribute('data-config'));
if (/^callout-(info|warning|success|error)$/.test(cfg.tagName)) {
renderCalloutTag(tagEl, cfg);
+ } else if (cfg.tagName === 'toc') {
+ renderTocTag(tagEl);
} else {
renderPostTag(tagEl, cfg);
}
diff --git a/app/pages/home.md b/app/pages/home.md
index 3ed6b63..1fe5fbc 100644
--- a/app/pages/home.md
+++ b/app/pages/home.md
@@ -3,101 +3,24 @@ title: Home
sort: 100
---
-# Phase 4 — Callout Tags
+# Phase 5 — Table of Contents Tag
-Check each callout below. Each should show a coloured left border, an icon, a bold title in the accent colour, and a rendered body.
+The `toc` tag renders a section-grouped list of all pages visible for the active category. The TOC page itself is excluded.
---
-## Basic types
+## Basic TOC
-```mdcms callout-info
-title: Information
-This is an **info** callout. Supports *italic*, `code`, and lists:
-
-- Item one
-- Item two
-```
-
-```mdcms callout-warning
-title: Warning
-Something needs your attention. This is a **warning** callout.
-```
-
-```mdcms callout-success
-title: Success
-The operation completed successfully. This is a **success** callout.
-```
-
-```mdcms callout-error
-title: Error
-Something went wrong. This is an **error** callout.
+```mdcms
+toc
```
---
-## No title
+## What to verify
-```mdcms callout-info
-No title key here. The title row should not appear at all — just the body.
-```
-
----
-
-## Markdown body
-
-```mdcms callout-warning
-title: Rich body
-- List item one
-- List item two
-
-A paragraph with `inline code` and a [link](https://example.com).
-```
-
----
-
-## Custom icon override
-
-```mdcms callout-info
-title: Info with warning icon
-icon: warning
-This info callout uses the warning icon instead of the default info icon.
-```
-
----
-
-## Config-defined message (message: key)
-
-The callout below uses `message: aitranslation` to pull its title and body from the `callouts:` block in `config.yml`. The type (`warning`) also comes from the config entry, not the tag name.
-
-```mdcms callout-info
-message: aitranslation
-```
-
----
-
-## message: overrides inline content
-
-When `message:` is present, any inline `title:` or body text is ignored. A warning should appear in the browser console.
-
-```mdcms callout-info
-message: aitranslation
-title: This title should be ignored
-This body text should also be ignored. Check the console for a warning.
-```
-
----
-
-## Missing icon
-
-This callout uses a non-existent icon name. A broken image should appear where the icon would be.
-
-```mdcms callout-info
-title: Custom icon that does not exist
-icon: nonexistent_icon
-The icon to the left of this title should show as a broken image.
-```
-
----
-
-Toggle dark mode and check all four callout types still look correct.
+- All sections appear as headings in sort order
+- Pages within each section appear in sort order
+- This page (Home) does **not** appear in the list
+- Draft pages are excluded
+- Switching category (if enabled) updates the page list
diff --git a/mdcms.py b/mdcms.py
index 473c2f6..593ed5e 100644
--- a/mdcms.py
+++ b/mdcms.py
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
#
-# mdcms v0.3.6 — CLI companion
+# mdcms v0.3.7 — CLI companion
#
# Copyright 2026 Kristian Benestad
# Apache License, Version 2.0 — https://www.apache.org/licenses/LICENSE-2.0
-"""MD-CMS v0.3.5 — CLI tool for managing and building MD-CMS sites."""
+"""MD-CMS v0.3.7 — CLI tool for managing and building MD-CMS sites."""
import json
import os
@@ -21,7 +21,7 @@ import certifi
import click
import yaml
-CLI_VERSION = "0.3.6"
+CLI_VERSION = "0.3.7"
CLI_RELEASE_DATE = "17 May 2026"
MIN_SUPPORTED_VERSION = "0.3"
diff --git a/pyproject.toml b/pyproject.toml
index 74807ec..9124c4c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "mdcms"
-version = "0.3.6"
+version = "0.3.7"
description = "MD-CMS — Markdown-based CMS companion CLI"
readme = "README.md"
license = { text = "Apache-2.0" }
diff --git a/test_phase.py b/test_phase.py
index 9104574..61dbfac 100644
--- a/test_phase.py
+++ b/test_phase.py
@@ -28,7 +28,7 @@ PHASES = {
2: ("v0.4_phase2", "Icon system — local SVGs, no Google Fonts"),
3: ("v0.4_phase3", "Asset validation in mdcms build"),
4: ("claude/debug-api-errors-gd730", "Callout tags"),
- 5: ("v0.4_phase5", "Table of contents tag"),
+ 5: ("claude/toc-tag-phase5", "Table of contents tag"),
6: ("v0.4_phase6", "Offline / fetch-deps"),
7: ("v0.4_phase7", "PWA — service worker and manifest"),
}
@@ -108,6 +108,9 @@ EXTRA_FILES = {
"app/config.yml", # has callouts: block for message: key test
"app/pages/home.md", # has Phase 4 callout test cases
],
+ 5: [
+ "app/pages/home.md", # has Phase 5 TOC test case
+ ],
}