cd /news/large-language-models/i-stopped-fighting-prompts-locking-d… · home topics large-language-models article
[ARTICLE · art-27451] src=dev.to ↗ pub= topic=large-language-models verified=true sentiment=↑ positive

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.

read2 min publishedJun 15, 2026

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):

prompt = "Write an article about {topic} in Markdown format."
response = llm.generate(prompt)

After (Deterministic):

prompt = "Output data about {topic} in JSON format."
json_data = llm.generate(prompt) 

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.

def sanitize_markdown(text):
    if not re.search(r'```

[\s\S]*?

```', text):
        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:

def get_stock_data(code):
    if is_hk_stock(code):
        return hk_api.get_price(code)

    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.

── more in #large-language-models 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-stopped-fighting-p…] indexed:0 read:2min 2026-06-15 ·