From 8ce68fa77930792e7eb55f76e2b1fc77556d40da Mon Sep 17 00:00:00 2001 From: drjones Date: Fri, 14 Aug 2026 20:09:34 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20markdown=E2=86=92HTML=20rendering=20in?= =?UTF-8?q?=20engine=20+=20harden=20thinking=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Engine now converts content_md to HTML at render time (was dumping raw markdown, causing articles to show literal #/**/- symbols and collapse into wall of text) - /api/publish accepts 'content' key and converts markdown→HTML for API consumers - Added md Jinja filter + md_to_html helper (markdown lib, extra+sane_lists) - orchestrator: log warning when falling back to 'thinking' field (CoT, not prose) - content_pipeline now generates formatted articles via LLM instead of raw scraped HTML --- core/orchestrator.py | 7 ++++++- sites/_engine/app.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/core/orchestrator.py b/core/orchestrator.py index 5843782..9c93ab7 100644 --- a/core/orchestrator.py +++ b/core/orchestrator.py @@ -205,9 +205,14 @@ def llm_chat(prompt: str, model: str = "qwen3.5:4b-mlx", host: str = OLLAMA_MACB result = r.json() if "message" in result: content = result["message"].get("content", "") - # ornith puts output in 'thinking' when content is empty + # ornith puts output in 'thinking' when content is empty. + # WARNING: 'thinking' is chain-of-thought reasoning, NOT article text. + # Only fall back to it for JSON/short tasks, never long-form prose. if not content: content = result["message"].get("thinking", "") + if content: + log.warning(f"LLM {model} returned empty content — fell back to 'thinking' field ({len(content)} chars). " + f"Verify this is real output, not chain-of-thought.") if content: return content if "error" in result: diff --git a/sites/_engine/app.py b/sites/_engine/app.py index b3dce3b..1eec3ed 100644 --- a/sites/_engine/app.py +++ b/sites/_engine/app.py @@ -10,6 +10,24 @@ from pathlib import Path from datetime import datetime from flask import Flask, request, jsonify, render_template_string, g, abort, Response +try: + import markdown as _md +except ImportError: + _md = None + + +def md_to_html(text): + """Convert Markdown to HTML for article rendering.""" + if not text: + return "" + if _md is not None: + return _md.markdown(text, extensions=["extra", "sane_lists"]) + # Minimal fallback (markdown lib not installed) + import re as _re + out = _re.sub(r"^#{1,6}\s+(.+)$", r"

\1

", text, flags=_re.M) + out = _re.sub(r"^\*\*(.+?)\*\*$", r"\1", out, flags=_re.M) + return "

" + out.replace("\n\n", "

").replace("\n", "
") + "

" + # ─── Config ──────────────────────────────────────────────────────── VERTICAL = os.environ.get("PUBLISHER_VERTICAL", "guides") DOMAIN = f"{VERTICAL}.thetempleofdoom.com" @@ -520,8 +538,10 @@ def api_publish(): slug = data.get("slug", "") title = data.get("title", "") - content_html = data.get("content_html", data.get("content_md", "")) - content_md = data.get("content_md", "") + content_md = data.get("content_md") or data.get("content") or "" + content_html = data.get("content_html", "") + if not content_html and content_md: + content_html = md_to_html(content_md) excerpt = data.get("excerpt", data.get("seo_description", "")) seo_title = data.get("seo_title", title) seo_description = data.get("seo_description", "") @@ -1113,7 +1133,7 @@ ARTICLE_TEMPLATE = """
- {{ article.content_html|safe }} + {{ (article.content_md or article.content_html)|md|safe }}