n8n HTML node: extracting page content with CSS selectors
Published 25 August 2026 · 8 min read
You fetched a page with HTTP Request and all you need is the price, the title, or the list of links it contains. Writing a regex against HTML is a bad idea, and pulling out a Code node for three lines of Cheerio is another one. n8n's HTML node (n8n-nodes-base.html) does exactly this: it takes an HTML string, applies CSS selectors, and outputs clean JSON — plus two bonus operations that produce HTML instead of consuming it.
Three operations, two directions
The HTML node exposes three choices in its Operation field:
- Extract HTML Content: the heart of the node. You give it an HTML string and a list of CSS selectors, it returns the matching values as JSON.
- Convert to HTML Table: turns the incoming items into an HTML
<table>, with formatting options. - Generate HTML Template: a template editor where you inject your data using n8n expressions.
One operation to read HTML, two to write it. This guide covers all three, with the emphasis on the first: it concentrates both most of the use cases and most of the traps.
Extract HTML Content: the data must already be there
Like the XML node, the HTML node fetches no URL of its own. It expects the HTML to be already present in the item, and the Source Data parameter says where:
- JSON: the HTML is a string in a field of the item. You name that field in JSON Property. In practice it's
data, the field filled by an HTTP Request node configured with Response → Response Format: Text. Without that setting, n8n may try to interpret the response and the HTML node won't get the string it expects. - Binary: the HTML is a binary file attached to the item (a download, an attachment, a file read from disk). You then name the binary field in Input Binary Field.
Picking the wrong source is the most common misconfiguration: a node that "returns nothing" while the selector is fine is usually pointing at an empty field.
Extraction Values: one row per value
The Extraction Values parameter is a collection: one row per piece of data you want, and each row holds:
- Key: the name of the output field. You choose it —
title,price,links. - CSS Selector: the selector, in standard CSS syntax (
h1,.price,#main .product-title,meta[property="og:title"],article a). - Return Value: what gets extracted from the matching elements —
- Text: the text only, tags stripped; the Skip Selectors option takes a comma-separated list of selectors to exclude, handy for dropping a legal-notice
<span>glued inside the title; - HTML: the element's inner HTML, when you want to keep the markup or re-parse it;
- Attribute: the value of an attribute, whose name you type in the Attribute field that appears (
href,src,datetime,content); - Value: the value of a form field (
input,select,textarea).
- Text: the text only, tags stripped; the Skip Selectors option takes a comma-separated list of selectors to exclude, handy for dropping a legal-notice
- Return Array: see below.
Return Array: the checkbox that changes the output shape
By default, a selector matching twenty elements returns only the first one, as a string: the behaviour you want for a title or a price, a trap for everything else. Turn on Return Array for that row and the key holds an array of every match instead. Decide this cardinality while you write the selector, not the day the workflow breaks. The array still lives inside a single n8n item: to process each element separately, chain a Split Out node on the key, as covered in our Split Out and Aggregate guide.
The options: Trim Values and Clean Up Text
Only two options, but they save a lot of manual cleanup. Trim Values removes leading and trailing spaces and newlines; you rarely have a reason to switch it off. Clean Up Text goes further: it collapses multiple spaces and internal whitespace, which makes text indented across ten source lines usable again — handy before a Number() on a price, or before sending the text to a language model.
Example: title, price and links from a product page
An HTTP Request GET on the URL with Response Format Text, then an HTML node in Extract HTML Content, Source Data JSON, JSON Property data, and three extraction rows:
| Key | CSS Selector | Return Value | Return Array |
|---|---|---|---|
title |
h1.product-title |
Text | no |
price |
meta[itemprop="price"] |
Attribute → content |
no |
links |
.related a |
Attribute → href |
yes |
The output looks like this:
{
"title": "Organic cotton t-shirt",
"price": "24.90",
"links": ["/products/enamel-mug", "/products/tote-bag", "/products/cap"]
}
Note the selector chosen for the price: targeting a meta tag or a data-* attribute rather than the visible text saves you from cleaning up "£24.90 incl. VAT" and survives redesigns better. From there, an Edit Fields node converts types and normalises relative URLs, and the pipeline moves on to a database or a spreadsheet.
Trap number one: JavaScript is not executed
The HTML node parses a static string: it loads nothing, renders nothing, runs no script. On a React, Vue or Angular app, the HTML served by the server is often just a <div id="root"></div>: your selectors match zero elements and the node returns empty fields with no error — far more confusing than an outright failure. Diagnosing it takes ten seconds: open the HTTP Request node's output and look for your value in the data field. If the text isn't there, no selector will conjure it up. The ways out are well known:
- Go through a headless browser that renders the page before handing you the HTML — Browserless or Playwright, set up step by step in our headless browser with n8n guide. The HTML node then does its job normally.
- Use a rendering and extraction API such as Firecrawl, which returns Markdown or structured JSON directly.
- Look for the internal API the front end calls: the browser's Network tab often reveals a JSON endpoint far more stable than any selector.
The full picture of the available strategies, legal considerations included, is in our web scraping with n8n guide.
Writing selectors that survive
A selector-based extractor is fragile by construction: it encodes an assumption about the document's structure, and that structure changes without warning. The problem has been studied for a long time under the name wrapper induction. The survey by Emilio Ferrara, Pasquale De Meo, Giacomo Fiumara and Robert Baumgartner, "Web data extraction, applications and techniques: A survey" (Knowledge-Based Systems, 2014), points out that maintaining extractors as websites evolve is the dominant cost of these systems, well ahead of writing them in the first place (see on Google Scholar). Aditya Parameswaran, Nilesh Dalvi, Hector Garcia-Molina and Rajeev Rastogi formalised the question in "Optimal Schemes for Robust Web Extraction" (PVLDB, 2011): among all the extraction paths that point to the same piece of data, some are measurably more robust to DOM changes, and the authors give algorithms to build the most robust one (see on Google Scholar).
Translated into n8n practice:
- Prefer semantics over generated class names:
meta[property="og:title"],[itemprop="price"],time[datetime]survive redesigns;.css-1x7k2p9disappears at the next build. - The shortest selector that stays unambiguous: every extra level of descent is one more chance to break.
- Check instead of hoping: an IF node testing
{{ $json.price ? true : false }}and raising an alert turns a silently empty extraction into a visible incident, instead of a database quietly filling up withnullfields. - Awkward cases: when one selector isn't enough (two template variants, deduplication, URLs to make absolute), a downstream Code node stays more readable than an acrobatic selector chain.
Producing HTML: table and template
Convert to HTML Table takes the incoming items and renders an HTML table. Its options are all about presentation: Capitalize Headers capitalises the headers, Caption adds a title above the table, Custom Styling enables your own styling, and Table Attributes, Header Attributes, Row Attributes, Cell Attributes inject HTML attributes onto the <table>, <th>, <tr> and <td> elements. It's the fastest way to turn a query result into a readable report inside an email.
Generate HTML Template opens an editor where you write a complete template: HTML, styles in a <style> tag, scripts if needed, and n8n expressions in double curly braces to inject your data. The output is an HTML string to pass to an email body — see our guide on sending emails over SMTP with n8n. Worth remembering: email clients run no JavaScript and support only a subset of CSS, so stick to inline styles and a simple structure.
The remaining friction
- Encoding: a page declared
ISO-8859-1and fetched as text can arrive with mangled accented characters. The HTML node can't help, the problem is upstream: check the response's actual charset and, if needed, route the page through as binary to control the encoding at conversion time. - HTML entities:
&, and friends can survive depending on the return mode. A replace inside an Edit Fields node settles it. - Malformed HTML: a page whose tags never close can produce a tree different from the one your browser inspector shows. When in doubt, test the selector against the raw HTML, not the rendered DOM.
Going further
The HTML node is a deliberately modest building block: a string in, selectors, JSON out. Used well — right source, Return Array chosen deliberately, semantic selectors, a non-empty check downstream — it replaces most scraping Code nodes. If your end goal is to make the extracted content queryable by an AI, the RAG Assistant Pack (€119) provides the ingestion and search chain to plug in behind the extraction. And if the pages you monitor mostly feed replies to send by email, the AI Inbox Pack (€79) covers triage, drafting and sending on the mailbox side.
FAQ
Frequently asked questions
Why does the n8n HTML node return nothing even though my selector is correct?
Nine times out of ten, the HTML the node receives is not the HTML you see in your browser. The HTML node parses a static string: it runs no JavaScript at all. If the page builds its content client-side, the raw HTML returned by HTTP Request is just an empty shell and no selector will ever match. Check the actual content of the source field in the HTTP Request node's output panel: if your value isn't there as text, you need a headless browser or a rendering API.
What is the difference between Return Value Text, HTML, Attribute and Value?
Text returns only the text of matching elements, with tags stripped. HTML returns the inner HTML, which is useful when you want to keep the markup or re-parse it later. Attribute returns the value of an attribute you name in the Attribute field — href for a link, src for an image, content for a meta tag. Value reads the value of a form field such as an input, select or textarea.
How do I get every link on a page instead of just the first one?
Turn on the Return Array option for that extraction row. Without it the node returns only the first match as a string; with it, the key holds an array of every match for the selector. That array still lives inside a single n8n item: add a Split Out node on the key if you want one item per link so each URL can be processed separately.
Can the HTML node build an email or a report?
Yes, two operations exist for that. Convert to HTML Table turns the incoming items into an HTML table with styling options such as Capitalize Headers, Caption, Table Attributes and Custom Styling. Generate HTML Template lets you write a full template, styles included, with n8n expressions in double curly braces to inject your data. The output is an HTML string you pass to your email-sending node.
Bundle FlowKit Complet
€269