From 9aa3610df4b10cdb02c365a6ae871c6b0dbe4f90 Mon Sep 17 00:00:00 2001 From: kbenestad Date: Mon, 20 Apr 2026 00:02:43 +0700 Subject: [PATCH] v0.2.2 migrated to GitHub --- .gitignore | 22 + README.md | 123 ++ mdcms.py | 1356 ++++++++++++ resources/banner/v0.1.txt | 1 + resources/banner/v0.2.1.txt | 1 + resources/banner/v0.2.2.txt | 1 + resources/banner/v0.2.txt | 1 + resources/documentation.md | 3 + resources/knownlimitations.md | 30 + resources/quickstart.md | 34 + samplesite/config.yml | 82 + samplesite/index.html | 2327 ++++++++++++++++++++ samplesite/nav.yml | 66 + samplesite/pages/about.md | 38 + samplesite/pages/about.nb.md | 38 + samplesite/pages/blog.md | 23 + samplesite/pages/blog.nb.md | 23 + samplesite/pages/home.ar.md | 18 + samplesite/pages/home.md | 18 + samplesite/pages/home.nb.md | 18 + samplesite/pages/products.md | 30 + samplesite/pages/products.nb.md | 30 + samplesite/posts/2022-q4-report.md | 23 + samplesite/posts/2023-analytics-launch.md | 27 + samplesite/posts/2023-security-update.md | 31 + samplesite/posts/2024-roadmap.md | 32 + samplesite/posts/2024-success-stories.md | 28 + samplesite/posts/2026-v9-release.md | 34 + samplesite/search.json | 197 ++ website/assets/fonts/.gitkeep | 0 website/assets/images/.gitkeep | 0 website/config.yml | 50 + website/index.html | 2328 +++++++++++++++++++++ website/nav.yml | 14 + website/pages/home.md | 67 + website/posts/.gitkeep | 0 website/search.json | 15 + 37 files changed, 7129 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 mdcms.py create mode 100644 resources/banner/v0.1.txt create mode 100644 resources/banner/v0.2.1.txt create mode 100644 resources/banner/v0.2.2.txt create mode 100644 resources/banner/v0.2.txt create mode 100644 resources/documentation.md create mode 100644 resources/knownlimitations.md create mode 100644 resources/quickstart.md create mode 100644 samplesite/config.yml create mode 100644 samplesite/index.html create mode 100644 samplesite/nav.yml create mode 100644 samplesite/pages/about.md create mode 100644 samplesite/pages/about.nb.md create mode 100644 samplesite/pages/blog.md create mode 100644 samplesite/pages/blog.nb.md create mode 100644 samplesite/pages/home.ar.md create mode 100644 samplesite/pages/home.md create mode 100644 samplesite/pages/home.nb.md create mode 100644 samplesite/pages/products.md create mode 100644 samplesite/pages/products.nb.md create mode 100644 samplesite/posts/2022-q4-report.md create mode 100644 samplesite/posts/2023-analytics-launch.md create mode 100644 samplesite/posts/2023-security-update.md create mode 100644 samplesite/posts/2024-roadmap.md create mode 100644 samplesite/posts/2024-success-stories.md create mode 100644 samplesite/posts/2026-v9-release.md create mode 100644 samplesite/search.json create mode 100644 website/assets/fonts/.gitkeep create mode 100644 website/assets/images/.gitkeep create mode 100644 website/config.yml create mode 100644 website/index.html create mode 100644 website/nav.yml create mode 100644 website/pages/home.md create mode 100644 website/posts/.gitkeep create mode 100644 website/search.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02eac69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +### AL ### +#Template for AL projects for Dynamics 365 Business Central +#launch.json folder +.vscode/ +#Cache folder +.alcache/ +#Symbols folder +.alpackages/ +#Snapshots folder +.snapshots/ +#Testing Output folder +.output/ +#Extension App-file +*.app +#Rapid Application Development File +rad.json +#Translation Base-file +*.g.xlf +#License-file +*.flf +#Test results file +TestResults.xml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad35c38 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# MD-CMS + +> Markdown-based static site publishing — no server, no database, no terminal required. + +MD-CMS lets you write and publish a website entirely in markdown. Drop your `.md` files in a folder, run the build tool, upload the output to any static host, and you're done. All rendering happens in the browser. + +--- + +## How it works + +MD-CMS has two parts: + +**`index.html`** — a single-file browser renderer. It reads your markdown files, config, and navigation at runtime and renders everything client-side. No build pipeline, no framework, no compilation step. + +**`mdcms.py`** — a zero-dependency Python CLI tool. It scans your content, generates `nav.yml` and `search.json`, validates your config, and packages everything into a zip file ready for upload. + +--- + +## Features + +- **Write in markdown** — pages and posts with YAML frontmatter +- **Categories** — serve multiple versions of the same page (e.g. languages, destinations, variants) via `?cat=` URL parameter and a dropdown UI +- **Sections** — nested navigation defined in `nav.yml`; pages declare their section via frontmatter +- **Full-text search** — category-aware, generated at build time +- **Dynamic content tags** — embed post lists with date sorting, pagination, and year grouping using fenced `mdcms` code blocks +- **RTL support** — per-category text direction +- **Custom fonts per category** — load a font file from `assets/fonts/` when a category is selected +- **Light and dark mode** — fully themeable via `config.yml` +- **No server required** — everything is static; deploy to GitHub Pages, Codeberg Pages, Cloudflare Pages, Netlify, or any file host +- **Zero dependencies** — `mdcms.py` uses only the Python standard library + +--- + +## File structure + +``` +mdcms.py ← build tool, run this +quickstart.md ← getting started guide +website/ ← everything in here gets deployed + index.html + config.yml + nav.yml + search.json + pages/ + home.md + about.md + about.nb.md ← Norwegian variant of about.md + posts/ + 2025-01-01-my-first-post.md + assets/ + images/ + fonts/ +``` + +The `website/` folder is your deployable site. `mdcms.py` lives outside it. + +--- + +## Getting started + +**Requirements:** Python 3 (standard library only). A modern browser. + +1. Clone or download this repository. +2. Run `python3 mdcms.py` and choose **option 2** to build your config and folder structure from scratch. +3. Write your pages in `website/pages/` and posts in `website/posts/`. +4. Run `mdcms.py` again and choose **option 3** to generate `nav.yml` and `search.json`. +5. Choose **option 8** to start a local webserver and preview your site. +6. When ready to publish, choose **option 1** to validate, build, and export `website.zip`. +7. Upload the contents of `website.zip` to your static host. + +> **Local preview note:** Open `index.html` via the built-in webserver (option 8), not by double-clicking the file. Browsers block local file access due to CORS restrictions. + +--- + +## Configuration + +Site behaviour is controlled by two YAML files in `website/`: + +**`config.yml`** — site title, logo, default page, search settings, typography, layout dimensions, light/dark theme colours, and category definitions. + +**`nav.yml`** — navigation structure. Sections are defined here; pages declare their section via `section-id` in frontmatter. Sections can be nested. + +Both files are human-readable and comment-supported. The `mdcms.py` wizard generates them for you and can fill in missing values interactively. + +--- + +## Categories + +Categories let you publish multiple versions of the same page — different languages, regions, or product variants — under a single URL with a `?cat=` parameter. + +Each variant is a separate file: + +``` +about.md ← default +about.en-gb.md ← British English variant +about.nb.md ← Norwegian variant +``` + +The category dropdown shows only categories for which a variant exists (or where a "not available" message is configured). All internal links preserve the active category. + +--- + +## Tag system + +Embed dynamic post lists in any page using fenced `mdcms` code blocks: + +````markdown +```mdcms +posts-date-reversechronological +limit: 10 +paginate: yes +``` +```` + +Available tags cover chronological and reverse-chronological post lists, grouped by year, with date or datetime display, and configurable pagination. + +--- + +## Licence + +Apache 2.0 — see [LICENCE]((https://www.apache.org/licenses/LICENSE-2.0)). + +© Kristian Benestad diff --git a/mdcms.py b/mdcms.py new file mode 100644 index 0000000..22b7a29 --- /dev/null +++ b/mdcms.py @@ -0,0 +1,1356 @@ +#!/usr/bin/env python3 +# +# MD-CMS v0.2.2 — Companion CLI +# +# LICENCE +# Copyright 2026 Kristian Benestad | docs.benestad.net +# +# Licensed under the Apache License, Version 2.0 (the "Licence"); +# you may not use this file except in compliance with the Licence. +# You may obtain a copy of the Licence at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Licence is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the Licence for the specific language governing permissions and +# limitations under the Licence. + +"""MD-CMS v0.2.2 — Companion CLI. + +Scans `pages/` and `posts/` in the active website directory and writes +`nav.yml` and `search.json`. Also manages a registry of named website paths +so the tool can be invoked from anywhere on the system. +""" + +import json +import os +import re +import sys +import subprocess +import time +import urllib.request +import webbrowser +import zipfile +from pathlib import Path + +VERSION = "0.2" +DATE = "2026-04-16" +BANNER_URL = f"https://raw.githubusercontent.com/kbenestad/mdcms/refs/heads/main/resources/banner/v{VERSION}.txt" +HELP_URL = "https://docs.benestad.net" + +CONFIG_DIR = Path.home() / ".mdcms" +PATHS_FILE = CONFIG_DIR / "paths.json" + +SEP = "-" * 80 + + +# ─── Path registry ─────────────────────────────────────────── + +def load_registry(): + if PATHS_FILE.exists(): + try: + return json.loads(PATHS_FILE.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + pass + return {"active": None, "paths": {}} + + +def save_registry(reg): + CONFIG_DIR.mkdir(parents=True, exist_ok=True) + PATHS_FILE.write_text(json.dumps(reg, indent=2), encoding="utf-8") + + +def active_path(): + reg = load_registry() + name = reg.get("active") + if not name: + return None, None + p = reg["paths"].get(name) + if not p: + return None, None + return name, Path(p) + + +# ─── Frontmatter parser ────────────────────────────────────── + +def parse_frontmatter(filepath): + """Return (meta_dict, body_text). Meta is a flat key:value map.""" + try: + content = Path(filepath).read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as e: + print(f" Warning: could not read {filepath}: {e}") + return None, "" + + match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL) + if not match: + return {}, content + + meta = {} + for line in match.group(1).split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + colon = line.find(":") + if colon == -1: + continue + key = line[:colon].strip() + value = line[colon + 1:].strip() + if (value.startswith('"') and value.endswith('"')) or \ + (value.startswith("'") and value.endswith("'")): + value = value[1:-1] + if value.lower() == "true": + value = True + elif value.lower() == "false": + value = False + elif re.match(r"^\d+$", value): + value = int(value) + meta[key] = value + + return meta, content[match.end():] + + +# ─── Scanner ───────────────────────────────────────────────── + +CATEGORY_CODE_RE = re.compile(r"^[a-zA-Z0-9\-]+$") + + +def parse_config_categories(text): + """Extract category info from config.yml. + + Returns {'use': bool, 'default_code': str|None, 'codes': [str, ...]}. + Only looks at `categories-use`, `default-category.code`, and `categories[].code`. + The `code` field must be the first key of each category item (per spec example). + """ + use = False + default_code = None + codes = [] + in_default = False + in_categories = False + + for raw in text.splitlines(): + line = raw.rstrip() + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if not line.startswith(" "): + in_default = False + in_categories = False + if line.startswith("categories-use:"): + val = line.split(":", 1)[1].strip().strip('"\'') + use = val.lower() in ("yes", "true") + elif line.rstrip(":") == "default-category": + in_default = True + elif line.rstrip(":") == "categories": + in_categories = True + continue + if in_default and line.startswith(" ") and not line.startswith(" "): + m = re.match(r"^ (\S+):\s*(.*)$", line) + if m and m.group(1) == "code": + default_code = m.group(2).strip().strip('"\'') + if in_categories and line.startswith(" - "): + m = re.match(r"^ - (\S+):\s*(.*)$", line) + if m and m.group(1) == "code": + codes.append(m.group(2).strip().strip('"\'')) + + return {"use": use, "default_code": default_code, "codes": codes} + + +def identify_variant(path_rel, known_codes): + """Split a .md path into (base_without_ext, category_code_or_None). + + `base_without_ext` includes the directory and base name but no extension + and no category suffix. `known_codes` is the set of valid category codes + (default + additional). A trailing `.` in the filename is only + recognised as a category suffix if `` is in `known_codes`. + """ + if not path_rel.endswith(".md"): + return None, None + stem = path_rel[:-3] + base_name = os.path.basename(stem) + if "." in base_name: + head, _, suffix = stem.rpartition(".") + if suffix in known_codes: + return head, suffix + return stem, None + + +def scan_and_categorize(directory, known_codes): + """Scan a directory for .md files. Each returned record includes `base` and + `code` (`None` for files without a category suffix).""" + records = [] + if not os.path.isdir(directory): + return records + for root, dirs, files in os.walk(directory): + dirs.sort() + for name in sorted(files): + if not name.endswith(".md"): + continue + full = os.path.join(root, name) + rel = os.path.relpath(full, ".").replace("\\", "/") + base, code = identify_variant(rel, known_codes) + if base is None: + continue + meta, body = parse_frontmatter(full) + if meta is None or meta.get("draft", False): + continue + records.append({ + "file": rel, + "base": base, + "code": code, + "title": meta.get("title") or os.path.basename(base).replace("_", " ").replace("-", " ").title(), + "sort": meta.get("sort"), + "section-id": meta.get("section-id"), + "author": meta.get("author"), + "date": str(meta.get("date", "")), + "datetime": str(meta.get("datetime", "")), + "created": str(meta.get("created", "")), + "modified": str(meta.get("modified", "")), + "language": meta.get("language", "en"), + "keywords": meta.get("keywords", ""), + "description": meta.get("description", ""), + "body": body[:5000], + }) + return records + + +def group_by_base(records): + """Group records by their conceptual base path. Returns {base: {code: record}}.""" + groups = {} + for r in records: + groups.setdefault(r["base"], {})[r["code"]] = r + return groups + + +def select_primary(variants_dict, default_code): + """Pick a representative record for a page group. + + Priority: default-coded variant → base file → first available. + """ + if default_code and default_code in variants_dict: + return variants_dict[default_code] + if None in variants_dict: + return variants_dict[None] + return next(iter(variants_dict.values())) + + +def _coerce(v): + """Coerce a YAML scalar string to a Python value.""" + if v == "" or v.lower() in ("null", "~"): + return None + if v == "true": + return True + if v == "false": + return False + if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")): + return v[1:-1] + if re.match(r"^-?\d+$", v): + return int(v) + m = re.match(r"^\[(.*)\]$", v) + if m: + inner = m.group(1).strip() + return [_coerce(x.strip()) for x in inner.split(",")] if inner else [] + return v + + +def _emit_value(v): + """Quote a YAML scalar if it contains characters that would confuse the parser.""" + if v is None: + return "" + s = str(v) + if s == "" or any(c in s for c in ':"\'#') or s.lower() in ("true", "false", "null"): + return '"' + s.replace('"', '\\"') + '"' + return s + + +def parse_nav_yml(text): + """Parse the nav.yml subset we emit. Returns (sections, pages) as lists of dicts. + + Handles: + - top-level keys `sections:` and `pages:` + - list items at 2-space indent: ` - key: value` + - item properties at 4-space indent: ` key: value` + - one level of nested dict at 6-space indent (for `categorynames`) + Ignores unknown top-level keys. Not a general YAML parser. + """ + sections, pages = [], [] + block = None + item = None + nested_key = None + + def flush(): + nonlocal item + if item is None: + return + if block == "sections": + sections.append(item) + elif block == "pages": + pages.append(item) + item = None + + for raw in text.splitlines(): + line = raw.rstrip() + stripped = line.lstrip() + if not stripped or stripped.startswith("#"): + continue + + # Top-level key + if not line.startswith(" "): + flush() + key = line.split(":", 1)[0].strip() + block = key if key in ("sections", "pages") else None + nested_key = None + continue + + if block is None: + continue + + # 6-space nested dict content + m = re.match(r"^ (\S[^:]*):\s*(.*)$", line) + if m and nested_key is not None and item is not None: + item.setdefault(nested_key, {})[m.group(1).strip()] = _coerce(m.group(2).strip()) + continue + + # 2-space list item start + m = re.match(r"^ - (\S[^:]*):\s*(.*)$", line) + if m: + flush() + item = {m.group(1).strip(): _coerce(m.group(2).strip())} + nested_key = None + continue + + if item is None: + continue + + # 4-space item property + m = re.match(r"^ (\S[^:]*):\s*(.*)$", line) + if m: + key = m.group(1).strip() + val = m.group(2).strip() + if val == "": + nested_key = key + item[key] = {} + else: + nested_key = None + item[key] = _coerce(val) + + flush() + return sections, pages + + +# ─── Section merge (phase 2) ───────────────────────────────── + +def merge_sections(page_entries, existing_sections): + """Merge existing sections with auto-created stubs for new section-ids. + + Returns (merged_sections, auto_created_codes). + Preserves every field on existing sections; only adds new entries. + """ + by_code = {s["code"]: dict(s) for s in existing_sections if s.get("code")} + + referenced = sorted({p.get("section-id") for p in page_entries if p.get("section-id")}) + + auto_created = [] + for code in referenced: + if code in by_code: + continue + used_sorts = {s.get("sort") for s in by_code.values() if isinstance(s.get("sort"), int)} + next_sort = 100 + while next_sort in used_sorts: + next_sort += 10 + by_code[code] = { + "code": code, + "defaultname": code.replace("-", " ").replace("_", " ").title(), + "sort": next_sort, + "pagesvisibility": "visible", + } + auto_created.append(code) + + merged = sorted(by_code.values(), key=lambda s: (s.get("sort") or 999, s["code"])) + return merged, auto_created + + +def build_page_nav(page_groups, existing_pages, categories_use=False, default_code=None): + """Produce the page nav list. One entry per conceptual page (grouped by base). + + When `categories_use`, adds `variants` (list of category codes with an + actual file on disk — the base file counts as the default category) and + `titles` (per-category titles). Preserves nav.yml `sort` when the file + path matches. + """ + existing_by_file = {p["file"]: p for p in existing_pages if p.get("file")} + out = [] + for base, variants in sorted(page_groups.items()): + file = base + ".md" + primary = select_primary(variants, default_code) + + existing = existing_by_file.get(file, {}) + sort = existing.get("sort") + if sort is None: + sort = primary.get("sort") + if sort is None: + sort = 100 + + entry = { + "file": file, + "title": primary.get("title", ""), + "section-id": primary.get("section-id"), + "sort": sort, + } + + if categories_use: + covered = set() + titles = {} + for code, record in variants.items(): + key = code if code is not None else default_code + if key is None: + continue + covered.add(key) + titles[key] = record.get("title", "") + entry["variants"] = sorted(covered) + entry["titles"] = titles + + out.append(entry) + out.sort(key=lambda p: (p["sort"], p["file"])) + return out + + +# ─── Generators ────────────────────────────────────────────── + +def generate_nav_yml(sections, pages, categories_use=False): + """Emit nav.yml with `sections:` and `pages:` blocks.""" + lines = [ + "# nav.yml — generated by mdcms.py", + "# Manual edits to section metadata (defaultname, sort, parent, parent-sort,", + "# pagesvisibility, categorynames) are preserved on rebuild. New sections", + "# are auto-created from page frontmatter section-id values.", + "", + "sections:", + ] + if not sections: + lines.append(" # (none yet — add section-id to page frontmatter to auto-create)") + else: + for s in sections: + lines.append(f" - code: {s['code']}") + lines.append(f" defaultname: {_emit_value(s.get('defaultname', s['code']))}") + lines.append(f" sort: {s.get('sort', 100)}") + if s.get("parent"): + lines.append(f" parent: {s['parent']}") + lines.append(f" parent-sort: {s.get('parent-sort', 100)}") + lines.append(f" pagesvisibility: {s.get('pagesvisibility', 'visible')}") + cn = s.get("categorynames") or {} + if cn: + lines.append(" categorynames:") + for k, v in cn.items(): + lines.append(f" {k}: {_emit_value(v)}") + lines.append("") + + lines.append("pages:") + if not pages: + lines.append(" # (no pages)") + else: + for p in pages: + lines.append(f" - file: {p['file']}") + lines.append(f" title: {_emit_value(p['title'])}") + if p.get("section-id"): + lines.append(f" section-id: {p['section-id']}") + lines.append(f" sort: {p.get('sort', 100)}") + if categories_use and p.get("variants"): + lines.append(f" variants: [{', '.join(p['variants'])}]") + if categories_use and p.get("titles"): + lines.append(" titles:") + for code, title in p["titles"].items(): + lines.append(f" {code}: {_emit_value(title)}") + lines.append("") + return "\n".join(lines) + + +def generate_search_json(records, categories_use=False, default_code=None): + """Emit search.json. When `categories_use`, each entry gets a `category` field + and the `file` is the conceptual base path (so search results link to the + same URL regardless of which variant matched).""" + out = [] + for r in records: + file_path = (r["base"] + ".md") if "base" in r else r.get("file", "") + entry = { + "file": file_path, + "title": r.get("title", ""), + "section-id": r.get("section-id"), + "keywords": r.get("keywords", ""), + "description": r.get("description", ""), + "author": r.get("author"), + "date": r.get("date", ""), + "datetime": r.get("datetime", ""), + "language": r.get("language", "en"), + "body": r.get("body", ""), + } + if categories_use: + code = r.get("code") + entry["category"] = code if code is not None else default_code + out.append(entry) + return json.dumps(out, indent=2, ensure_ascii=False) + + +# ─── Banner ────────────────────────────────────────────────── + +def load_banner(): + try: + req = urllib.request.Request(BANNER_URL, headers={"User-Agent": f"mdcms/{VERSION}"}) + with urllib.request.urlopen(req, timeout=3) as r: + return r.read().decode("utf-8").strip() + except Exception: + return None + + +# ─── UI helpers ────────────────────────────────────────────── + +def clear(): + os.system("cls" if os.name == "nt" else "clear") + + +def header(): + print(f"MD-CMS v{VERSION} ({DATE}) (C) Kristian Benestad") + print("Apache 2.0 Licence") + print(SEP) + + +def pause(): + input("\nPress Enter to continue...") + + +# ─── Build actions (options 3 & 4) ─────────────────────────── + +def do_build_nav_and_search(project): + """Option 3: scan pages/ and posts/ in project/website/, write nav.yml and search.json.""" + site = project / "website" + if not site.is_dir(): + print(f" Error: {site} not found.") + return + os.chdir(site) + print(f"Working directory: {site}") + print() + + if not os.path.isdir("pages"): + print(" Error: pages/ folder not found.") + return + + # Load category config from config.yml so filename parsing can recognise + # category suffixes. Missing / disabled = no category handling. + cat_cfg = {"use": False, "default_code": None, "codes": []} + if os.path.isfile("config.yml"): + try: + cat_cfg = parse_config_categories(Path("config.yml").read_text(encoding="utf-8")) + except Exception as e: + print(f" Warning: could not read category config ({e}).") + + # Validate codes + all_declared = [c for c in [cat_cfg["default_code"]] + cat_cfg["codes"] if c] + invalid = [c for c in all_declared if not CATEGORY_CODE_RE.match(c)] + if invalid: + print(f" Error: invalid category code(s) {invalid}. Must match [a-zA-Z0-9-]+.") + return + + if cat_cfg["use"] and not cat_cfg["default_code"]: + print(" Error: categories-use: yes but no default-category.code defined.") + return + + known_codes = set(all_declared) if cat_cfg["use"] else set() + + # Scan + page_records = scan_and_categorize("pages", known_codes) + post_records = scan_and_categorize("posts", known_codes) + print(f"Scanned pages/ — {len(page_records)} file(s)") + print(f"Scanned posts/ — {len(post_records)} file(s)") + + page_groups = group_by_base(page_records) + + # Existing nav.yml → preserve manual edits + existing_sections, existing_pages = [], [] + if os.path.isfile("nav.yml"): + try: + existing_sections, existing_pages = parse_nav_yml( + Path("nav.yml").read_text(encoding="utf-8") + ) + except Exception as e: + print(f" Warning: could not parse existing nav.yml ({e}); starting fresh.") + + # One representative record per conceptual page, for section-id discovery + primary_entries = [select_primary(v, cat_cfg["default_code"]) for v in page_groups.values()] + sections, auto_created = merge_sections(primary_entries, existing_sections) + + page_nav = build_page_nav( + page_groups, existing_pages, + categories_use=cat_cfg["use"], + default_code=cat_cfg["default_code"], + ) + + Path("nav.yml").write_text( + generate_nav_yml(sections, page_nav, categories_use=cat_cfg["use"]), + encoding="utf-8", + ) + print("Wrote nav.yml") + + # Filter out draft-section pages from search. + draft_codes = {s["code"] for s in sections if s.get("pagesvisibility") == "draft"} + live_pages = [r for r in page_records if r.get("section-id") not in draft_codes] + Path("search.json").write_text( + generate_search_json( + live_pages + post_records, + categories_use=cat_cfg["use"], + default_code=cat_cfg["default_code"], + ), + encoding="utf-8", + ) + print(f"Wrote search.json ({len(live_pages) + len(post_records)} entries)") + + if auto_created: + print() + print(f" Notice: {len(auto_created)} section(s) auto-created from page frontmatter:") + for c in auto_created: + print(f" - {c}") + print(" Edit nav.yml to customise defaultname, sort, parent, or pagesvisibility.") + + +def do_build_search_only(project): + """Option 4: regenerate search.json only.""" + site = project / "website" + if not site.is_dir(): + print(f" Error: {site} not found.") + return + os.chdir(site) + + cat_cfg = {"use": False, "default_code": None, "codes": []} + if os.path.isfile("config.yml"): + try: + cat_cfg = parse_config_categories(Path("config.yml").read_text(encoding="utf-8")) + except Exception: + pass + known_codes = set(c for c in [cat_cfg["default_code"]] + cat_cfg["codes"] if c) if cat_cfg["use"] else set() + + page_records = scan_and_categorize("pages", known_codes) + post_records = scan_and_categorize("posts", known_codes) + + draft_codes = set() + if os.path.isfile("nav.yml"): + try: + sections, _ = parse_nav_yml(Path("nav.yml").read_text(encoding="utf-8")) + draft_codes = {s["code"] for s in sections if s.get("pagesvisibility") == "draft"} + except Exception: + pass + live_pages = [r for r in page_records if r.get("section-id") not in draft_codes] + + Path("search.json").write_text( + generate_search_json( + live_pages + post_records, + categories_use=cat_cfg["use"], + default_code=cat_cfg["default_code"], + ), + encoding="utf-8", + ) + print(f"Wrote search.json — {len(live_pages) + len(post_records)} entries.") + + +# ─── Option 7: path management ─────────────────────────────── + +def manage_paths(): + while True: + clear() + header() + reg = load_registry() + print("Website paths\n") + if not reg["paths"]: + print(" (none defined)") + else: + for name, p in reg["paths"].items(): + marker = "*" if name == reg.get("active") else " " + print(f" {marker} {name} → {p}") + print() + print(" a Add a path") + print(" s Set active path") + print(" r Remove a path") + print(" b Back") + print() + choice = input("Select: ").strip().lower() + + if choice == "a": + name = input("Name: ").strip() + if not name: + continue + p = input("Path to project root (containing website/ folder): ").strip() + if not p: + continue + path = Path(p).expanduser().resolve() + if not path.is_dir(): + print(f" Not a directory: {path}") + pause() + continue + if not (path / "website").is_dir(): + print(f" No website/ subfolder in: {path}") + pause() + continue + reg["paths"][name] = str(path) + if not reg.get("active"): + reg["active"] = name + save_registry(reg) + pause() + elif choice == "s": + if not reg["paths"]: + pause() + continue + name = input("Name to activate: ").strip() + if name in reg["paths"]: + reg["active"] = name + save_registry(reg) + else: + print(" Not found.") + pause() + elif choice == "r": + if not reg["paths"]: + pause() + continue + name = input("Name to remove: ").strip() + if name in reg["paths"]: + del reg["paths"][name] + if reg.get("active") == name: + reg["active"] = next(iter(reg["paths"]), None) + save_registry(reg) + else: + print(" Not found.") + pause() + elif choice == "b": + return + + +# ─── Option 8: webserver ───────────────────────────────────── + +def start_webserver(project): + port = 8800 + site = project / "website" + if not site.is_dir(): + print(f"\n Error: {site} not found.") + return + os.chdir(site) + print(f"\nStarting python3 -m http.server {port} in {site}") + print("Press Ctrl+C to stop.\n") + try: + proc = subprocess.Popen([sys.executable, "-m", "http.server", str(port)]) + time.sleep(0.8) + webbrowser.open(f"http://localhost:{port}/") + proc.wait() + except KeyboardInterrupt: + proc.terminate() + print("\nStopped.") + + +# ─── Option 9 & 10 ─────────────────────────────────────────── + +def online_help(): + print(f"Opening {HELP_URL} ...") + webbrowser.open(HELP_URL) + pause() + + +def about(): + clear() + header() + print("MD-CMS is a lightweight Markdown-based CMS by Kristian Benestad.") + print() + print(f" Docs: https://docs.benestad.net") + print(f" Repo: https://github.com/kbenestad/mdcms") + print() + print("Licensed under the Apache License, Version 2.0.") + print(" https://www.apache.org/licenses/LICENSE-2.0") + pause() + + +# ─── Validation ────────────────────────────────────────────── + +def _read_file(path): + """Read a file, returning (text, error). On failure text is None.""" + try: + return Path(path).read_text(encoding="utf-8"), None + except OSError as e: + return None, str(e) + + +def validate_config(site): + """Check config.yml for missing or invalid values. + + Returns a list of dicts: {'field': ..., 'issue': ..., 'current': ...}. + """ + path = site / "config.yml" + text, err = _read_file(path) + if text is None: + return [{"field": "config.yml", "issue": f"Cannot read file: {err}", "current": None}] + + issues = [] + + # Simple key extraction from the raw text (reuses our non-YAML parser style) + def get_top(key): + for line in text.splitlines(): + if line.startswith(key + ":"): + return line.split(":", 1)[1].strip().strip('"\'') + return None + + # sitename + sn = get_top("sitename") + if not sn: + issues.append({"field": "sitename", "issue": "Missing site name", "current": sn}) + + # navigation + nav = get_top("navigation") + if nav and nav not in ("sidebar", "topbar"): + issues.append({"field": "navigation", "issue": f"Must be 'sidebar' or 'topbar'", "current": nav}) + + # category validation + cat_cfg = parse_config_categories(text) + if cat_cfg["use"]: + if not cat_cfg["default_code"]: + issues.append({"field": "default-category.code", + "issue": "categories-use: yes but no default category code defined", + "current": None}) + if not cat_cfg["codes"]: + issues.append({"field": "categories", + "issue": "categories-use: yes but no additional categories defined", + "current": None}) + + # Validate all codes + all_codes = [c for c in [cat_cfg["default_code"]] + cat_cfg["codes"] if c] + for code in all_codes: + if not CATEGORY_CODE_RE.match(code): + issues.append({"field": f"category code '{code}'", + "issue": "Must match [a-zA-Z0-9-]+", + "current": code}) + + # Check for duplicate codes + seen = set() + for code in all_codes: + if code in seen: + issues.append({"field": f"category code '{code}'", + "issue": "Duplicate category code", + "current": code}) + seen.add(code) + + return issues + + +def validate_nav(site): + """Check nav.yml for missing or invalid values. + + Returns a list of dicts: {'field': ..., 'issue': ..., 'current': ...}. + """ + path = site / "nav.yml" + text, err = _read_file(path) + if text is None: + return [{"field": "nav.yml", "issue": f"Cannot read file: {err}", "current": None}] + + try: + sections, pages = parse_nav_yml(text) + except Exception as e: + return [{"field": "nav.yml", "issue": f"Parse error: {e}", "current": None}] + + issues = [] + + # Load category config to check categorynames + config_text, _ = _read_file(site / "config.yml") + cat_cfg = parse_config_categories(config_text) if config_text else {"use": False, "default_code": None, "codes": []} + sectionnames_mode = None + if config_text: + for line in config_text.splitlines(): + if line.startswith("categories-sectionnames:"): + sectionnames_mode = line.split(":", 1)[1].strip().strip('"\'') + + all_codes = [c for c in [cat_cfg["default_code"]] + cat_cfg["codes"] if c] + + # Section checks + section_codes = set() + for i, s in enumerate(sections): + label = f"sections[{i}]" + if not s.get("code"): + issues.append({"field": f"{label}.code", "issue": "Missing section code", "current": None}) + else: + section_codes.add(s["code"]) + if not s.get("defaultname"): + issues.append({"field": f"{label}.defaultname", + "issue": f"Missing default name for section '{s.get('code', '?')}'", + "current": None}) + pv = s.get("pagesvisibility", "visible") + if pv not in ("visible", "hidden", "draft"): + issues.append({"field": f"{label}.pagesvisibility", + "issue": f"Must be visible, hidden, or draft", + "current": pv}) + if s.get("parent") and s.get("parent-sort") is None: + issues.append({"field": f"{label}.parent-sort", + "issue": f"parent is set but parent-sort is missing", + "current": None}) + + # categorynames check + if cat_cfg["use"] and sectionnames_mode == "per-category": + cn = s.get("categorynames") or {} + for code in all_codes: + if code not in cn: + issues.append({"field": f"{label}.categorynames.{code}", + "issue": f"Missing category name for '{code}' in section '{s.get('code', '?')}'", + "current": None}) + + # Page checks + for i, p in enumerate(pages): + label = f"pages[{i}]" + if not p.get("file"): + issues.append({"field": f"{label}.file", "issue": "Missing file path", "current": None}) + if not p.get("title"): + issues.append({"field": f"{label}.title", + "issue": f"Missing title for '{p.get('file', '?')}'", + "current": None}) + sid = p.get("section-id") + if sid and sid not in section_codes: + issues.append({"field": f"{label}.section-id", + "issue": f"Section-id '{sid}' not defined in sections", + "current": sid}) + + return issues + + +def _print_issues(issues, label): + """Print validation issues in a readable format.""" + if not issues: + print(f"\n {label}: no issues found.") + return + print(f"\n {label}: {len(issues)} issue(s):\n") + for iss in issues: + cur = f" (current: {iss['current']})" if iss["current"] is not None else "" + print(f" • {iss['field']}: {iss['issue']}{cur}") + + +# ─── Option 1: Prepare for upload ─────────────────────────── + +def do_prepare_for_upload(project): + """Validate config + nav, build search.json, create zip.""" + site = project / "website" + if not site.is_dir(): + print(f"\n Error: {site} not found.") + return + + print() + + # Step 1: Validate config.yml + config_issues = validate_config(site) + _print_issues(config_issues, "config.yml") + + # Step 2: Validate nav.yml + nav_issues = validate_nav(site) + _print_issues(nav_issues, "nav.yml") + + total_issues = len(config_issues) + len(nav_issues) + if total_issues > 0: + print() + fix = input(f" {total_issues} issue(s) found. Continue anyway? (y/n): ").strip().lower() + if fix != "y": + print(" Aborted.") + return + + # Step 3: Build search.json + print() + try: + do_build_search_only(project) + except Exception as e: + print(f" Error building search.json: {e}") + return + + # Step 4: Create zip + print() + cwd = Path.cwd() + default_dest = cwd / "website.zip" + dest_input = input(f" Zip destination [{default_dest}]: ").strip() + dest = Path(dest_input) if dest_input else default_dest + dest = dest.expanduser().resolve() + + # Ensure parent directory exists + dest.parent.mkdir(parents=True, exist_ok=True) + + try: + count = 0 + with zipfile.ZipFile(dest, "w", zipfile.ZIP_DEFLATED) as zf: + for root, dirs, files in os.walk(site): + dirs.sort() + for name in sorted(files): + full = Path(root) / name + arcname = full.relative_to(site) + zf.write(full, arcname) + count += 1 + print(f"\n Created {dest} ({count} files)") + except OSError as e: + print(f"\n Error creating zip: {e}") + + +# ─── Option 2: Build from scratch ─────────────────────────── + +def do_build_from_scratch(project): + """Interactive wizard to create config.yml, folder structure, and home.md.""" + site = project / "website" + + print("\n Build from scratch wizard\n") + + # Check for existing config + if (site / "config.yml").exists(): + overwrite = input(" config.yml already exists. Overwrite? (y/n): ").strip().lower() + if overwrite != "y": + print(" Aborted.") + return + + # Gather settings + sitename = input(" Site name: ").strip() or "My Site" + + nav_choice = "" + while nav_choice not in ("sidebar", "topbar"): + nav_choice = input(" Navigation layout (sidebar/topbar) [sidebar]: ").strip().lower() or "sidebar" + + nav_pos = "left" + if nav_choice == "sidebar": + while nav_pos not in ("left", "right"): + nav_pos = input(" Navigation position (left/right) [left]: ").strip().lower() or "left" + + search_choice = input(" Enable search? (yes/no) [yes]: ").strip().lower() or "yes" + search_on = search_choice in ("yes", "y", "true") + + # Categories + use_categories = input(" Use categories? (yes/no) [no]: ").strip().lower() + use_categories = use_categories in ("yes", "y") + + cat_block = "" + if use_categories: + default_code = input(" Default category code (e.g. en): ").strip() or "en" + default_name = input(f" Default category display name [{default_code}]: ").strip() or default_code + select_text = input(" Category selector label [Select language]: ").strip() or "Select language" + + extra_cats = [] + print(" Add additional categories (leave code blank to finish):") + while True: + code = input(" Code: ").strip() + if not code: + break + if not CATEGORY_CODE_RE.match(code): + print(f" Invalid code '{code}'. Must match [a-zA-Z0-9-]+.") + continue + name = input(f" Display name [{code}]: ").strip() or code + direction = input(f" Direction (ltr/rtl) [ltr]: ").strip().lower() or "ltr" + extra_cats.append({"code": code, "name": name, "direction": direction}) + + if not extra_cats: + print(" No additional categories added — disabling categories.") + use_categories = False + else: + cat_block = f""" +# ─── Category settings ─────────────────────────── +categories-use: yes +categories-sectionnames: same +categories-selecttext: "{select_text}" + +# ─── Default category ──────────────────────────── +default-category: + code: {default_code} + name: {default_name} + direction: ltr + message: "Read in {default_name}" + +# ─── Additional categories ─────────────────────── +categories: +""" + for c in extra_cats: + cat_block += f""" - code: {c['code']} + name: {c['name']} + direction: {c['direction']} + message: "{c['name']}" + notfoundmessage: "Not available in {c['name']}" +""" + + # Create folder structure + for d in ["pages", "posts", "assets/fonts", "assets/images"]: + (site / d).mkdir(parents=True, exist_ok=True) + + # Write config.yml + config_text = f"""# ─── Site settings ─────────────────────────────── +sitename: {sitename} +navigation: {nav_choice} +{"nav-position: " + nav_pos if nav_choice == "sidebar" else ""} +search: {"true" if search_on else "false"} +{cat_block} +# ─── Appearance ────────────────────────────────── +light: + bg: "#ededed" + bg-nav: "#ffffff" + font-colour: "#1a1a1a" + accent: "#29307d" + accent-hover: "#f9ad18" + +dark: + bg: "#14171c" + bg-nav: "#1a1e25" + font-colour: "#d4d4d4" + accent: "#7b83d4" + accent-hover: "#f9ad18" +""" + (site / "config.yml").write_text(config_text, encoding="utf-8") + print(f"\n Wrote config.yml") + + # Write home.md if it doesn't exist + home = site / "pages" / "home.md" + if not home.exists(): + home.write_text( + f"---\ntitle: Home\nsort: 100\n---\n\n# Welcome to {sitename}\n\n" + "This is the default landing page.\n", + encoding="utf-8", + ) + print(" Created pages/home.md") + + # Create .gitkeep files in empty dirs + for d in ["posts", "assets/fonts", "assets/images"]: + gk = site / d / ".gitkeep" + if not gk.exists() and not any((site / d).iterdir()): + gk.write_text("", encoding="utf-8") + + # Run option 3 to generate nav.yml + search.json + print() + try: + do_build_nav_and_search(project) + except Exception as e: + print(f" Error during build: {e}") + + +# ─── Option 5: Correct config.yml ─────────────────────────── + +def do_correct_config(project): + """Check config.yml for missing/invalid values and prompt to fix.""" + site = project / "website" + config_path = site / "config.yml" + + issues = validate_config(site) + _print_issues(issues, "config.yml") + + if not issues: + return + + fix = input("\n Fix interactively? (y/n): ").strip().lower() + if fix != "y": + return + + text, err = _read_file(config_path) + if text is None: + print(f" Cannot read config.yml: {err}") + return + + lines = text.splitlines() + + for iss in issues: + field = iss["field"] + print(f"\n Issue: {field} — {iss['issue']}") + + if field == "sitename": + val = input(" Enter site name: ").strip() + if val: + # Find and replace or append + replaced = False + for i, ln in enumerate(lines): + if ln.startswith("sitename:"): + lines[i] = f"sitename: {val}" + replaced = True + break + if not replaced: + lines.insert(0, f"sitename: {val}") + print(f" Set sitename to: {val}") + + elif field == "navigation": + val = "" + while val not in ("sidebar", "topbar"): + val = input(" Navigation (sidebar/topbar): ").strip().lower() + for i, ln in enumerate(lines): + if ln.startswith("navigation:"): + lines[i] = f"navigation: {val}" + break + + elif "category code" in field and "Invalid" in iss.get("issue", ""): + print(" Please edit config.yml manually to fix category codes.") + + else: + print(" Please edit config.yml manually to fix this issue.") + + # Write back + config_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + print("\n Saved config.yml") + + +# ─── Option 6: Correct nav.yml ───────────────────────────── + +def do_correct_nav(project): + """Check nav.yml for missing/invalid values and prompt to fix.""" + site = project / "website" + nav_path = site / "nav.yml" + + issues = validate_nav(site) + _print_issues(issues, "nav.yml") + + if not issues: + return + + # For missing categorynames, offer to auto-fill from defaultname + text, err = _read_file(nav_path) + if text is None: + print(f" Cannot read nav.yml: {err}") + return + + try: + sections, pages = parse_nav_yml(text) + except Exception as e: + print(f" Cannot parse nav.yml: {e}") + return + + # Separate auto-fixable (missing categorynames) from manual fixes + catname_issues = [i for i in issues if ".categorynames." in i["field"]] + other_issues = [i for i in issues if ".categorynames." not in i["field"]] + + if other_issues: + print("\n The following issues require manual editing of nav.yml:") + for iss in other_issues: + print(f" • {iss['field']}: {iss['issue']}") + + if catname_issues: + print(f"\n {len(catname_issues)} missing category name(s) found.") + fill = input(" Auto-fill missing categorynames from defaultname? (y/n): ").strip().lower() + if fill == "y": + # Load category codes + config_text, _ = _read_file(site / "config.yml") + cat_cfg = parse_config_categories(config_text) if config_text else {"use": False, "default_code": None, "codes": []} + all_codes = [c for c in [cat_cfg["default_code"]] + cat_cfg["codes"] if c] + + for s in sections: + cn = s.get("categorynames") or {} + changed = False + for code in all_codes: + if code not in cn: + cn[code] = s.get("defaultname", s.get("code", "")) + changed = True + if changed: + s["categorynames"] = cn + + # Regenerate nav.yml + config_text2, _ = _read_file(site / "config.yml") + cat_use = False + if config_text2: + cat_use = parse_config_categories(config_text2)["use"] + + nav_path.write_text( + generate_nav_yml(sections, pages, categories_use=cat_use), + encoding="utf-8", + ) + print(" Saved nav.yml with auto-filled category names.") + print(" Edit nav.yml to provide correct translations.") + + +# ─── Sections-sitemap (§2.3) ──────────────────────────────── +# Note: sections-sitemap is not yet implemented. Section headings +# remain non-clickable in the renderer. + + +# ─── Main menu ─────────────────────────────────────────────── + +def main_menu(): + while True: + clear() + header() + banner = load_banner() + if banner: + print(banner) + print() + name, website = active_path() + if name: + print(f"WEBSITE: {name} ({website})") + else: + print("WEBSITE: (none — select in option 7)") + print() + print(" 1 Prepare for upload") + print(" 2 Build config.yml and nav.yml from scratch") + print(" 3 Build nav.yml and search.json (new pages/sections)") + print(" 4 Build search.json (updated pages only)") + print(" 5 Correct missing values in config.yml") + print(" 6 Correct missing values in nav.yml") + print(" 7 Define website path") + print(" 8 Start python webserver") + print(" 9 Online help") + print(" 10 About MD-CMS") + print() + print(SEP) + choice = input("Select option (q to quit): ").strip().lower() + + if choice == "q": + return + if choice == "7": + manage_paths() + continue + if choice == "10": + about() + continue + if choice == "9": + online_help() + continue + + # Options 1-6 and 8 require an active website path + if choice in {"1", "2", "3", "4", "5", "6", "8"}: + if not website: + print("\n No active website path. Use option 7 first.") + pause() + continue + if not (website / "website").is_dir() and choice != "2": + print(f"\n Error: {website / 'website'} not found.") + pause() + continue + + if choice == "1": + try: + do_prepare_for_upload(website) + except Exception as e: + print(f"\n Error: {e}") + pause() + elif choice == "2": + try: + do_build_from_scratch(website) + except Exception as e: + print(f"\n Error: {e}") + pause() + elif choice == "3": + try: + do_build_nav_and_search(website) + except Exception as e: + print(f"\n Error: {e}") + pause() + elif choice == "4": + try: + do_build_search_only(website) + except Exception as e: + print(f"\n Error: {e}") + pause() + elif choice == "5": + try: + do_correct_config(website) + except Exception as e: + print(f"\n Error: {e}") + pause() + elif choice == "6": + try: + do_correct_nav(website) + except Exception as e: + print(f"\n Error: {e}") + pause() + elif choice == "8": + start_webserver(website) + + +def main(): + try: + main_menu() + except KeyboardInterrupt: + print() + + +if __name__ == "__main__": + main() diff --git a/resources/banner/v0.1.txt b/resources/banner/v0.1.txt new file mode 100644 index 0000000..c921ff2 --- /dev/null +++ b/resources/banner/v0.1.txt @@ -0,0 +1 @@ +This version is outdated. Please visit https://github.com/kbenestad/mdcms/ to update. diff --git a/resources/banner/v0.2.1.txt b/resources/banner/v0.2.1.txt new file mode 100644 index 0000000..c921ff2 --- /dev/null +++ b/resources/banner/v0.2.1.txt @@ -0,0 +1 @@ +This version is outdated. Please visit https://github.com/kbenestad/mdcms/ to update. diff --git a/resources/banner/v0.2.2.txt b/resources/banner/v0.2.2.txt new file mode 100644 index 0000000..c921ff2 --- /dev/null +++ b/resources/banner/v0.2.2.txt @@ -0,0 +1 @@ +This version is outdated. Please visit https://github.com/kbenestad/mdcms/ to update. diff --git a/resources/banner/v0.2.txt b/resources/banner/v0.2.txt new file mode 100644 index 0000000..c921ff2 --- /dev/null +++ b/resources/banner/v0.2.txt @@ -0,0 +1 @@ +This version is outdated. Please visit https://github.com/kbenestad/mdcms/ to update. diff --git a/resources/documentation.md b/resources/documentation.md new file mode 100644 index 0000000..c03f49d --- /dev/null +++ b/resources/documentation.md @@ -0,0 +1,3 @@ +# MD-CMS — Documentation + +Documentation is available on [docs.benestad.net](https://docs.benestad.net/) - which is also a showcase for the use of MD-CMS. \ No newline at end of file diff --git a/resources/knownlimitations.md b/resources/knownlimitations.md new file mode 100644 index 0000000..b688f7f --- /dev/null +++ b/resources/knownlimitations.md @@ -0,0 +1,30 @@ +# MD-CMS — Known limitations + +MD-CMS is under active development. + +## mdcms.py only targets `/website` directory inside a project directory +You can run `mdcms.py` from anywhere and define multiple projects. However, the current version excepts the website itself to live inside a `/website` directory inside your project directory. + +In other words, you cannot keep the files in + +- `/home/username/mdcmssites/site-1` and +- `/home/username/mdcmssites/site-2` + +they must be in + +- `/home/username/mdcmssites/site-1/website` and +- `/home/username/mdcmssites/site-2/website`. + +## mdcms tags for posts +The tags that lists posts are broken. Currently, the only tags that reliably show posts are: + +- `posts-datetime-chronological-byyearmonth` +- `posts-datetime-reversechronological` + +To correctly show posts, use `datetime` in frontmatter. + +The tag `created` is defined in the frontmatter as the created date of a file, but it is not used. + +Use `datetime` to indicate the date and time a file was created, using the format `YYYY-MM-DD HH:MM` (e.g., `2026-01-14 13:35`). + +Use `modified` using the format `YYYY-MM-DD HH:MM` (e.g., `2026-01-14 13:35`) to show users when a file was last updated. diff --git a/resources/quickstart.md b/resources/quickstart.md new file mode 100644 index 0000000..a5e0047 --- /dev/null +++ b/resources/quickstart.md @@ -0,0 +1,34 @@ +# MD-CMS — Quickstart + +A lightweight Markdown-based CMS. Content is written as plain Markdown with YAML frontmatter; configuration lives in `website/config.yml`; navigation and search are generated by `mdcms.py`. + +## First run + +1. Put content into `website/pages/` and `website/posts/`. +2. Edit `website/config.yml` with your site name and preferences. +3. From this directory, run `python3 mdcms.py`. +4. In the menu, pick option **7** to register the website path (point it at the `website/` folder). +5. Pick option **3** to build `nav.yml` and `search.json`. +6. Pick option **8** to start a local webserver and preview the site. + +## File layout + +``` +mdcms.py +quickstart.md +website/ + assets/ + fonts/ + images/ + pages/ + home.md + posts/ + index.html + config.yml + nav.yml (generated) + search.json (generated) +``` + +## Licence + +Apache 2.0. See [docs.benestad.net](https://docs.benestad.net) for documentation. diff --git a/samplesite/config.yml b/samplesite/config.yml new file mode 100644 index 0000000..bc86ba8 --- /dev/null +++ b/samplesite/config.yml @@ -0,0 +1,82 @@ +# MD-CMS v0.2 — Sample Site Configuration + +# ─── Site settings ─────────────────────────────── +sitename: Acme Corporation +sitedescription: Quality products since 1998 +navigation: sidebar +nav-position: left +search: true + +# ─── Category settings ─────────────────────────── +categories-use: yes +categories-sectionnames: per-category +categories-selecttext: "Choose language" +categories-selecticon: language + +# ─── Default category ──────────────────────────── +default-category: + code: en + name: English + direction: ltr + message: "Read in English" + pagenotfoundmessage: "This page is not available in English. Please select another language." + +# ─── Additional categories ─────────────────────── +categories: + - code: nb + name: Norsk + name-latin: Norsk + direction: ltr + message: "Les på norsk" + notfoundmessage: "Ikke tilgjengelig på norsk" + pagenotfoundmessage: "Denne siden er ikke tilgjengelig på norsk. Velg et annet språk." + + - code: ar + name: العربية + name-latin: Arabic + direction: rtl + message: "اقرأ بالعربية" + notfoundmessage: "غير متاح باللغة العربية" + pagenotfoundmessage: "هذه الصفحة غير متاحة باللغة العربية. يرجى اختيار لغة أخرى." + +# ─── Date and time formatting ──────────────────── +date: "D Mmmm YYYY" +time: 24hrs +monthnames: "January, February, March, April, May, June, July, August, September, October, November, December" +monthnamesabbreviated: "Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec" + +# ─── Appearance ────────────────────────────────── +light: + bg: "#ededed" + bg-nav: "#ffffff" + font-colour: "#1a1a1a" + font-colour-muted: "#666666" + accent: "#29307d" + accent-hover: "#f9ad18" + divider: "#e0e0e0" + nav-active-bg: "#f5f5f5" + nav-hover-bg: "#fafafa" + scrollbar-thumb: "#cccccc" + scrollbar-track: "#f5f5f5" + +dark: + bg: "#14171c" + bg-nav: "#1a1e25" + font-colour: "#d4d4d4" + font-colour-muted: "#999999" + accent: "#7b83d4" + accent-hover: "#f9ad18" + divider: "#2a2f38" + nav-active-bg: "#22272f" + nav-hover-bg: "#1f252d" + scrollbar-thumb: "#444444" + scrollbar-track: "#1a1e25" + +# ─── Fonts ─────────────────────────────────────── +font-title: IBM Plex Sans +font-title-weight: 300 +font-body: IBM Plex Sans +font-body-weight: 300 + +# ─── Footer ────────────────────────────────────── +footer: "© 2026 Acme Corporation. Licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)." diff --git a/samplesite/index.html b/samplesite/index.html new file mode 100644 index 0000000..c2b1e8a --- /dev/null +++ b/samplesite/index.html @@ -0,0 +1,2327 @@ + + + + + + +MD-CMS + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/samplesite/nav.yml b/samplesite/nav.yml new file mode 100644 index 0000000..3cdb71f --- /dev/null +++ b/samplesite/nav.yml @@ -0,0 +1,66 @@ +# nav.yml — generated by mdcms.py +# Manual edits to section metadata (defaultname, sort, parent, parent-sort, +# pagesvisibility, categorynames) are preserved on rebuild. New sections +# are auto-created from page frontmatter section-id values. + +sections: + - code: blog + defaultname: Blog + sort: 100 + pagesvisibility: visible + categorynames: + nb: Blogg + ar: المدونة + + - code: company + defaultname: Company + sort: 110 + pagesvisibility: visible + categorynames: + nb: Selskap + ar: الشركة + + - code: products + defaultname: Products + sort: 120 + pagesvisibility: visible + categorynames: + nb: Produkter + ar: المنتجات + +pages: + - file: pages/about.md + title: About + section-id: company + sort: 100 + variants: [en, nb] + titles: + en: About + nb: Om oss + + - file: pages/home.md + title: Home + sort: 100 + variants: [ar, en, nb] + titles: + ar: الرئيسية + en: Home + nb: Hjem + + - file: pages/products.md + title: Products + section-id: products + sort: 100 + variants: [en, nb] + titles: + en: Products + nb: Produkter + + - file: pages/blog.md + title: Blog + section-id: blog + sort: 110 + variants: [en, nb] + titles: + en: Blog + nb: Blogg diff --git a/samplesite/pages/about.md b/samplesite/pages/about.md new file mode 100644 index 0000000..0f431d5 --- /dev/null +++ b/samplesite/pages/about.md @@ -0,0 +1,38 @@ +--- +title: About +section-id: company +sort: 100 +--- + +# About Acme Corporation + +Acme Corporation was founded in 1998 with a mission to deliver innovative, high-quality software solutions to businesses worldwide. + +## Our Story + +From humble beginnings as a small consulting firm, we have grown into a global technology company serving over 5,000 customers across 50 countries. + +## Our Values + +**Innovation** — We invest heavily in research and development to stay ahead of industry trends. + +**Quality** — Every product undergoes rigorous testing to ensure reliability and performance. + +**Customer Focus** — Our customers are at the heart of everything we do. Their success is our success. + +**Integrity** — We conduct our business with honesty, transparency, and ethical standards. + +## Leadership Team + +Our leadership team brings decades of combined experience in software development, business management, and customer service. + +**Sarah Chen** — Chief Executive Officer (20 years in tech) +**David Okonkwo** — Chief Technology Officer (18 years in software) +**Maria Garcia** — Chief Operating Officer (15 years in business) + +## Offices + +- **London** — European headquarters +- **San Francisco** — North American operations +- **Singapore** — Asia-Pacific hub +- **Sydney** — Australasia operations diff --git a/samplesite/pages/about.nb.md b/samplesite/pages/about.nb.md new file mode 100644 index 0000000..371502f --- /dev/null +++ b/samplesite/pages/about.nb.md @@ -0,0 +1,38 @@ +--- +title: Om oss +section-id: company +sort: 100 +--- + +# Om Acme Corporation + +Acme Corporation ble grunnlagt i 1998 med en misjon å levere innovative, høykvalitets programvareløsninger til virksomheter verden over. + +## Vår historie + +Fra beskjedne begynnelser som et lite konsulentfirma, har vi vokst til et globalt teknologiselskap som betjener over 5000 kunder i 50 land. + +## Våre verdier + +**Innovasjon** — Vi investerer tungt i forskning og utvikling for å holde oss fremme i bransjen. + +**Kvalitet** — Hvert produkt gjennomgår streng testing for å sikre pålitelighet og ytelse. + +**Kundefokus** — Våre kunder er hjertepunktet i alt vi gjør. Deres suksess er vår suksess. + +**Integritet** — Vi driver vår virksomhet med ærlighet, transparens og etiske standarder. + +## Ledergruppe + +Ledergruppen vår har tiår med kombinert erfaring innen programvareutvikling, forretningsledelse og kundeservice. + +**Sarah Chen** — Administrerende direktør (20 år innen teknologi) +**David Okonkwo** — Teknologidirektør (18 år innen programvare) +**Maria Garcia** — Driftsdirektør (15 år innen forretning) + +## Kontorer + +- **London** — Europeisk hovedkontor +- **San Francisco** — Nordamerikansk drift +- **Singapore** — Asia-Stillehavs-hub +- **Sydney** — Australasia-drift diff --git a/samplesite/pages/blog.md b/samplesite/pages/blog.md new file mode 100644 index 0000000..e43716e --- /dev/null +++ b/samplesite/pages/blog.md @@ -0,0 +1,23 @@ +--- +title: Blog +section-id: blog +sort: 110 +--- + +# Latest News + +Stay up to date with announcements, product updates, and industry insights from the Acme Corporation team. + +## All Posts + +```mdcms +posts-date-reversechronological-byyear +limit: all +defaultyear: current +selectyear: yes +paginate: no +``` + +## Older Posts + +For posts from previous years, use the year selector above to browse our full archive. diff --git a/samplesite/pages/blog.nb.md b/samplesite/pages/blog.nb.md new file mode 100644 index 0000000..23c1f01 --- /dev/null +++ b/samplesite/pages/blog.nb.md @@ -0,0 +1,23 @@ +--- +title: Blogg +section-id: blog +sort: 110 +--- + +# Siste nyheter + +Bli oppdatert med kunngjøringer, produktoppdateringer og bransjeinnsikter fra Acme Corporation-teamet. + +## Alle innlegg + +```mdcms +posts-date-reversechronological-byyear +limit: all +defaultyear: current +selectyear: yes +paginate: no +``` + +## Eldre innlegg + +For innlegg fra tidligere år, bruk årvalgeren ovenfor for å bla gjennom vårt komplette arkiv. diff --git a/samplesite/pages/home.ar.md b/samplesite/pages/home.ar.md new file mode 100644 index 0000000..7b14fe1 --- /dev/null +++ b/samplesite/pages/home.ar.md @@ -0,0 +1,18 @@ +--- +title: الرئيسية +sort: 100 +--- + +# أهلا وسهلا بك في Acme Corporation + +تقدم شركة Acme Corporation منتجات عالية الجودة منذ عام 1998. نتخصص في حلول مبتكرة للأعمال الحديثة. + +## أحدث الأخبار + +تتضمن مدونتنا أحدث التحديثات والإعلانات عن المنتجات والرؤى الصناعية. تفضل بزيارتنا بانتظام لقراءة منشورات جديدة. + +## الأقسام المميزة + +- **المنتجات** — اكتشف نطاق منتجاتنا الكامل +- **المدونة** — اقرأ أحدث الأخبار والإعلانات +- **الشركة** — تعرف على تاريخنا وقيمنا diff --git a/samplesite/pages/home.md b/samplesite/pages/home.md new file mode 100644 index 0000000..e701a67 --- /dev/null +++ b/samplesite/pages/home.md @@ -0,0 +1,18 @@ +--- +title: Home +sort: 100 +--- + +# Welcome to Acme Corporation + +Acme Corporation has been delivering quality products since 1998. We specialise in innovative solutions for modern business. + +## Latest News + +Our blog features the latest updates, product announcements, and industry insights. Check back regularly for new posts. + +## Featured Sections + +- **Products** — Explore our full range of offerings +- **Blog** — Read the latest news and announcements +- **Company** — Learn about our history and values diff --git a/samplesite/pages/home.nb.md b/samplesite/pages/home.nb.md new file mode 100644 index 0000000..ac22346 --- /dev/null +++ b/samplesite/pages/home.nb.md @@ -0,0 +1,18 @@ +--- +title: Hjem +sort: 100 +--- + +# Velkommen til Acme Corporation + +Acme Corporation har levert kvalitetsprodukter siden 1998. Vi spesialiserer oss på innovative løsninger for moderne næringsliv. + +## Siste nyheter + +Bloggen vår inneholder de siste oppdateringene, produktvarslinger og innsikter fra bransjen. Besøk oss igjen regelmessig for nye innlegg. + +## Utvalgte seksjoner + +- **Produkter** — Utforsk vårt fullstendige produktutvalg +- **Blogg** — Les de siste nyhetene og varslinger +- **Selskap** — Lær om vår historie og verdier diff --git a/samplesite/pages/products.md b/samplesite/pages/products.md new file mode 100644 index 0000000..371c00e --- /dev/null +++ b/samplesite/pages/products.md @@ -0,0 +1,30 @@ +--- +title: Products +section-id: products +sort: 100 +--- + +# Our Products + +Acme Corporation offers a comprehensive range of products designed to meet the needs of modern businesses. + +## Product Categories + +### Enterprise Solutions +Our flagship enterprise platform provides integrated tools for project management, collaboration, and analytics. Suitable for organisations of all sizes. + +**Key features:** +- Real-time collaboration +- Advanced analytics dashboard +- Custom integrations +- 24/7 support + +### Professional Services +We provide consulting, implementation, and training services to ensure smooth deployment and maximum ROI. + +### Support and Maintenance +Annual support packages include software updates, security patches, and technical assistance. + +## Get in Touch + +Contact our sales team to schedule a demo or discuss your organisation's specific needs. diff --git a/samplesite/pages/products.nb.md b/samplesite/pages/products.nb.md new file mode 100644 index 0000000..ae0ff54 --- /dev/null +++ b/samplesite/pages/products.nb.md @@ -0,0 +1,30 @@ +--- +title: Produkter +section-id: products +sort: 100 +--- + +# Våre produkter + +Acme Corporation tilbyr et omfattende utvalg av produkter designet for å møte behovene til moderne virksomheter. + +## Produktkategorier + +### Enterprise-løsninger +Vår flaggskip-plattform for enterprise gir integrerte verktøy for prosjektstyring, samarbeid og analyse. Egnet for organisasjoner av alle størrelser. + +**Viktige funksjoner:** +- Sanntidssamarbeid +- Avansert analysedashbord +- Egendefinerte integrasjoner +- 24/7 support + +### Profesjonelle tjenester +Vi tilbyr konsultering, implementering og treningsgjeld for å sikre problemfri distribusjon og maksimal avkastning. + +### Støtte og vedlikehold +Årlige støttepakker inkluderer programvareoppdateringer, sikkerhetsoppdateringer og teknisk assistanse. + +## Ta kontakt + +Kontakt saltesget vårt for å planlegge en demonstrasjon eller diskutere organisasjonens spesifikke behov. diff --git a/samplesite/posts/2022-q4-report.md b/samplesite/posts/2022-q4-report.md new file mode 100644 index 0000000..c24a474 --- /dev/null +++ b/samplesite/posts/2022-q4-report.md @@ -0,0 +1,23 @@ +--- +title: Q4 2022 Performance Report +date: 2022-11-15 +datetime: 2022-11-15 09:00 +author: Sarah Chen +--- + +# Q4 2022 Performance Report + +We're pleased to report strong performance across all metrics in the fourth quarter of 2022. + +## Key Highlights + +- Revenue growth of 28% year-over-year +- Customer satisfaction score of 4.7/5.0 +- 312 new enterprise customers onboarded +- 99.98% platform uptime + +## Investment in Innovation + +We've doubled our R&D investment this year, focusing on AI-driven analytics and workflow automation. + +Read our full 2022 annual report for comprehensive details. diff --git a/samplesite/posts/2023-analytics-launch.md b/samplesite/posts/2023-analytics-launch.md new file mode 100644 index 0000000..96dd7d6 --- /dev/null +++ b/samplesite/posts/2023-analytics-launch.md @@ -0,0 +1,27 @@ +--- +title: Introducing Advanced Analytics Dashboard +date: 2023-03-22 +datetime: 2023-03-22 14:30 +author: David Okonkwo +--- + +# Introducing Advanced Analytics Dashboard + +We are excited to announce the launch of our new Advanced Analytics Dashboard, available now to all Enterprise customers. + +## What's New + +The dashboard provides real-time insights into user behaviour, system performance, and business metrics. Features include: + +- Custom metric builders +- Automated alerting +- Data export in multiple formats +- Integration with popular BI tools + +## How to Get Started + +Existing Enterprise customers can enable the new dashboard in their account settings. Contact support for any questions. + +## What's Next + +We're planning further enhancements based on user feedback, including mobile support and advanced forecasting. diff --git a/samplesite/posts/2023-security-update.md b/samplesite/posts/2023-security-update.md new file mode 100644 index 0000000..32e5169 --- /dev/null +++ b/samplesite/posts/2023-security-update.md @@ -0,0 +1,31 @@ +--- +title: Security Update - November 2023 +date: 2023-11-10 +datetime: 2023-11-10 11:45 +author: Security Team +--- + +# Security Update — November 2023 + +All Acme Corporation customers should update to the latest version immediately to apply critical security patches. + +## Affected Versions + +- Version 7.2.0 through 7.2.3 +- Version 8.0.0 through 8.1.2 + +## What to Do + +1. Log in to your account dashboard +2. Navigate to Settings → Software Updates +3. Click "Update Now" + +The update takes approximately 5 minutes and requires no downtime. + +## What Was Fixed + +Our security team identified and resolved three vulnerabilities related to API authentication. We have notified all affected customers directly. + +## Questions? + +Contact our support team at security@acmecorp.com for any questions or concerns. diff --git a/samplesite/posts/2024-roadmap.md b/samplesite/posts/2024-roadmap.md new file mode 100644 index 0000000..b082033 --- /dev/null +++ b/samplesite/posts/2024-roadmap.md @@ -0,0 +1,32 @@ +--- +title: 2024 Product Roadmap +date: 2024-01-30 +datetime: 2024-01-30 10:00 +author: David Okonkwo +--- + +# 2024 Product Roadmap + +We're thrilled to share our ambitious plans for 2024. These updates are based on customer feedback and industry trends. + +## H1 2024 + +- Mobile app (iOS and Android) +- Advanced workflow automation +- Expanded API capabilities +- Support for 15 additional languages + +## H2 2024 + +- AI-powered insights engine +- Blockchain-based audit trails +- Advanced multi-tenancy +- Sustainability reporting module + +## Customer Advisory Board + +We're establishing a Customer Advisory Board to help guide our product direction. Interested in joining? Contact us. + +## Schedule a Demo + +Want to see what's coming? Schedule a demo with our product team to discuss how these features will benefit your organisation. diff --git a/samplesite/posts/2024-success-stories.md b/samplesite/posts/2024-success-stories.md new file mode 100644 index 0000000..69be3fb --- /dev/null +++ b/samplesite/posts/2024-success-stories.md @@ -0,0 +1,28 @@ +--- +title: Q2 2024 Customer Success Stories +date: 2024-07-08 +datetime: 2024-07-08 15:20 +author: Maria Garcia +--- + +# Q2 2024 Customer Success Stories + +This quarter we highlight three customers who have achieved remarkable results using Acme Corporation products. + +## Global Manufacturing Group + +A multinational manufacturing company reduced project delivery time by 35% and improved team collaboration across 12 global offices using our Enterprise platform. + +"Acme's solution transformed how we collaborate. We now complete projects weeks ahead of schedule." — Operations Director + +## Healthcare Network + +A regional healthcare network implemented our analytics dashboard to track patient outcomes and operational metrics, resulting in improved patient care and 18% cost reduction. + +## Professional Services Firm + +A 500-person consulting firm used our workflow automation to standardise client engagement processes, enabling 40% faster project initiation. + +## Apply to Be Featured + +Is your organisation achieving great results with Acme Corporation? We'd love to feature your story. Contact marketing@acmecorp.com. diff --git a/samplesite/posts/2026-v9-release.md b/samplesite/posts/2026-v9-release.md new file mode 100644 index 0000000..63dfec1 --- /dev/null +++ b/samplesite/posts/2026-v9-release.md @@ -0,0 +1,34 @@ +--- +title: Version 9.0 Released — A New Era +date: 2026-04-10 +datetime: 2026-04-10 13:00 +author: Sarah Chen +--- + +# Version 9.0 Released — A New Era + +Today we launch Acme Corporation Platform v9.0, our most significant release to date. + +## Major Features + +**AI-Powered Insights** — Automatic anomaly detection and predictive analytics using advanced machine learning models. + +**Universal Integration** — Connect to 500+ enterprise systems with our new integration marketplace. + +**Enhanced Security** — End-to-end encryption, zero-trust architecture, and compliance with latest standards. + +**Performance** — 3x faster than v8.0, with new caching strategies and database optimisations. + +**Global Scale** — Now available in 45 regions worldwide with sub-100ms latency. + +## Upgrade Path + +Existing customers can upgrade free from v8.x. Migration typically takes less than 1 hour. + +## Community Edition + +We're also releasing a free Community Edition for non-profit organisations, startups, and open source projects. + +## Thank You + +Thank you to our customers, partners, and community for helping us reach this milestone. diff --git a/samplesite/search.json b/samplesite/search.json new file mode 100644 index 0000000..e593439 --- /dev/null +++ b/samplesite/search.json @@ -0,0 +1,197 @@ +[ + { + "file": "pages/about.md", + "title": "About", + "section-id": "company", + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# About Acme Corporation\n\nAcme Corporation was founded in 1998 with a mission to deliver innovative, high-quality software solutions to businesses worldwide.\n\n## Our Story\n\nFrom humble beginnings as a small consulting firm, we have grown into a global technology company serving over 5,000 customers across 50 countries.\n\n## Our Values\n\n**Innovation** — We invest heavily in research and development to stay ahead of industry trends.\n\n**Quality** — Every product undergoes rigorous testing to ensure reliability and performance.\n\n**Customer Focus** — Our customers are at the heart of everything we do. Their success is our success.\n\n**Integrity** — We conduct our business with honesty, transparency, and ethical standards.\n\n## Leadership Team\n\nOur leadership team brings decades of combined experience in software development, business management, and customer service.\n\n**Sarah Chen** — Chief Executive Officer (20 years in tech)\n**David Okonkwo** — Chief Technology Officer (18 years in software)\n**Maria Garcia** — Chief Operating Officer (15 years in business)\n\n## Offices\n\n- **London** — European headquarters\n- **San Francisco** — North American operations\n- **Singapore** — Asia-Pacific hub\n- **Sydney** — Australasia operations\n", + "category": "en" + }, + { + "file": "pages/about.md", + "title": "Om oss", + "section-id": "company", + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Om Acme Corporation\n\nAcme Corporation ble grunnlagt i 1998 med en misjon å levere innovative, høykvalitets programvareløsninger til virksomheter verden over.\n\n## Vår historie\n\nFra beskjedne begynnelser som et lite konsulentfirma, har vi vokst til et globalt teknologiselskap som betjener over 5000 kunder i 50 land.\n\n## Våre verdier\n\n**Innovasjon** — Vi investerer tungt i forskning og utvikling for å holde oss fremme i bransjen.\n\n**Kvalitet** — Hvert produkt gjennomgår streng testing for å sikre pålitelighet og ytelse.\n\n**Kundefokus** — Våre kunder er hjertepunktet i alt vi gjør. Deres suksess er vår suksess.\n\n**Integritet** — Vi driver vår virksomhet med ærlighet, transparens og etiske standarder.\n\n## Ledergruppe\n\nLedergruppen vår har tiår med kombinert erfaring innen programvareutvikling, forretningsledelse og kundeservice.\n\n**Sarah Chen** — Administrerende direktør (20 år innen teknologi)\n**David Okonkwo** — Teknologidirektør (18 år innen programvare)\n**Maria Garcia** — Driftsdirektør (15 år innen forretning)\n\n## Kontorer\n\n- **London** — Europeisk hovedkontor\n- **San Francisco** — Nordamerikansk drift\n- **Singapore** — Asia-Stillehavs-hub\n- **Sydney** — Australasia-drift\n", + "category": "nb" + }, + { + "file": "pages/blog.md", + "title": "Blog", + "section-id": "blog", + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Latest News\n\nStay up to date with announcements, product updates, and industry insights from the Acme Corporation team.\n\n## All Posts\n\n```mdcms\nposts-date-reversechronological-byyear\nlimit: all\ndefaultyear: current\nselectyear: yes\npaginate: no\n```\n\n## Older Posts\n\nFor posts from previous years, use the year selector above to browse our full archive.\n", + "category": "en" + }, + { + "file": "pages/blog.md", + "title": "Blogg", + "section-id": "blog", + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Siste nyheter\n\nBli oppdatert med kunngjøringer, produktoppdateringer og bransjeinnsikter fra Acme Corporation-teamet.\n\n## Alle innlegg\n\n```mdcms\nposts-date-reversechronological-byyear\nlimit: all\ndefaultyear: current\nselectyear: yes\npaginate: no\n```\n\n## Eldre innlegg\n\nFor innlegg fra tidligere år, bruk årvalgeren ovenfor for å bla gjennom vårt komplette arkiv.\n", + "category": "nb" + }, + { + "file": "pages/home.md", + "title": "الرئيسية", + "section-id": null, + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# أهلا وسهلا بك في Acme Corporation\n\nتقدم شركة Acme Corporation منتجات عالية الجودة منذ عام 1998. نتخصص في حلول مبتكرة للأعمال الحديثة.\n\n## أحدث الأخبار\n\nتتضمن مدونتنا أحدث التحديثات والإعلانات عن المنتجات والرؤى الصناعية. تفضل بزيارتنا بانتظام لقراءة منشورات جديدة.\n\n## الأقسام المميزة\n\n- **المنتجات** — اكتشف نطاق منتجاتنا الكامل\n- **المدونة** — اقرأ أحدث الأخبار والإعلانات\n- **الشركة** — تعرف على تاريخنا وقيمنا\n", + "category": "ar" + }, + { + "file": "pages/home.md", + "title": "Home", + "section-id": null, + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Welcome to Acme Corporation\n\nAcme Corporation has been delivering quality products since 1998. We specialise in innovative solutions for modern business.\n\n## Latest News\n\nOur blog features the latest updates, product announcements, and industry insights. Check back regularly for new posts.\n\n## Featured Sections\n\n- **Products** — Explore our full range of offerings\n- **Blog** — Read the latest news and announcements\n- **Company** — Learn about our history and values\n", + "category": "en" + }, + { + "file": "pages/home.md", + "title": "Hjem", + "section-id": null, + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Velkommen til Acme Corporation\n\nAcme Corporation har levert kvalitetsprodukter siden 1998. Vi spesialiserer oss på innovative løsninger for moderne næringsliv.\n\n## Siste nyheter\n\nBloggen vår inneholder de siste oppdateringene, produktvarslinger og innsikter fra bransjen. Besøk oss igjen regelmessig for nye innlegg.\n\n## Utvalgte seksjoner\n\n- **Produkter** — Utforsk vårt fullstendige produktutvalg\n- **Blogg** — Les de siste nyhetene og varslinger\n- **Selskap** — Lær om vår historie og verdier\n", + "category": "nb" + }, + { + "file": "pages/products.md", + "title": "Products", + "section-id": "products", + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Our Products\n\nAcme Corporation offers a comprehensive range of products designed to meet the needs of modern businesses.\n\n## Product Categories\n\n### Enterprise Solutions\nOur flagship enterprise platform provides integrated tools for project management, collaboration, and analytics. Suitable for organisations of all sizes.\n\n**Key features:**\n- Real-time collaboration\n- Advanced analytics dashboard\n- Custom integrations\n- 24/7 support\n\n### Professional Services\nWe provide consulting, implementation, and training services to ensure smooth deployment and maximum ROI.\n\n### Support and Maintenance\nAnnual support packages include software updates, security patches, and technical assistance.\n\n## Get in Touch\n\nContact our sales team to schedule a demo or discuss your organisation's specific needs.\n", + "category": "en" + }, + { + "file": "pages/products.md", + "title": "Produkter", + "section-id": "products", + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Våre produkter\n\nAcme Corporation tilbyr et omfattende utvalg av produkter designet for å møte behovene til moderne virksomheter.\n\n## Produktkategorier\n\n### Enterprise-løsninger\nVår flaggskip-plattform for enterprise gir integrerte verktøy for prosjektstyring, samarbeid og analyse. Egnet for organisasjoner av alle størrelser.\n\n**Viktige funksjoner:**\n- Sanntidssamarbeid\n- Avansert analysedashbord\n- Egendefinerte integrasjoner\n- 24/7 support\n\n### Profesjonelle tjenester\nVi tilbyr konsultering, implementering og treningsgjeld for å sikre problemfri distribusjon og maksimal avkastning.\n\n### Støtte og vedlikehold\nÅrlige støttepakker inkluderer programvareoppdateringer, sikkerhetsoppdateringer og teknisk assistanse.\n\n## Ta kontakt\n\nKontakt saltesget vårt for å planlegge en demonstrasjon eller diskutere organisasjonens spesifikke behov.\n", + "category": "nb" + }, + { + "file": "posts/2022-q4-report.md", + "title": "Q4 2022 Performance Report", + "section-id": null, + "keywords": "", + "description": "", + "author": "Sarah Chen", + "date": "2022-11-15", + "datetime": "2022-11-15 09:00", + "language": "en", + "body": "# Q4 2022 Performance Report\n\nWe're pleased to report strong performance across all metrics in the fourth quarter of 2022.\n\n## Key Highlights\n\n- Revenue growth of 28% year-over-year\n- Customer satisfaction score of 4.7/5.0\n- 312 new enterprise customers onboarded\n- 99.98% platform uptime\n\n## Investment in Innovation\n\nWe've doubled our R&D investment this year, focusing on AI-driven analytics and workflow automation.\n\nRead our full 2022 annual report for comprehensive details.\n", + "category": "en" + }, + { + "file": "posts/2023-analytics-launch.md", + "title": "Introducing Advanced Analytics Dashboard", + "section-id": null, + "keywords": "", + "description": "", + "author": "David Okonkwo", + "date": "2023-03-22", + "datetime": "2023-03-22 14:30", + "language": "en", + "body": "# Introducing Advanced Analytics Dashboard\n\nWe are excited to announce the launch of our new Advanced Analytics Dashboard, available now to all Enterprise customers.\n\n## What's New\n\nThe dashboard provides real-time insights into user behaviour, system performance, and business metrics. Features include:\n\n- Custom metric builders\n- Automated alerting\n- Data export in multiple formats\n- Integration with popular BI tools\n\n## How to Get Started\n\nExisting Enterprise customers can enable the new dashboard in their account settings. Contact support for any questions.\n\n## What's Next\n\nWe're planning further enhancements based on user feedback, including mobile support and advanced forecasting.\n", + "category": "en" + }, + { + "file": "posts/2023-security-update.md", + "title": "Security Update - November 2023", + "section-id": null, + "keywords": "", + "description": "", + "author": "Security Team", + "date": "2023-11-10", + "datetime": "2023-11-10 11:45", + "language": "en", + "body": "# Security Update — November 2023\n\nAll Acme Corporation customers should update to the latest version immediately to apply critical security patches.\n\n## Affected Versions\n\n- Version 7.2.0 through 7.2.3\n- Version 8.0.0 through 8.1.2\n\n## What to Do\n\n1. Log in to your account dashboard\n2. Navigate to Settings → Software Updates\n3. Click \"Update Now\"\n\nThe update takes approximately 5 minutes and requires no downtime.\n\n## What Was Fixed\n\nOur security team identified and resolved three vulnerabilities related to API authentication. We have notified all affected customers directly.\n\n## Questions?\n\nContact our support team at security@acmecorp.com for any questions or concerns.\n", + "category": "en" + }, + { + "file": "posts/2024-roadmap.md", + "title": "2024 Product Roadmap", + "section-id": null, + "keywords": "", + "description": "", + "author": "David Okonkwo", + "date": "2024-01-30", + "datetime": "2024-01-30 10:00", + "language": "en", + "body": "# 2024 Product Roadmap\n\nWe're thrilled to share our ambitious plans for 2024. These updates are based on customer feedback and industry trends.\n\n## H1 2024\n\n- Mobile app (iOS and Android)\n- Advanced workflow automation\n- Expanded API capabilities\n- Support for 15 additional languages\n\n## H2 2024\n\n- AI-powered insights engine\n- Blockchain-based audit trails\n- Advanced multi-tenancy\n- Sustainability reporting module\n\n## Customer Advisory Board\n\nWe're establishing a Customer Advisory Board to help guide our product direction. Interested in joining? Contact us.\n\n## Schedule a Demo\n\nWant to see what's coming? Schedule a demo with our product team to discuss how these features will benefit your organisation.\n", + "category": "en" + }, + { + "file": "posts/2024-success-stories.md", + "title": "Q2 2024 Customer Success Stories", + "section-id": null, + "keywords": "", + "description": "", + "author": "Maria Garcia", + "date": "2024-07-08", + "datetime": "2024-07-08 15:20", + "language": "en", + "body": "# Q2 2024 Customer Success Stories\n\nThis quarter we highlight three customers who have achieved remarkable results using Acme Corporation products.\n\n## Global Manufacturing Group\n\nA multinational manufacturing company reduced project delivery time by 35% and improved team collaboration across 12 global offices using our Enterprise platform.\n\n\"Acme's solution transformed how we collaborate. We now complete projects weeks ahead of schedule.\" — Operations Director\n\n## Healthcare Network\n\nA regional healthcare network implemented our analytics dashboard to track patient outcomes and operational metrics, resulting in improved patient care and 18% cost reduction.\n\n## Professional Services Firm\n\nA 500-person consulting firm used our workflow automation to standardise client engagement processes, enabling 40% faster project initiation.\n\n## Apply to Be Featured\n\nIs your organisation achieving great results with Acme Corporation? We'd love to feature your story. Contact marketing@acmecorp.com.\n", + "category": "en" + }, + { + "file": "posts/2026-v9-release.md", + "title": "Version 9.0 Released — A New Era", + "section-id": null, + "keywords": "", + "description": "", + "author": "Sarah Chen", + "date": "2026-04-10", + "datetime": "2026-04-10 13:00", + "language": "en", + "body": "# Version 9.0 Released — A New Era\n\nToday we launch Acme Corporation Platform v9.0, our most significant release to date.\n\n## Major Features\n\n**AI-Powered Insights** — Automatic anomaly detection and predictive analytics using advanced machine learning models.\n\n**Universal Integration** — Connect to 500+ enterprise systems with our new integration marketplace.\n\n**Enhanced Security** — End-to-end encryption, zero-trust architecture, and compliance with latest standards.\n\n**Performance** — 3x faster than v8.0, with new caching strategies and database optimisations.\n\n**Global Scale** — Now available in 45 regions worldwide with sub-100ms latency.\n\n## Upgrade Path\n\nExisting customers can upgrade free from v8.x. Migration typically takes less than 1 hour.\n\n## Community Edition\n\nWe're also releasing a free Community Edition for non-profit organisations, startups, and open source projects.\n\n## Thank You\n\nThank you to our customers, partners, and community for helping us reach this milestone.\n", + "category": "en" + } +] \ No newline at end of file diff --git a/website/assets/fonts/.gitkeep b/website/assets/fonts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/website/assets/images/.gitkeep b/website/assets/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/website/config.yml b/website/config.yml new file mode 100644 index 0000000..6c32f19 --- /dev/null +++ b/website/config.yml @@ -0,0 +1,50 @@ +# MD-CMS v0.2 — Site configuration +# +# Only `sitename` and `navigation` are required. Uncomment and edit the rest +# as needed. See https://kbenestad.codeberg.page/md-cms for the full reference. +# +# LICENCE +# Copyright 2026 Kristian Benestad | kbenestad.codeberg.page +# +# Licensed under the Apache License, Version 2.0 (the "Licence"); +# you may not use this file except in compliance with the Licence. +# You may obtain a copy of the Licence at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the Licence is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +# ────────────────────────────────── +# Site identity +# ────────────────────────────────── +sitename: MD-CMS New Site +navigation: topbar # sidebar | topbar + +# homepage: pages/home.md # override the default landing page + +# sitedescription: A short description for meta tags +# logo: logo.svg # filename in assets/images/ +# favicon: favicon.png +# footer: "© 2026 Your Name" + +# ────────────────────────────────── +# Typography (optional) +# ────────────────────────────────── +# font-title: "Inter:700" +# font-body: Inter +# font-code: JetBrains Mono + +# ────────────────────────────────── +# Layout (optional) +# ────────────────────────────────── +# main-width: 80em +# nav-width: 20em +# nav-position: left # left | right (sidebar mode) + +# ────────────────────────────────── +# Features (optional) +# ────────────────────────────────── +# search: true +# default-theme: system # light | dark | system diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..ec16766 --- /dev/null +++ b/website/index.html @@ -0,0 +1,2328 @@ + + + + + + +MD-CMS + + + + + + + + + + + + + + + + +
+ + + + + + diff --git a/website/nav.yml b/website/nav.yml new file mode 100644 index 0000000..68a0854 --- /dev/null +++ b/website/nav.yml @@ -0,0 +1,14 @@ +# nav.yml — generated by mdcms.py +# Manual edits to section metadata (defaultname, sort, parent, parent-sort, +# pagesvisibility, categorynames) are preserved on rebuild. New sections +# are auto-created from page frontmatter section-id values. + +sections: + # (none yet — add section-id to page frontmatter to auto-create) +pages: + - file: pages/home.md + title: Home + sort: 100 + variants: [en] + titles: + en: Home diff --git a/website/pages/home.md b/website/pages/home.md new file mode 100644 index 0000000..5b41f1f --- /dev/null +++ b/website/pages/home.md @@ -0,0 +1,67 @@ +--- +title: Home +sort: 100 +--- + +# MD-CMS + +This is the default startpage for MD-CMS. + +## Testing MD-CMS + +If you want to test `MD-CMS` you can grab `samplesite` from the repo and place the content in your website root. This page (`pages/home.md`) won't be replaced. + +**Post listing tests** below contains various custom tags to display posts. There are no posts now, but if you download the `samplesite` it will fetch the posts in + +## Post listing tests + +## Reverse chronological (newest first) + +```mdcms +posts-date-reversechronological +limit: 3 +paginate: no +``` + +## Chronological (oldest first) + +```mdcms +posts-date-chronological +limit: all +paginate: none +``` + +## By year (date, reverse chrono) + +```mdcms +posts-date-reversechronological-byyear +limit: all +defaultyear: current +selectyear: yes +paginate: none +``` + +## By year+month (datetime, chrono) + +```mdcms +posts-datetime-chronological-byyearmonth +limit: all +defaultyear: 2024 +selectyear: yes +``` + +## Last 30 days + +```mdcms +posts-date-reversechronological-lastmonth +limit: all +paginate: none +``` + +## Paginated (2 per page) + +```mdcms +posts-datetime-reversechronological +limit: 2 +paginate: yes +``` diff --git a/website/posts/.gitkeep b/website/posts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/website/search.json b/website/search.json new file mode 100644 index 0000000..5ec1f8a --- /dev/null +++ b/website/search.json @@ -0,0 +1,15 @@ +[ + { + "file": "pages/home.md", + "title": "Home", + "section-id": null, + "keywords": "", + "description": "", + "author": null, + "date": "", + "datetime": "", + "language": "en", + "body": "# Post Listing Tests\n\n## Reverse chronological (newest first)\n\n```mdcms\nposts-date-reversechronological\nlimit: 3\npaginate: no\n```\n\n## Chronological (oldest first)\n\n```mdcms\nposts-date-chronological\nlimit: all\npaginate: none\n```\n\n## By year (date, reverse chrono)\n\n```mdcms\nposts-date-reversechronological-byyear\nlimit: all\ndefaultyear: current\nselectyear: yes\npaginate: none\n```\n\n## By year+month (datetime, chrono)\n\n```mdcms\nposts-datetime-chronological-byyearmonth\nlimit: all\ndefaultyear: 2024\nselectyear: yes\n```\n\n## Last 30 days\n\n```mdcms\nposts-date-reversechronological-lastmonth\nlimit: all\npaginate: none\n```\n\n## Paginated (2 per page)\n\n```mdcms\nposts-datetime-reversechronological\nlimit: 2\npaginate: yes\n```\n", + "category": "en" + } +] \ No newline at end of file