How to extract clean web content for RAG pipelines and LLM agents
Most web pages were not designed for LLMs. They were designed for browsers.
That distinction matters more than it sounds. A browser is tolerant: it renders whatever it is given and a human eye filters out the rest. A model does not filter. Everything you send it becomes context, and everything irrelevant becomes noise. Navigation bars, cookie banners, JavaScript-rendered sections, sidebar widgets, and CMS image URLs all end up in your output if you do not handle them upstream.
Your model sees the noise and treats it as signal. Chunking breaks. Embeddings get polluted. Retrieval quality drops and it is not obvious why.
The extraction step is often treated as a footnote in pipeline architecture. In practice, it determines how useful everything downstream is.
What the raw output looks like
Run a standard extraction on a documentation page or a blog post and look at what comes back.
The article text is in there. So is everything else.
You get the navigation repeated at the top of the output and again in the footer. Sidebar links with anchor text like "Read more" and "Related posts" mixed into the body where the DOM placed them. Cookie consent text injected between paragraphs. Escaped HTML characters like & and that never got cleaned. CMS-generated image URLs pointing to CDN paths with no context. Script and style tag content if the extraction did not filter for those explicitly.
On a JavaScript-rendered page, the situation is different but not better. A static fetch often returns the page shell with placeholder elements where the content should be. The article text was injected by a framework after load, so it never appears in the raw response at all. What you get is a skeleton.
Neither output is ready to pass into a model or a chunker without additional processing.
What clean output looks like
Clean output for an LLM pipeline looks like structured Markdown.
The article title becomes a top-level heading. Section headings are preserved at the correct hierarchy. Paragraphs are separated cleanly. Lists stay as lists. Code blocks retain their content and language identifiers.
What is not there: the navigation bar, the cookie banner, the sidebar, the footer links, the CMS image URLs, the escaped characters, the repeated boilerplate that surrounded the content on the original page.
The shape of the output matters because it determines how your pipeline behaves downstream. If a chunker splits on heading boundaries, the headings need to be consistent. If an embedding model reads paragraph text, that text needs to be clean prose, not prose mixed with link fragments and repeated navigation items.
The difference between raw extraction and clean Markdown is not cosmetic. It is the difference between content that chunks and embeds predictably and content that does not.
What you are actually handling when you build this yourself
Building reliable web content extraction is more involved than it looks from the outside.
JavaScript rendering requires a headless browser rather than a simple HTTP request. That means managing browser instances, handling page load timing, deciding when a page has finished rendering, and dealing with sites that behave differently in headless environments than in a regular browser session.
Content-only extraction requires identifying what is main content and what is chrome. Navigation and sidebars are not always wrapped in semantic tags that make them easy to identify. You are working with heuristics based on element position, text density, and link-to-text ratios. Those heuristics do not generalise cleanly across sites.
Dynamic content adds another layer. Sections that load on scroll, content behind tab interactions, carousels that inject image URLs into the text flow: none of these are guaranteed to be present in a single extraction pass.
Error states need explicit handling. Pages that return 200 but contain a login wall. Rate limiting. Pages that time out during rendering. Silent failures that return empty strings instead of surfacing the problem.
None of this is insurmountable. It is a real scope of ongoing work, because sites change, libraries update, and extraction rules that worked last quarter stop working.
Using Perceive
Perceive is the EnConvert endpoint built for this. You pass a URL and get back clean Markdown.
It runs a full browser pass for JavaScript-rendered pages, strips navigation and chrome, and returns structured content with headings, lists, tables, and code blocks preserved. The response is the same structure regardless of which site the URL points to. Your downstream code does not need to account for source-specific formatting differences.
That consistency is what matters most. You write your chunking and embedding logic once.
If you have built your own extraction layer, what was the edge case that took the longest to solve?
Get your free API key; it's 500 ops/month, no card required.
We launched on Product Hunt recently; if you have tried the free tier, an upvote, comment or honest review on the listing would mean a lot to us. producthunt.com/products/enconvert