v0.4 Phase 7: PWA support + bump to 0.3.8

- Add generate_pwa(): builds manifest.json (name, short_name, theme_color,
  standalone display, favicon icon) and service-worker.js with a
  cache-first strategy; cache name is versioned by a hash of the precache
  file list so new builds automatically invalidate old caches; precache
  list covers index.html, config.yml, nav.yml, search.json, theme file,
  and all pages/posts/assets
- Call generate_pwa() from run_build() when pwa: yes/true in config.yml
- index.html: add <link rel="manifest"> and SW registration script in
  <head>; both silently no-op when the generated files are absent
- index.html boot(): write offline-message from config to localStorage
  on every load so the message survives cache eviction
- index.html navigateTo(): show localStorage offline message when a page
  fetch fails instead of the generic not-found message
- Update CLAUDE.md with PWA config key reference

https://claude.ai/code/session_015XtsgTMi8UtmgxEgb5Qt2c
This commit is contained in:
Claude 2026-05-16 17:02:37 +00:00
parent ca8deba23f
commit 92615fad1c
No known key found for this signature in database
4 changed files with 120 additions and 8 deletions

View file

@ -60,6 +60,20 @@ During development, run directly: `python3 mdcms.py <command>`
| `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. |
## 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`

View file

@ -24,6 +24,14 @@
<title>MD-CMS</title>
<meta name="description" content="">
<link rel="icon" href="assets/images/favicon.png">
<link rel="manifest" href="manifest.json">
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./service-worker.js').catch(() => {});
});
}
</script>
<!-- Libraries (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
@ -2541,10 +2549,11 @@ function fmtDatetime(dtStr) {
const result = await fetchPageFile(file);
if (!result.ok) {
contentEl.innerHTML = `<div class="error-message">
<h2>Page not available</h2>
<p>${pageNotFoundMessage()}</p>
</div>`;
const offlineMsg = localStorage.getItem('mdcms-offline');
const bodyMsg = offlineMsg
? `<p>${offlineMsg}</p>`
: `<p>${pageNotFoundMessage()}</p>`;
contentEl.innerHTML = `<div class="error-message"><h2>Page not available</h2>${bodyMsg}</div>`;
document.title = (config.sitename || 'MD-CMS');
refreshCategoryBar();
if (categoriesUse) populateCategoryOptions('');
@ -2639,6 +2648,15 @@ function fmtDatetime(dtStr) {
} catch (e) { /* fall back to hardcoded CSS defaults */ }
}
// Write offline message to localStorage for SW offline fallback
const offlineMsgCfg = config['offline-message'];
if (offlineMsgCfg) {
const offlineText = typeof offlineMsgCfg === 'string'
? offlineMsgCfg
: (offlineMsgCfg[defaultCategoryCode] || offlineMsgCfg['en'] || Object.values(offlineMsgCfg)[0] || '');
if (offlineText) localStorage.setItem('mdcms-offline', offlineText);
}
loadFonts(themeConfig);
initCategories();

View file

@ -1,11 +1,11 @@
#!/usr/bin/env python3
#
# mdcms v0.3.6 — CLI companion
# mdcms v0.3.9 — 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.9 — 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.9"
CLI_RELEASE_DATE = "17 May 2026"
MIN_SUPPORTED_VERSION = "0.3"
@ -480,6 +480,10 @@ def run_build(site_path: Path):
)
click.echo(f" Wrote search.json ({len(live_pages) + len(post_records)} entries)")
pwa_enabled = str(cfg.get("pwa", "no")).lower() in ("yes", "true")
if pwa_enabled:
generate_pwa(site_path, cfg)
asset_warnings = validate_assets(site_path, cfg)
for w in asset_warnings:
click.echo(click.style(w, fg="yellow"))
@ -492,6 +496,82 @@ def run_build(site_path: Path):
))
# ─── PWA generation ───────────────────────────────────────────
def generate_pwa(site_path: Path, cfg: dict):
"""Generate manifest.json and service-worker.js when pwa: yes."""
pwa_name = cfg.get("pwa-name", cfg.get("sitename", "MD-CMS Site"))
pwa_shortname = cfg.get("pwa-shortname", pwa_name)
pwa_colour = cfg.get("pwa-colour", "#2563EB")
# manifest.json
manifest = {
"name": pwa_name,
"short_name": pwa_shortname,
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": pwa_colour,
"icons": [
{"src": "assets/images/favicon.png", "sizes": "any", "type": "image/png"}
],
}
(site_path / "manifest.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
)
click.echo(" Wrote manifest.json")
# Collect all files to precache
precache: list = [
"index.html", "config.yml", "nav.yml", "search.json",
]
theme_file = cfg.get("theme")
if theme_file and (site_path / theme_file).exists():
precache.append(theme_file)
for folder in ("pages", "posts", "assets"):
d = site_path / folder
if not d.is_dir():
continue
for f in sorted(d.rglob("*")):
if f.is_file():
precache.append(str(f.relative_to(site_path)).replace("\\", "/"))
# Version hash — deterministic from sorted file list
cache_hash = format(hash(tuple(sorted(precache))) & 0xFFFFFFFF, "08x")
cache_name = f"mdcms-{cache_hash}"
urls_js = json.dumps(precache, indent=2, ensure_ascii=False)
sw = f"""// mdcms service worker — generated by mdcms build
const CACHE_NAME = '{cache_name}';
const PRECACHE_URLS = {urls_js};
self.addEventListener('install', event => {{
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS))
);
self.skipWaiting();
}});
self.addEventListener('activate', event => {{
event.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
self.clients.claim();
}});
self.addEventListener('fetch', event => {{
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
);
}});
"""
(site_path / "service-worker.js").write_text(sw, encoding="utf-8")
click.echo(f" Wrote service-worker.js (cache: {cache_name})")
# ─── GitHub template download ─────────────────────────────────
def _github_get(url: str) -> bytes:

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "mdcms"
version = "0.3.6"
version = "0.3.9"
description = "MD-CMS — Markdown-based CMS companion CLI"
readme = "README.md"
license = { text = "Apache-2.0" }