From 100e200c3bfb853aaf6368100f73d5c31b6e669b Mon Sep 17 00:00:00 2001 From: Deeman Date: Mon, 2 Mar 2026 11:43:26 +0100 Subject: [PATCH] fix(articles): find .md by slug scan + lighter editor theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes: - _find_article_md() scans _ARTICLES_DIR for files whose frontmatter slug matches, so padel-halle-bauen-de.md is found for slug 'padel-halle-bauen'. The previous exact-name lookup missed any file where the filename ≠ slug (e.g. {slug}-{lang}.md naming convention). - Editor pane: replace dark navy background with warm off-white (#FEFDFB) and dark text so it reads like a document, not a code editor. --- web/src/padelnomics/admin/routes.py | 30 ++++++++++++++++--- .../admin/templates/admin/article_form.html | 30 +++++++++++-------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index 47b3765..7a0a43e 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -2203,6 +2203,27 @@ _ARTICLES_DIR = Path(__file__).parent.parent.parent.parent.parent / "data" / "co _FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL) +def _find_article_md(slug: str) -> Path | None: + """Return the Path of the .md file whose frontmatter slug matches, or None. + + Tries the exact name first ({slug}.md), then scans _ARTICLES_DIR for any + file whose YAML frontmatter contains 'slug: '. This handles the + common pattern where files are named {slug}-{lang}.md but the frontmatter + slug omits the language suffix. + """ + if not _ARTICLES_DIR.is_dir(): + return None + exact = _ARTICLES_DIR / f"{slug}.md" + if exact.exists(): + return exact + for path in _ARTICLES_DIR.glob("*.md"): + raw = path.read_text(encoding="utf-8") + m = _FRONTMATTER_RE.match(raw) + if m and f"slug: {slug}" in m.group(1): + return path + return None + + async def _sync_static_articles() -> None: """Upsert static .md articles from data/content/articles/ into the DB. @@ -2626,11 +2647,12 @@ async def article_edit(article_id: int): # Load markdown source if available (manual or generated) from ..content.routes import BUILD_DIR as CONTENT_BUILD_DIR - md_path = _ARTICLES_DIR / f"{article['slug']}.md" - if not md_path.exists(): + md_path = _find_article_md(article["slug"]) + if md_path is None: lang = article["language"] or "en" - md_path = CONTENT_BUILD_DIR / lang / "md" / f"{article['slug']}.md" - raw = md_path.read_text() if md_path.exists() else "" + fallback = CONTENT_BUILD_DIR / lang / "md" / f"{article['slug']}.md" + md_path = fallback if fallback.exists() else None + raw = md_path.read_text() if md_path else "" # Strip YAML frontmatter so only the prose body appears in the editor m = _FRONTMATTER_RE.match(raw) body = raw[m.end():].lstrip("\n") if m else raw diff --git a/web/src/padelnomics/admin/templates/admin/article_form.html b/web/src/padelnomics/admin/templates/admin/article_form.html index d0107e8..e488873 100644 --- a/web/src/padelnomics/admin/templates/admin/article_form.html +++ b/web/src/padelnomics/admin/templates/admin/article_form.html @@ -156,8 +156,8 @@ align-items: center; justify-content: space-between; padding: 0.375rem 0.875rem; - background: #1E293B; - border-bottom: 1px solid #0F172A; + background: #F8FAFC; + border-bottom: 1px solid #E2E8F0; flex-shrink: 0; } .ae-pane--preview .ae-pane__header { @@ -171,13 +171,11 @@ letter-spacing: 0.09em; color: #94A3B8; } - .ae-pane--editor .ae-pane__label { color: #475569; } .ae-pane__hint { font-size: 0.625rem; font-family: var(--font-mono); - color: #475569; + color: #94A3B8; } - .ae-pane--preview .ae-pane__hint { color: #94A3B8; } /* The markdown textarea */ .ae-editor { @@ -185,16 +183,16 @@ resize: none; border: none; outline: none; - padding: 1.125rem 1.25rem; + padding: 1.5rem 2rem; font-family: var(--font-mono); - font-size: 0.8125rem; - line-height: 1.75; - background: #0F172A; - color: #CBD5E1; - caret-color: #3B82F6; + font-size: 0.875rem; + line-height: 1.8; + background: #FEFDFB; + color: #1E293B; + caret-color: #1D4ED8; tab-size: 2; } - .ae-editor::placeholder { color: #334155; } + .ae-editor::placeholder { color: #CBD5E1; } .ae-editor:focus { outline: none; } /* Preview pane */ @@ -283,9 +281,15 @@ color: #94A3B8; font-family: var(--font-mono); opacity: 0; - transition: opacity 0.15s; + transition: opacity 0.2s; } .ae-loading.htmx-request { opacity: 1; } + + /* Responsive: stack on narrow screens */ + @media (max-width: 900px) { + .ae-split { grid-template-columns: 1fr; } + .ae-pane--preview { display: none; } + } {% endblock %}