I Stopped Fighting Prompts: Locking Down Markdown with Jinja2 A developer solved malformed Markdown output from LLMs by switching from probabilistic prompt engineering to a deterministic pipeline. The LLM now outputs structured JSON data, which is rendered into Markdown using Jinja2 templates, with regex-based post-processing as a safety net. The approach decouples content generation from formatting, ensuring consistent syntax. We faced a recurring issue in our content generation pipeline: the LLM frequently outputted malformed Markdown. Unclosed code blocks, broken list levels—you name it. Relying solely on Prompt engineering became a game of whack-a-mole that we couldn't win. The core problem? Asking an LLM to generate Markdown is a probabilistic process. A Prompt is a "soft constraint." No matter how well you phrase it, a slight token fluctuation can break the syntax, causing frontend crashes. We realized we were violating the Single Responsibility Principle. We were asking the model to do two jobs: Models are great at semantics but terrible at strict formatting rules. So, we decoupled them. Instead of asking the LLM to write Markdown, we switched to JSON output and let Jinja2 handle the rendering. Before Probabilistic : LLM generates raw text - hope for the best prompt = "Write an article about {topic} in Markdown format." response = llm.generate prompt After Deterministic : LLM outputs structured data only prompt = "Output data about {topic} in JSON format." json data = llm.generate prompt Jinja2 enforces the syntax md content = jinja env.get template 'article.md' .render data=json data This moved the formatting from a "maybe" to a "definitely." If the template is correct, the Markdown is correct. Just in case and for legacy compatibility , we added a post-processing layer with regex validation. It acts as a safety net for unclosed code fences. python def sanitize markdown text : Check if code blocks are properly closed if not re.search r' \s\S ? ', text : Attempt to wrap raw code in fences text = re.sub r' ^. $ ', r' \n\1\n ', text return text final markdown = sanitize markdown llm output While fixing the text generation, we also noticed a logic gap in our stock data queries. We treated A-shares, ETFs, and Hong Kong stocks identically. This caused failures because: .SH or .SZ suffixes.We implemented a router at the query entry point: python def get stock data code : Route HK stocks to specific API if is hk stock code : return hk api.get price code Append suffix for ETFs if missing elif ".SH" not in code and ".SZ" not in code: code = f"{code}.SH" return api.get price code By shifting from "Prompt Optimization" to "Engineering Hard Constraints": If you are fighting with LLMs to output perfect HTML or Markdown, stop. Use the LLM for what it's good at—generating structured JSON data—and use a template engine like Jinja2 to enforce the view layer. It turns a probabilistic headache into a deterministic pipeline.