From 75e32a80bf7392b4f5361a4e59cb98102b811b5e Mon Sep 17 00:00:00 2001
From: Florian
Date: Fri, 2 Oct 2026 15:19:13 +0200
Subject: [PATCH 1/8] readme
---
readme.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/readme.md b/readme.md
index e69de29..c292ff9 100644
--- a/readme.md
+++ b/readme.md
@@ -0,0 +1 @@
+sdfkjsdkjhfakjshfd
From f2942df1b96c0f8983f900c30304141d93c8475e Mon Sep 17 00:00:00 2001
From: Florian
Date: Sat, 3 Oct 2026 08:00:50 +0200
Subject: [PATCH 2/8] image folder
---
index.html | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 index.html
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e69de29
From 45470f8cd92ed47e083ee762d803f50f07fdaa0e Mon Sep 17 00:00:00 2001
From: Florian
Date: Sat, 3 Oct 2026 10:27:28 +0200
Subject: [PATCH 3/8] =?UTF-8?q?readme=20ver=C3=A4ndert?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.DS_Store | Bin 0 -> 6148 bytes
images/logo.svg | 148 ++++++++++++++++++++++++++++++++++++++++++++++++
readme.md | 2 +
3 files changed, 150 insertions(+)
create mode 100644 .DS_Store
create mode 100644 images/logo.svg
diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..dc8b8565f60c1108e96f7e8e6aa716aba776c692
GIT binary patch
literal 6148
zcmeHK%}T>S5Z-O8-BN@c6g@6@Em+$k7B3;z7cim+m735}gK4%jsXdfJuKGg0iO=KA
z?&c6I9!2a7?0&QJvzz%K`@UY{L+3TLI*9~!Sd~$I;e2J%tdNVw7pj^wI#RA?zDV6l<&y!ds6R;OKMJypPKnxHA
z#K0CZU=IV)+``(aWMY6A_?ZFRA0#wH*J5r^ZynI#^%?y&L=@2RErBR4x)yVT-~r(#
z70{$|`^4ZT9qiJ^xfXMSCY^D;GR$LFE*>vjuMT#p!x?uCQcnyJ17!xPX6WGge+9qH
z#z%fRg+{~xG4RhA;I)B2@SrGrwtg!Q&squX2^tE<<*0ywUb+Op0QZq2?KFOgI>fma
WbAvbw+I2c0T?8~C)DZ)}z`z%zmrL9L
literal 0
HcmV?d00001
diff --git a/images/logo.svg b/images/logo.svg
new file mode 100644
index 0000000..9a59969
--- /dev/null
+++ b/images/logo.svg
@@ -0,0 +1,148 @@
+
+
+
+
diff --git a/readme.md b/readme.md
index c292ff9..1a6195c 100644
--- a/readme.md
+++ b/readme.md
@@ -1 +1,3 @@
+eine änderung hier mal.
+
sdfkjsdkjhfakjshfd
From fac4f10062f04866fb626b79063b1d459160cc5b Mon Sep 17 00:00:00 2001
From: Florian
Date: Sat, 3 Oct 2026 10:47:29 +0200
Subject: [PATCH 4/8] cms begonnen zu bauen
---
.DS_Store | Bin 6148 -> 12292 bytes
ascii/ascii-logo.md | 28 +++
cms.py | 482 +++++++++++++++++++++++++++++++++++++++++
css/style.css | 1 +
md/footer.md | 6 +
md/startseite.md | 5 +
output/css/style.css | 1 +
output/index.html | 20 ++
start.command | 4 +
start.sh | 4 +
templates/default.html | 20 ++
11 files changed, 571 insertions(+)
create mode 100644 ascii/ascii-logo.md
create mode 100755 cms.py
create mode 100644 css/style.css
create mode 100644 md/footer.md
create mode 100644 md/startseite.md
create mode 100644 output/css/style.css
create mode 100644 output/index.html
create mode 100755 start.command
create mode 100755 start.sh
create mode 100644 templates/default.html
diff --git a/.DS_Store b/.DS_Store
index dc8b8565f60c1108e96f7e8e6aa716aba776c692..f912345d14e1a99293a46850160b35b11351f22d 100644
GIT binary patch
literal 12292
zcmeGiTWl0n^qy%cGj@P!El-*3*79hu&?mJ8!R@xZq!naa`k+{McZPQC>`dL6-2zQb
zjqw4RXhe<1590H)QKA83Q1pugAJH@^rNv6U9;?(n$y4)6wWA`$d=2;-@HKF!X}}F0d``T-)70S0>ubQ*KpqXy
z??VtPg&`MCFz9?bu%TOMI!?FHPQgB&0~}h=g&`MCFzB!%kHdSPQZOKJ-E;U&eIVF5y1i>2l$Pz0bc`oG(cm2*4bCL(yi?5TM)^dea~dh
zzQSF_;w-tlr?*e+@6qC>5z+Rm1JWD|55QXx-JVp_c<}9xn|dl?SOZc?JgUc2del~1
zbe&E{+8isMGsb
zY1>BVGS*HtX(6l#)i5!I$=UM*ODb2^-Ty#n!Wx}))lf8Jun$k+hOO<<)TB@XRYIo`O{i0)3l=WAXR$2j&d#jTlZt6iq%?byOVaC_9U
zw~eSq=9R+oW}fe}hN4Mz+bAX~G92wZ@BFYxrz{&3%{G&cHXbgQ$RlY+TEo-`{uV4l!dGPMt|GL8bt_H`pL2SSjs==P@)jBlWU`Dj;m5y|
z>>(z3io8e;laI-9@;Uj2oF-?ZD{d<);f
zS@;dk!|!kbF2QB^i!0$4aZ9+R+%m43tKsUndTt}PiQCTgaxrd@8)9N`Zj>Usy<%aw
z+=qCsyNxh+<0kXFTVT`XEf0z9x4gUkIGZ}y-DPv<%?~bJQQgqAuH}juGONuPs@DM=
zo6qtT$L1Zo$hBAIiDIA&okHC^okL19sq^wvbawf@VnCXKZo|-J%MmR?mtp9ARe^xS
zp{FvmrZyl-1?Y!@!@If)QE-vwHKHgLqDQi^^)jNwMZ&GP4D~Pm-?sGc$r*BvoF^B^
zMJR$|kRSk+unHQ`vNytJwC;B3K&_!xwM@zI5z<3Vwp0;g=js?#Q-exR7hf=}6Mh
zO=A%0)cQG0rz&}pj}ARywzatlR{qFaZyRQg8KEsr
zJiF^2%XrgGOWZCP8>?@?Y;9eA{Xb-G52Bn{rp?^GfP+~w`2pDv$fa^v_5<>b3&@^N
z2)dgeko|z1f9rYc2V{>W=LBRY?D&nZfh-Nsv>Ce(c5aLR>kU+6V;ydwnpR)u2FfE*
zo-L?%)X#SK*$#hmbpx{<+0N!~t^BRk|8r~Qv14!c;?Sv8TixtM){Lt;jQlh5WHLPb
z-mTjWI{!Oy-`NUO(mQ|+)Bpd^ndCRV27C?No*IC{wn$qTWy{=Jw0nzOk+5i9m
diff --git a/ascii/ascii-logo.md b/ascii/ascii-logo.md
new file mode 100644
index 0000000..7598ce1
--- /dev/null
+++ b/ascii/ascii-logo.md
@@ -0,0 +1,28 @@
+
+
+ &&&& &&&&&
+ +xX&&XX+++x
+ &&$x+xX++x+:;
+ &;;:$&;:;X&&
+ ::::::::::;
+ &&$$$$$$$$$$$;::::x::+:;
+ &&XXX;::::::::::::::::::xxx
+ &&$x+x;::::::::::::::::::X&
+ &&X+++x;::::::::::::::::::X&
+ &&Xxxxx;::::::::::::::::::X&
+ &&&&&&+;::++;::;++;::;++:x&&
+ &$::&&X:;&&&&;:X&&::X&
+ &&xX&&$:;&&&&Xx&&&:;X&
+ &&&&&&&&& &&&& &&&&
+
+
+██╗ ██╗███████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗██╗██╗ ██████╗ ██╗ ██╗███╗ ███╗
+██║ ██║██╔════╝██╔════╝ ██╔═══██╗██╔════╝ ██╔══██╗██║ ██║██║██║ ██╔══██╗ ██║ ██║████╗ ████║
+███████║█████╗ ██║ ███╗ ██║ ██║███████╗ ██████╔╝██║ ██║██║██║ ██║ ██║ ██║ ██║██╔████╔██║
+██╔══██║██╔══╝ ██║ ██║ ██║ ██║╚════██║ ██╔══██╗██║ ██║██║██║ ██║ ██║ ╚██╗ ██╔╝██║╚██╔╝██║
+██║ ██║██║ ╚██████╔╝███████╗╚██████╔╝███████║ ██████╔╝╚██████╔╝██║███████╗██████╔╝ ╚████╔╝ ██║ ╚═╝ ██║
+╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═══╝ ╚═╝ ╚═╝
+
+DEBIAN 13 VM auf w4d-gpu unterm Schreibtisch
+
+
diff --git a/cms.py b/cms.py
new file mode 100755
index 0000000..61612d3
--- /dev/null
+++ b/cms.py
@@ -0,0 +1,482 @@
+#!/usr/bin/env python3
+"""Mini-CMS: MD-Dateien in md/ per Browseroberflaeche bearbeiten
+und als statische Seite (HTML + CSS) nach output/ exportieren.
+Nur Python-Standardbibliothek, kein Internet noetig.
+
+Aufruf:
+ python3 cms.py -> Server + UI starten (http://localhost:8500)
+ python3 cms.py export -> nur exportieren, kein Server
+ python3 cms.py port 9000 -> Server auf anderem Port
+"""
+
+import json
+import os
+import re
+import shutil
+import sys
+import threading
+import webbrowser
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from urllib.parse import unquote, urlparse
+
+ROOT = os.path.dirname(os.path.abspath(__file__))
+MD_DIR = os.path.join(ROOT, "md")
+TPL_DIR = os.path.join(ROOT, "templates")
+CSS_DIR = os.path.join(ROOT, "css")
+OUT_DIR = os.path.join(ROOT, "output")
+PORT = 8500
+
+for d in (MD_DIR, TPL_DIR, CSS_DIR, OUT_DIR):
+ os.makedirs(d, exist_ok=True)
+
+SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$", re.IGNORECASE)
+START_PAGE = "startseite"
+FOOTER_PAGE = "footer" # Baustein: wird in jede Seite eingefuegt, nicht eigenstaendig exportiert
+
+
+# ---------------------------------------------------------------- markdown
+
+def md_to_html(text):
+ """Minimaler Markdown->HTML-Konverter:
+ Ueberschriften, **fett**, *kursiv*, `code`, [Links](url),
+ Listen, Code-Bloecke, > Zitate, --- Trennlinien, Absaetze.
+ """
+ lines = text.replace("\r\n", "\n").split("\n")
+ out = []
+ i = 0
+ in_code = False
+ in_list = False
+ list_tag = None
+
+ def inline(s):
+ s = re.sub(r"`([^`]+)`", r"\1", s)
+ s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s)
+ s = re.sub(r"\*([^*]+)\*", r"\1", s)
+ s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', s)
+ return s
+
+ def close_list():
+ nonlocal in_list, list_tag
+ if in_list:
+ out.append("%s>" % list_tag)
+ in_list = False
+ list_tag = None
+
+ while i < len(lines):
+ line = lines[i]
+
+ if line.strip().startswith("```"):
+ close_list()
+ if not in_code:
+ out.append("")
+ in_code = True
+ else:
+ out.append("
")
+ in_code = False
+ i += 1
+ continue
+
+ if in_code:
+ out.append(_escape(line))
+ i += 1
+ continue
+
+ stripped = line.strip()
+
+ if not stripped:
+ close_list()
+ i += 1
+ continue
+
+ m = re.match(r"^(#{1,6})\s+(.*)$", stripped)
+ if m:
+ close_list()
+ level = len(m.group(1))
+ out.append("%s" % (level, inline(_escape(m.group(2))), level))
+ i += 1
+ continue
+
+ if re.match(r"^-{3,}$", stripped):
+ close_list()
+ out.append("
")
+ i += 1
+ continue
+
+ m = re.match(r"^([-*+])\s+(.*)$", stripped)
+ if m:
+ if not in_list or list_tag != "ul":
+ close_list()
+ out.append("")
+ in_list, list_tag = True, "ul"
+ out.append("- %s
" % inline(_escape(m.group(2))))
+ i += 1
+ continue
+
+ m = re.match(r"^\d+\.\s+(.*)$", stripped)
+ if m:
+ if not in_list or list_tag != "ol":
+ close_list()
+ out.append("")
+ in_list, list_tag = True, "ol"
+ out.append("- %s
" % inline(_escape(m.group(1))))
+ i += 1
+ continue
+
+ if stripped.startswith(">"):
+ close_list()
+ quote = [stripped.lstrip("> ")]
+ while i + 1 < len(lines) and lines[i + 1].strip().startswith(">"):
+ i += 1
+ quote.append(lines[i].strip().lstrip("> "))
+ out.append("%s
" % inline(_escape(" ".join(quote))))
+ i += 1
+ continue
+
+ close_list()
+ para = [stripped]
+ while (i + 1 < len(lines) and lines[i + 1].strip()
+ and not re.match(r"^(#{1,6}\s|[-*+]\s|\d+\.\s|>|```|-{3,}$)",
+ lines[i + 1].strip())):
+ i += 1
+ para.append(lines[i].strip())
+ out.append("%s
" % inline(_escape(" ".join(para))))
+ i += 1
+
+ close_list()
+ return "\n".join(out)
+
+
+def _escape(s):
+ return (s.replace("&", "&").replace("<", "<").replace(">", ">"))
+
+
+# ------------------------------------------------------------------- seiten
+
+def parse_md(raw):
+ """Trennt Front-Matter (titel: ...) vom Inhalt."""
+ meta = {}
+ body = raw
+ if raw.startswith("---"):
+ parts = raw.split("---", 2)
+ if len(parts) >= 3:
+ for line in parts[1].strip().splitlines():
+ if ":" in line:
+ k, v = line.split(":", 1)
+ meta[k.strip().lower()] = v.strip()
+ body = parts[2].lstrip("\n")
+ return meta, body
+
+
+def md_path(slug):
+ return os.path.join(MD_DIR, slug + ".md")
+
+
+def list_pages():
+ pages = []
+ for f in sorted(os.listdir(MD_DIR)):
+ if f.endswith(".md") and f[:-3] != FOOTER_PAGE:
+ pages.append(f[:-3])
+ return pages
+
+
+def load_page(slug):
+ with open(md_path(slug), encoding="utf-8") as fh:
+ return fh.read()
+
+
+def save_page(slug, raw):
+ with open(md_path(slug), "w", encoding="utf-8") as fh:
+ fh.write(raw)
+
+
+def delete_page(slug):
+ p = md_path(slug)
+ if os.path.exists(p):
+ os.remove(p)
+
+
+# ------------------------------------------------------------------ export
+
+def render_page(raw, template, footer_html=""):
+ meta, body = parse_md(raw)
+ html = md_to_html(body)
+ page = (template
+ .replace("{{titel}}", _escape(meta.get("titel", "")))
+ .replace("{{inhalt}}", html))
+ if "{{footer}}" in template:
+ page = page.replace("{{footer}}", footer_html)
+ elif footer_html and "
+
+
Seiten
+
+
+
Neue Seite:
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+" in page:
+ # Template ohne {{footer}}-Platzhalter: Baustein automatisch anhaengen
+ idx = page.rindex("")
+ page = page[:idx] + "\n" + page[idx:]
+ return page, meta.get("titel", "")
+
+
+def load_template():
+ files = [f for f in os.listdir(TPL_DIR) if f.endswith(".html")]
+ if not files:
+ raise SystemExit("Kein Template in templates/ gefunden (z.B. default.html).")
+ with open(os.path.join(TPL_DIR, files[0]), encoding="utf-8") as fh:
+ return fh.read()
+
+
+def export_site():
+ """Baut output/: pro MD-Datei eine HTML-Seite, CSS wird kopiert."""
+ template = load_template()
+ # Output leeren und neu aufbauen
+ if os.path.isdir(OUT_DIR):
+ shutil.rmtree(OUT_DIR)
+ os.makedirs(os.path.join(OUT_DIR, "css"), exist_ok=True)
+
+ for f in os.listdir(CSS_DIR):
+ if f.endswith(".css"):
+ shutil.copy2(os.path.join(CSS_DIR, f), os.path.join(OUT_DIR, "css", f))
+
+ footer_html = ""
+ if os.path.exists(md_path(FOOTER_PAGE)):
+ _, fbody = parse_md(load_page(FOOTER_PAGE))
+ footer_html = md_to_html(fbody)
+
+ exported = []
+ for slug in list_pages():
+ page, _ = render_page(load_page(slug), template, footer_html)
+ # startseite.md wird zur index.html
+ out_name = "index.html" if slug == START_PAGE else slug + ".html"
+ with open(os.path.join(OUT_DIR, out_name), "w", encoding="utf-8") as fh:
+ fh.write(page)
+ exported.append(out_name)
+ return exported
+
+
+# ------------------------------------------------------------------- server
+
+class Handler(BaseHTTPRequestHandler):
+ def log_message(self, *args):
+ pass
+
+ def _send(self, code, body, ctype="application/json; charset=utf-8"):
+ data = body.encode("utf-8") if isinstance(body, str) else body
+ self.send_response(code)
+ self.send_header("Content-Type", ctype)
+ self.send_header("Content-Length", str(len(data)))
+ self.end_headers()
+ self.wfile.write(data)
+
+ def _json(self, obj, code=200):
+ self._send(code, json.dumps(obj))
+
+ def _body(self):
+ n = int(self.headers.get("Content-Length", 0))
+ return json.loads(self.rfile.read(n).decode("utf-8")) if n else {}
+
+ def do_GET(self):
+ path = urlparse(self.path).path
+
+ if path in ("/", "/admin"):
+ self._send(200, ADMIN_HTML, "text/html; charset=utf-8")
+ elif path == "/api/pages":
+ self._json({"pages": list_pages(), "start": START_PAGE,
+ "footer": os.path.exists(md_path(FOOTER_PAGE))})
+ elif path.startswith("/api/page/"):
+ slug = unquote(path[len("/api/page/"):])
+ if slug == FOOTER_PAGE or slug in list_pages():
+ self._json({"slug": slug, "raw": load_page(slug)})
+ else:
+ self._json({"error": "Seite nicht gefunden"}, 404)
+ elif path == "/api/export":
+ self._json({"exported": export_site()})
+ else:
+ self._json({"error": "unbekannter Pfad"}, 404)
+
+ def do_POST(self):
+ path = urlparse(self.path).path
+ try:
+ data = self._body()
+ except (ValueError, json.JSONDecodeError):
+ return self._json({"error": "ungueltige Anfrage"}, 400)
+
+ if path == "/api/page":
+ slug = (data.get("slug") or "").strip()
+ raw = data.get("raw") or ""
+ if not SLUG_RE.match(slug):
+ return self._json({"error": "Ungueltiger Name (a-z, 0-9, -)."}, 400)
+ save_page(slug, raw)
+ self._json({"ok": True, "slug": slug})
+ elif path == "/api/delete":
+ slug = data.get("slug", "")
+ if slug != FOOTER_PAGE and slug not in list_pages():
+ return self._json({"error": "Seite nicht gefunden"}, 404)
+ if slug in (START_PAGE, FOOTER_PAGE):
+ return self._json({"error": "Startseite und Footer-Baustein koennen nicht geloescht werden."}, 400)
+ delete_page(slug)
+ self._json({"ok": True})
+ elif path == "/api/preview":
+ meta, body = parse_md(data.get("raw") or "")
+ self._json({"html": md_to_html(body)})
+ else:
+ self._json({"error": "unbekannter Pfad"}, 404)
+
+
+ADMIN_HTML = """
+
+
+
+
Mini-CMS
+
+
+
+