Large language models are brilliant — right up until you ask them about something that happened after their training cutoff, or about content that lives on a specific website. Then they start hallucinating with total confidence.
The standard fix is Retrieval-Augmented Generation (RAG): fetch the real content, find the relevant pieces, and hand them to the model as context. The annoying part has always been step one — turning a messy web page into clean text you can actually feed to a model.
That's exactly what the Web to Markdown/JSON API solves. One HTTP call takes any URL and returns clean, structured content — Markdown or JSON — with the nav bars, ads, cookie banners, and other boilerplate stripped away.
In this tutorial you'll build a working RAG bot that answers questions about any web page, in roughly 60 lines of Python. No scraping libraries, no HTML parsing, no headless browsers.
The API at a glance
Endpoint:
POST https://web2md-api-production-d822.up.railway.app/extract
Request:
{
"url": "https://example.com/article",
"format": "markdown",
"max_length": 50000
}
-
format—markdown,json, ortext - Free tier: 50 requests/day
- Sign up / grab a key on RapidAPI
Response (markdown):
{
"success": true,
"url": "https://example.com/article",
"title": "Article Title",
"content": "# Markdown content...",
"word_count": 1234,
"language": "en"
}
The content field is clean Markdown, ready to chunk and embed. That's the whole point — this API does the dirty work so your RAG pipeline starts with clean text instead of raw HTML.
The plan
-
POSTa URL to/extractand get Markdown back. - Split the Markdown into chunks.
- Embed every chunk.
- Embed the user's question.
- Find the chunks most similar to the question.
- Feed those chunks + the question to an LLM.
Prerequisites
The code
Save this as rag_bot.py:
import requests
API_URL = "https://web2md-api-production-d822.up.railway.app/extract"
OLLAMA = "http://localhost:11434" # Ollama's REST API
def fetch_markdown(url: str) -> dict:
"""Turn any web page into clean Markdown via the API."""
r = requests.post(
API_URL,
json={"url": url, "format": "markdown", "max_length": 50000},
)
r.raise_for_status()
return r.json()
def chunk_markdown(md: str, max_chars: int = 1200) -> list[str]:
"""Split Markdown into chunks, respecting paragraph boundaries."""
paragraphs = [p.strip() for p in md.split("\n\n") if p.strip()]
chunks, current = [], ""
for p in paragraphs:
if len(current) + len(p) > max_chars and current:
chunks.append(current)
current = p
else:
current = f"{current}\n\n{p}" if current else p
if current:
chunks.append(current)
return chunks
def embed(text: str) -> list[float]:
"""Get a vector embedding from Ollama."""
r = requests.post(
f"{OLLAMA}/api/embeddings",
json={"model": "nomic-embed-text", "prompt": text},
)
r.raise_for_status()
return r.json()["embedding"]
def cosine(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = sum(x * x for x in a) ** 0.5
nb = sum(y * y for y in b) ** 0.5
return dot / (na * nb) if na and nb else 0.0
def answer(chunks: list[str], question: str) -> str:
"""Retrieve the top-k chunks, then ask the LLM with them as context."""
q_emb = embed(question)
ranked = sorted(chunks, key=lambda c: cosine(embed(c), q_emb), reverse=True)
context = "\n\n".join(ranked[:3])
prompt = (
"Answer the question using ONLY the context below. "
"If the context doesn't contain the answer, say so.\n\n"
f"Context:\n{context}\n\n"
f"Question: {question}\nAnswer:"
)
r = requests.post(
f"{OLLAMA}/api/generate",
json={"model": "llama3", "prompt": prompt, "stream": False},
)
r.raise_for_status()
return r.json()["response"]
if __name__ == "__main__":
data = fetch_markdown("https://en.wikipedia.org/wiki/Retrieval-augmented_generation")
print(f"Extracted: {data['title']} ({data['word_count']} words)")
chunks = chunk_markdown(data["content"])
question = "How does retrieval-augmented generation reduce hallucination?"
print("Answer:", answer(chunks, question))
How it works
-
fetch_markdown— onePOSTto the API. The service fetches the page, strips the boilerplate, and returns clean Markdown. NoBeautifulSoup, notrafilatura, no headless Chrome. -
chunk_markdown— RAG models work best on focused snippets, so we split the article into ~1,200-character chunks along paragraph boundaries. -
embed/cosine— turns text into vectors and measures similarity. Here we use Ollama's freenomic-embed-textmodel, but any embedding provider works. -
answer— embeds the question, ranks chunks by similarity, then asks the LLM to answer only from the top three. That "only use the context" instruction is what keeps the model grounded instead of hallucinating.
Run it
pip install requests
ollama pull nomic-embed-text
ollama pull llama3
python rag_bot.py
Expected output:
Extracted: Retrieval-augmented generation (1500 words)
Answer: RAG reduces hallucination by grounding the model's responses in retrieved
documents. Instead of relying only on its training data, the model is given relevant
passages as context and instructed to answer from them, so its output is constrained
by verifiable source material rather than learned but possibly wrong associations.
Why clean Markdown matters for RAG
If you feed raw HTML into an embedding model, your vectors get polluted by <div> tags, nav menus, and footer links. That means your similarity search surfaces "Copyright © 2026" instead of the actual answer. Starting from clean Markdown — headings, paragraphs, lists, nothing else — dramatically improves retrieval quality for free.
This is also why the API's json format is handy: it returns structured paragraphs, headings, images, and links separately, so you can chunk by heading hierarchy or skip image-heavy sections entirely.
Taking it further
- Point it at docs, changelogs, or a competitor's blog and ask questions about them.
- Swap Ollama for OpenAI, Anthropic, or Gemini — the
answerfunction only needs two small changes. - Store embeddings in a vector DB (Chroma, Qdrant) and index many pages at once to build a searchable knowledge base.
- Use
"format": "json"when you need the raw structure rather than prose.
Wrapping up
RAG is only as good as the text you feed it. The Web to Markdown/JSON API removes the most tedious 80% of the pipeline — fetching and cleaning arbitrary web pages — so you can spend your time on the interesting part: retrieval and generation.
Try it free at 50 requests/day on RapidAPI, and drop a comment if you build something cool with it.
Top comments (0)