HTML β†’ Markdown for LLMs#

Raw HTML is 90% navigation, scripts, and cookie banners. Strip it to clean Markdown and you cut your token bill while improving the model’s answers.

⏱ ~8 min read Β· ~12 min hands-on πŸ”— needs: Hidden JSON APIs Β· Document Parsing

Feeding raw HTML to an LLM wastes tokens on markup the model doesn’t need and buries the actual content in boilerplate. Converting to Markdown first is one of the highest-leverage steps in any scrape-to-LLM pipeline.

First, though: if the page has a hidden JSON API, use that instead. Structured JSON beats converted prose every time.

Try it in 5 minutes#

# /// script
# requires-python = ">=3.12"
# dependencies = ["trafilatura>=2.0"]
# ///
"""Fetch a page and extract just the main article as clean Markdown.

Run:  uv run to_markdown.py https://en.wikipedia.org/wiki/Web_scraping
"""

import sys

import trafilatura

url = sys.argv[1] if len(sys.argv) > 1 else "https://en.wikipedia.org/wiki/Web_scraping"

downloaded = trafilatura.fetch_url(url)
markdown = trafilatura.extract(downloaded, output_format="markdown", with_metadata=True)

print(markdown[:1500])
print(f"\n--- HTML {len(downloaded):,} chars β†’ Markdown {len(markdown):,} chars ---")

βœ… Note the size drop β€” typically 80–95%. That reduction is your token bill, and everything removed was noise.

Pick the right tool#

ToolUse it forNote
trafilaturaArticle/main-content extraction from web pagesBest default: strips nav, ads, footers. output_format="markdown"
markdownifyFaithful HTML→MD of a fragment you already isolatedConverts everything — no boilerplate removal
MarkItDownPDFs, DOCX, PPTX, XLSX, images β†’ MarkdownMicrosoft; many formats, one API β†’ Document Parsing
Jina ReaderOne hosted call: https://r.jina.ai/<url>Zero setup, renders JS; a third party sees your URLs

The distinction that matters: trafilatura decides what’s worth keeping; markdownify converts whatever you hand it. Use trafilatura on a full page, markdownify on a <div> you already selected.

flowchart LR
    P["Page"] --> Q{"Hidden JSON API?"}
    Q -->|Yes| J["Use the JSON. Done."]
    Q -->|No| R{"Whole page, or<br/>one element?"}
    R -->|"Whole page"| T["trafilatura<br/>(drops boilerplate)"]
    R -->|"One element"| M["select it, then<br/>markdownify"]
    T --> L["Clean Markdown β†’ LLM"]
    M --> L

Local or hosted?#

Self-hosting (trafilatura/markdownify) is free, private, and has no rate limit β€” this is the role a managed API like Firecrawl used to play, and it’s a few lines of code. A hosted reader is worth it when the page needs JavaScript rendering and you don’t want to run a browser; the cost is that you send every URL to a third party.

When it fails#

SymptomCauseFix
Empty outputJS-rendered page β€” no content in the HTMLRender with Playwright first, then convert
Main content droppedAggressive extraction on an unusual layoutLoosen with favor_recall=True, or select the element yourself + markdownify
Nav/ads still presentUsed markdownify on the whole pageUse trafilatura, or select the content node first
Tables mangledComplex/nested tablesinclude_tables=True; for real data prefer the underlying API
Links lostDefault drops theminclude_links=True

Your turn (β‰ˆ12 min)#

  1. Run to_markdown.py on a Wikipedia article; record the HTML β†’ Markdown size ratio.
  2. Run it on a news article. Did the byline and date survive with with_metadata=True?
  3. Compare against https://r.jina.ai/<same-url> β€” which is cleaner?
  4. Estimate the token saving: chars / 4 β‰ˆ tokens. At $3/M input tokens, what did you save on 10,000 pages?

Checklist#

  • I check for a JSON API before converting HTML at all.
  • I use trafilatura for whole pages, markdownify for isolated fragments.
  • I know an empty extraction usually means JS rendering is required.
  • I can estimate the token/cost saving from the size reduction.

Go deeper#