---
title: "Ingest HTML Files into LangChain with EnConvert"
description: "Use EnConvert to ingest HTML files into LangChain as heading-aware chunks with navigation stripped and section metadata ready to load as Document objects"
canonical: "https://www.enconvert.com/solutions/ingest-html-files-into-langchain-with-enconvert"
locale: "en"
---

# How to Ingest HTML Files into LangChain with EnConvert

A developer building a LangChain agent or RAG pipeline that reads HTML files needs clean, heading-aware chunks with section metadata, not raw HTML that still contains navigation chrome, sidebar content, and markup that buries the actual content. Parsing HTML with BeautifulSoup or LangChain’s BSHTMLLoader extracts text but does not strip navigation sidebars, does not split at heading boundaries, and does not attach the headings_path or section metadata a retrieval layer can filter on. For an agent reading exported documentation, a saved web page, or an HTML knowledge base article, flat extracted text without section context produces chunks the agent cannot locate within the document or filter by section. EnConvert’s Ingest endpoint takes the uploaded HTML file and returns JSONL chunks split at H1 to H3 heading boundaries with navigation chrome stripped, each carrying its full headings_path breadcrumb, section name, and chunk index in metadata, so clean HTML content is ready to load as LangChain Document objects and retrieve in one call.

## Example

```python
import json
import os
import time

import requests
from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings

HEADERS = {"X-API-Key": os.environ["ENCONVERT_API_KEY"]}

# Step 1: Upload the HTML file to Ingest
# Navigation and footer chrome is stripped where detected
with open("/path/to/your/exported-docs-page.html", "rb") as f:
    upload = requests.post(
        "https://api.enconvert.com/v2/ingest/files",
        headers=HEADERS,
        files={"files": ("exported-docs-page.html", f, "text/html")},
        timeout=60,
    )
upload.raise_for_status()
job_id = upload.json()["job_id"]

# Step 2: Poll until the job completes (give up after 10 minutes)
deadline = time.monotonic() + 600
while True:
    status_response = requests.get(
        f"https://api.enconvert.com/v2/ingest/{job_id}",
        headers=HEADERS,
        timeout=60,
    )
    status_response.raise_for_status()
    job = status_response.json()
    if job["status"] == "completed":
        break
    if job["status"] in ("failed", "canceled"):
        raise RuntimeError(f"Ingest {job['status']}: {job.get('error_message')}")
    if time.monotonic() > deadline:
        raise TimeoutError(f"Ingest {job_id} is still {job['status']} after 10 minutes")
    time.sleep(2)

# Step 3: Download the JSONL output
# The signed URL is valid for 15 minutes. Plain GET, no API key.
jsonl = requests.get(job["output_url"], timeout=60)
jsonl.raise_for_status()

# Split on newline only: str.splitlines() also breaks on characters like U+0085
chunks = [
    json.loads(line)
    for line in jsonl.content.decode("utf-8").split("\n")
    if line.strip()
]
print(f"Loaded {len(chunks)} chunks ready for embedding")

# Step 4: Load each chunk as a LangChain Document with metadata preserved
# Do not apply another text splitter - heading-aware boundaries are already set
# headings_path is a list of strings. A store that accepts only scalar metadata
# needs " > ".join(headings_path) instead.
documents = [
    Document(
        page_content=chunk["content"],
        metadata={
            "source_url": chunk["metadata"]["source_url"],
            "title": chunk["metadata"]["title"],
            "section": chunk["metadata"]["section"],
            "headings_path": chunk["metadata"]["headings_path"],
            "chunk_index": chunk["metadata"]["chunk_index"],
            "word_count": chunk["metadata"]["word_count"],
        },
    )
    for chunk in chunks
]

# Step 5: Embed the chunks into an in-memory vector store and save it locally
# InMemoryVectorStore ships with langchain-core; its search needs numpy installed
embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    api_key=os.environ["OPENAI_API_KEY"],
)
vectorstore = InMemoryVectorStore.from_documents(documents, embeddings)
vectorstore.dump("./html_index.json")

# Step 6: Similarity search - results carry the section they came from
results = vectorstore.similarity_search("how do I rotate an API key", k=3)
for doc in results:
    print(doc.metadata["section"])
    print(" > ".join(doc.metadata["headings_path"]))
    print(doc.page_content[:200])

# Step 7: Restrict retrieval to one section of the file
# Replace with a section heading from your own file (printed in Step 6)
SECTION = "Rotating API keys"
section_results = vectorstore.similarity_search(
    "how do I rotate an API key",
    k=3,
    filter=lambda doc: doc.metadata["section"] == SECTION,
)
for doc in section_results:
    print(doc.metadata["section"], "-", doc.page_content[:200])

# To reload the saved index later:
# vectorstore = InMemoryVectorStore.load("./html_index.json", embeddings)
```

## Output

Ingest returns the HTML file as JSONL chunks split at H1 to H3 heading boundaries with navigation chrome stripped, each carrying its headings_path breadcrumb, section name, and chunk index, so LangChain retrieval can filter by section. Parsing the same file with BeautifulSoup or BSHTMLLoader extracts text but does not strip navigation sidebars, does not split at heading boundaries, and attaches no section metadata LangChain can filter on.

## Get started

[Get free API key — 500 ops per month, no card required.](https://www.enconvert.com/auth?mode=signup)

## Related solutions

- [How to Ingest Markdown Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-files-into-llamaindex-with-enconvert.md)
- [How to Ingest Web Pages into Vertex AI with EnConvert](https://www.enconvert.com/solutions/ingest-web-pages-into-vertex-ai-with-enconvert.md)
- [How to Ingest Markdown into OpenAI File Search with EnConvert](https://www.enconvert.com/solutions/ingest-markdown-into-openai-file-search-enconvert.md)
- [How to Ingest HTML Files into LlamaIndex with EnConvert](https://www.enconvert.com/solutions/ingest-html-files-into-llamaindex-with-enconvert.md)
- [Best Browserless Alternative for Document Parsing](https://www.enconvert.com/solutions/browserless-alternative-for-document-parsing-enconvert.md)
- [How to Ingest DOCX into CrewAI with EnConvert](https://www.enconvert.com/solutions/ingest-docx-into-crewai-with-enconvert.md)
