mirror of
https://github.com/kbenestad/mdcms.git
synced 2026-06-18 07:24:31 +00:00
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:
parent
ca8deba23f
commit
92615fad1c
4 changed files with 120 additions and 8 deletions
14
CLAUDE.md
14
CLAUDE.md
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
86
mdcms.py
86
mdcms.py
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue