feat: 24 individuelle SEO-Artikel-URLs (/artikel/<slug>)

- Jeder der 24 Artikel hat jetzt seine eigene Unterseite
- SEO-Slugs aus Artikeltiteln (ti-anschlusspflicht-2025, etc.)
- /blog/<slug> bleibt als Sektions-Uebersichtsseite
- Sidebar-Links zeigen jetzt auf individuelle Artikel-URLs
- _ARTIKEL um 4. Feld (SEO-Slug) erweitert
- Neue /artikel/<slug>-Route mit vollstaendigem Artikel-Layout
This commit is contained in:
Hermes Agent
2026-06-25 21:16:52 +02:00
parent 9375f9d426
commit 3cbee2911c
+82 -39
View File
@@ -297,30 +297,30 @@ def datenschutz() -> str:
# ── Blog-Artikel-Datenbank ─────────────────────────────── # ── Blog-Artikel-Datenbank ───────────────────────────────
_ARTIKEL = [ _ARTIKEL = [
("/blog/telematik-vorschriften", "TI-Anschlusspflicht 2025", "artikel-01-ti-anschlusspflicht.html"), ("/blog/telematik-vorschriften", "TI-Anschlusspflicht 2025", "artikel-01-ti-anschlusspflicht.html", "ti-anschlusspflicht-2025"),
("/blog/telematik-vorschriften", "Elektronische Abrechnung ab 12/2026", "artikel-02-elektronische-abrechnung.html"), ("/blog/telematik-vorschriften", "Elektronische Abrechnung ab 12/2026", "artikel-02-elektronische-abrechnung.html", "elektronische-abrechnung-2026"),
("/blog/telematik-vorschriften", "SMC-B-Karte — 33% Abdeckung", "artikel-03-smc-b-karte.html"), ("/blog/telematik-vorschriften", "SMC-B-Karte — 33% Abdeckung", "artikel-03-smc-b-karte.html", "smc-b-karte-pflege"),
("/blog/dsgvo-it-sicherheit", "46,9 Mio. EUR DSGVO-Bussgelder", "artikel-04-dsgvo-bussgelder.html"), ("/blog/dsgvo-it-sicherheit", "46,9 Mio. EUR DSGVO-Bussgelder", "artikel-04-dsgvo-bussgelder.html", "dsgvo-bussgelder-gesundheitswesen"),
("/blog/dsgvo-it-sicherheit", "BSI: 16 Schwachstellen Pflegesoftware", "artikel-05-bsi-schwachstellen.html"), ("/blog/dsgvo-it-sicherheit", "BSI: 16 Schwachstellen Pflegesoftware", "artikel-05-bsi-schwachstellen.html", "bsi-schwachstellen-pflegesoftware"),
("/blog/dsgvo-it-sicherheit", "138 Sicherheitsvorfaelle Gesundheitswesen", "artikel-06-sicherheitsvorfaelle.html"), ("/blog/dsgvo-it-sicherheit", "138 Sicherheitsvorfaelle Gesundheitswesen", "artikel-06-sicherheitsvorfaelle.html", "sicherheitsvorfaelle-gesundheitswesen"),
("/blog/telematik-vorschriften", "NIS-2 Registrierungsfrist", "artikel-07-nis2.html"), ("/blog/telematik-vorschriften", "NIS-2 Registrierungsfrist", "artikel-07-nis2.html", "nis2-registrierungsfrist"),
("/blog/fachkraeftemangel", "200.000 unbesetzte Pflegestellen", "artikel-08-fachkraeftemangel.html"), ("/blog/fachkraeftemangel", "200.000 unbesetzte Pflegestellen", "artikel-08-fachkraeftemangel.html", "fachkraeftemangel-pflege"),
("/blog/fachkraeftemangel", "62% Altenpflege — Personalplanung", "artikel-09-personalplanung.html"), ("/blog/fachkraeftemangel", "62% Altenpflege — Personalplanung", "artikel-09-personalplanung.html", "personalplanung-altenpflege"),
("/blog/fachkraeftemangel", "KI-Sprachassistent spart 27% Zeit", "artikel-10-ki-sprachassistent.html"), ("/blog/fachkraeftemangel", "KI-Sprachassistent spart 27% Zeit", "artikel-10-ki-sprachassistent.html", "ki-sprachassistent-pflege"),
("/blog/foerderung-finanzierung", "Bis 12.000 EUR Digitalisierungszuschuss", "artikel-11-digitalisierungszuschuss.html"), ("/blog/foerderung-finanzierung", "Bis 12.000 EUR Digitalisierungszuschuss", "artikel-11-digitalisierungszuschuss.html", "digitalisierungszuschuss-pflege"),
("/blog/foerderung-finanzierung", "DiPA — 70 EUR monatlich ab 2026", "artikel-12-dipa.html"), ("/blog/foerderung-finanzierung", "DiPA — 70 EUR monatlich ab 2026", "artikel-12-dipa.html", "dipa-digitale-pflegeanwendungen"),
("/blog/foerderung-finanzierung", "Pflegeversicherung 1 Mrd. Defizit", "artikel-13-pflegeversicherung.html"), ("/blog/foerderung-finanzierung", "Pflegeversicherung 1 Mrd. Defizit", "artikel-13-pflegeversicherung.html", "pflegeversicherung-defizit"),
("/blog/qualitaet-pruefung", "QPR ambulant ab Juli 2026", "artikel-14-qpr-ambulant.html"), ("/blog/qualitaet-pruefung", "QPR ambulant ab Juli 2026", "artikel-14-qpr-ambulant.html", "qpr-ambulant-2026"),
("/blog/qualitaet-pruefung", "PeBeM verpflichtend ab 2026", "artikel-15-pebem.html"), ("/blog/qualitaet-pruefung", "PeBeM verpflichtend ab 2026", "artikel-15-pebem.html", "pebem-2026"),
("/blog/qualitaet-pruefung", "MDK-Pruefung digital bestehen", "artikel-16-mdk-pruefung.html"), ("/blog/qualitaet-pruefung", "MDK-Pruefung digital bestehen", "artikel-16-mdk-pruefung.html", "mdk-pruefung-digital"),
("/blog/telematik-vorschriften", "ePA fuer alle — Opt-out", "artikel-17-epa.html"), ("/blog/telematik-vorschriften", "ePA fuer alle — Opt-out", "artikel-17-epa.html", "epa-opt-out"),
("/blog/dsgvo-it-sicherheit", "Cloud-Hosting Deutschland", "artikel-18-cloud-hosting.html"), ("/blog/dsgvo-it-sicherheit", "Cloud-Hosting Deutschland", "artikel-18-cloud-hosting.html", "cloud-hosting-deutschland"),
("/blog/praxis-tipps", "Tablets fuer die Pflege — Top 5", "artikel-19-tablets.html"), ("/blog/praxis-tipps", "Tablets fuer die Pflege — Top 5", "artikel-19-tablets.html", "tablets-pflege-top5"),
("/blog/praxis-tipps", "WLAN im Pflegeheim", "artikel-20-wlan.html"), ("/blog/praxis-tipps", "WLAN im Pflegeheim", "artikel-20-wlan.html", "wlan-pflegeheim"),
("/blog/praxis-tipps", "Offline-Dokumentation", "artikel-21-offline.html"), ("/blog/praxis-tipps", "Offline-Dokumentation", "artikel-21-offline.html", "offline-dokumentation-pflege"),
("/blog/dsgvo-it-sicherheit", "E-Mail-Verschluesselung", "artikel-22-email-verschluesselung.html"), ("/blog/dsgvo-it-sicherheit", "E-Mail-Verschluesselung", "artikel-22-email-verschluesselung.html", "email-verschluesselung-pflege"),
("/blog/praxis-tipps", "Medikationsplan digital", "artikel-23-medikationsplan.html"), ("/blog/praxis-tipps", "Medikationsplan digital", "artikel-23-medikationsplan.html", "medikationsplan-digital"),
("/blog/praxis-tipps", "IT-Notfallplan 10-Punkte", "artikel-24-it-notfallplan.html"), ("/blog/praxis-tipps", "IT-Notfallplan 10-Punkte", "artikel-24-it-notfallplan.html", "it-notfallplan-pflege"),
] ]
# ── Leistungen-Daten ───────────────────────────────────── # ── Leistungen-Daten ─────────────────────────────────────
@@ -392,15 +392,15 @@ _IT_SERVICE = {
# ── Helper: Seitenleiste ────────────────────────────────── # ── Helper: Seitenleiste ──────────────────────────────────
def _sidebar(routes: list[tuple[str, str, str]]) -> str: def _sidebar(routes: list[tuple[str, str, str, str]]) -> str:
"""Erstelle Seitenleiste mit Artikel-Links. """Erstelle Seitenleiste mit Artikel-Links.
Args: Args:
routes: Liste von (Route, Titel, Datei)-Tupeln. routes: Liste von (Sektion, Titel, Datei, Slug)-Tupeln.
""" """
links = [] links: list[str] = []
for route, titel, _datei in routes: for _sektion, titel, _datei, slug in routes:
links.append('<a href="' + route + '" class="sidebar-link">' + titel + "</a>") links.append('<a href="/artikel/' + slug + '" class="sidebar-link">' + titel + "</a>")
return ( return (
"<div class='article-sidebar'><div class='sidebar-title'>Weitere Artikel</div>" "<div class='article-sidebar'><div class='sidebar-title'>Weitere Artikel</div>"
+ "".join(links) + "".join(links)
@@ -414,18 +414,19 @@ def _cards(titel: str, beschreibung: str) -> str:
return '<div class="card"><h3>' + titel + "</h3><p>" + beschreibung + "</p></div>" return '<div class="card"><h3>' + titel + "</h3><p>" + beschreibung + "</p></div>"
# ── Blog-Route (FIX: Nutzt _ARTIKEL-Mapping) ───────────── # ── Blog-Sektionen (Übersichtsseiten) ─────────────────────
@app.route("/blog/<path:slug>") @app.route("/blog/<path:slug>")
def blog(slug: str) -> str: def blog_section(slug: str) -> str:
"""Blog-Artikel — nutzt _ARTIKEL-Mapping fuer korrekte Dateinamen.""" """Blog-Sektion — zeigt ersten Artikel + Sidebar mit allen Artikeln der Sektion."""
route_key = "/blog/" + slug route_key = "/blog/" + slug
# Finde ALLE Artikel dieser Sektion # Finde ALLE Artikel dieser Sektion (4-Tupel)
sektions_artikel = [(r, t, d) for r, t, d in _ARTIKEL if r == route_key] sektions_artikel: list[tuple[str, str, str, str]] = [
(r, t, d, s) for r, t, d, s in _ARTIKEL if r == route_key
]
if sektions_artikel: if sektions_artikel:
# Ersten Artikel als Hauptinhalt laden _, haupt_titel, datei, _ = sektions_artikel[0]
_, haupt_titel, datei = sektions_artikel[0]
pfad = ARTIKEL_DIR / datei pfad = ARTIKEL_DIR / datei
if pfad.exists(): if pfad.exists():
inhalt = pfad.read_text(encoding="utf-8") inhalt = pfad.read_text(encoding="utf-8")
@@ -434,11 +435,10 @@ def blog(slug: str) -> str:
else: else:
inhalt = "<p>Artikeltext folgt in Kuerze.</p>" inhalt = "<p>Artikeltext folgt in Kuerze.</p>"
else: else:
inhalt = "<p>Artikel nicht gefunden.</p>" inhalt = "<p>Keine Artikel in dieser Sektion gefunden.</p>"
sektions_artikel = _ARTIKEL
haupt_titel = slug.replace("-", " ").title() haupt_titel = slug.replace("-", " ").title()
sektions_artikel = _ARTIKEL
# Seitenleiste: alle Artikel dieser Sektion
sidebar = _sidebar(sektions_artikel) sidebar = _sidebar(sektions_artikel)
content = ( content = (
@@ -453,6 +453,49 @@ def blog(slug: str) -> str:
return _base_template("Blog: " + haupt_titel, content, active="blog") return _base_template("Blog: " + haupt_titel, content, active="blog")
# ── Individuelle Artikel-Seiten (SEO-optimiert) ──────────
_ARTIKEL_BY_SLUG: dict[str, tuple[str, str, str, str]] = {
slug: (sektion, titel, datei, slug)
for sektion, titel, datei, slug in _ARTIKEL
}
@app.route("/artikel/<path:slug>")
def artikel_detail(slug: str) -> str:
"""Einzelner Artikel — eigene SEO-URL mit vollstaendigem Inhalt."""
eintrag = _ARTIKEL_BY_SLUG.get(slug)
if eintrag:
sektion, titel, datei, _ = eintrag
pfad = ARTIKEL_DIR / datei
if pfad.exists():
inhalt = pfad.read_text(encoding="utf-8")
match = re.search(r"<body[^>]*>(.*?)</body>", inhalt, re.DOTALL | re.IGNORECASE)
inhalt = match.group(1) if match else inhalt
else:
inhalt = "<p>Artikeltext folgt in Kuerze.</p>"
# Sidebar: andere Artikel derselben Sektion
sektions_artikel: list[tuple[str, str, str, str]] = [
(r, t, d, s) for r, t, d, s in _ARTIKEL if r == sektion and s != slug
]
sidebar = _sidebar(sektions_artikel)
content = (
"<div class='article-layout'><div class='article-content'><h1>"
+ titel
+ "</h1>"
+ inhalt
+ "</div>"
+ sidebar
+ "</div>"
)
return _base_template(titel, content, active="blog")
else:
content = "<div class='content'><h1>Artikel nicht gefunden</h1><p>Dieser Artikel existiert leider nicht.</p></div>"
return _base_template("Artikel nicht gefunden", content)
# ── Weitere Routen ─────────────────────────────────────── # ── Weitere Routen ───────────────────────────────────────
@app.route("/ueber-uns") @app.route("/ueber-uns")
def ueber_uns() -> str: def ueber_uns() -> str: