Skip to content

HTML Mapping

github-actions[bot] edited this page Sep 11, 2026 · 5 revisions

HTML Mapping

HTMLMapper.toComponents(html, params?) turns an HTML string into a typed Component[]. This page explains the pipeline and the default rules; configuration is on Custom Mappings.

← Back to Home · Related: Custom Mappings · Component Types

Entry points

import { HTMLMapper } from '@canvasflow/feed';

const components = HTMLMapper.toComponents(html, params, root /* optional */);
const scopedHtml = HTMLMapper.getRootElement(html, rootMapping); // string | null
Method Returns Purpose
toComponents(html, params?, root?) Component[] The full HTML → components conversion, with optional root-element scoping.
getRootElement(html, mapping) string | null Serialize the first element matching mapping (used to scope extraction).

The pipeline

The HTML pipeline parses the input exactly once and applies all transformations as pure Node[] passes on that single tree:

  1. Parse with linkedom (via parser.ts) into a Node[] AST.
  2. stripBreaklines — strips \r\n/\n/\r from text content and attribute values; drops text nodes that become empty.
  3. hoistAnchorsWithImages — lifts <a> elements containing <img> descendants out of enclosing <p>/heading blocks.
  4. splitImagesFromParagraphs — splits <p>/h1h6 elements that have <img> direct children so each image becomes a sibling block.
  5. sanitizeInvalidAnchorHrefs — rewrites <a href> values that fail URL validation to href="#".
  6. Root scoping (optional) — if a root mapping is passed, getRootElement locates the matching subtree in the processed tree and narrows to [rootNode] before reduction.
  7. reduceComponents(params) — walks the node tree and emits Component[]; each node is serialized to HTML inline via sanitizeNodes() (no re-parse).

Evaluation order

For each element the reducer tries, in order:

  1. Exclusion — matches an excludes mapping, or has data-cf-ignore → element and children skipped.
  2. Unwrap — matches an unwrap mapping → the element is dropped but its children are kept and evaluated in its place. Checked ahead of everything below, so it can pierce a wrapper whose tag would otherwise consume the subtree (e.g. <p>).
  3. Built-in detection — social embeds (Instagram, Twitter/X, TikTok, YouTube, Vimeo, Dailymotion, Infogram, Apple Podcasts), <table>, <video>, <audio>, <iframe>, buttons, images (<img>, <picture>), <figure> (always produces a FigureContainerComponent), and role="gallery"/role="mosaic".
  4. Custom mappings — each mappings entry, in order; the first match wins.
  5. Default text rules — the tag → text-component table below.
  6. Descend — otherwise ignore the element and evaluate its children.

A custom mapping is only tested against the element currently being visited — never against its descendants. Combined with step 5, that means a wrapper with a default text mapping swallows everything inside it; unwrap is how you reach that content without editing the HTML.

Default text mapping

HTML Component type
h1 headline
h2 title
h3 subtitle
h4 intro
h5 crosshead
h6 byline
p body
ol body
ul body
a body
blockquote blockquote
footer footer

Any text element's role attribute overrides the default (e.g. <p role="crosshead">crosshead, <p role="text12">text12).

<hr> and <br> follow a similar but separate default rule — <hr>divider, <br>spacer (margin: "margin-20") — and can likewise be matched by any other element via a divider/spacer custom mapping.

Text sanitizing

Text components keep only phrasing content; styles and classes are stripped. On <a> elements only href, target, and rel survive. Whitespace-only text between inline elements is preserved as a non-breaking space so spacing in markup like <b>foo</b> <i>bar</i> is not collapsed.

Built-in element detection (summary)

Content Detected from
Image <img>, <picture> (uses the fallback <img>).
Figure <figure> — always produces a FigureContainerComponent (component: 'container', type: 'figure'). Caption and credit are extracted from a <figcaption>; credit nodes are identified by the <small> tag, role="credit", or class="credit". The contained media components (image, video, audio) are nested under components.
Gallery role="gallery"/role="mosaic" container, or a custom gallery mapping.
Video <video> (src or first <source>); YouTube/Vimeo/Dailymotion via <iframe>.
Audio <audio>; Apple Podcasts via <iframe>.
Social blockquote/a markers for Instagram, Twitter/X, TikTok.
Table <table>htmltable (restricted tag allow-list).
Button <a role="button"> or <button><a></button>.

To recognise content that does not follow these conventions, define a custom mapping.

Relative media URLs

HTMLMapper.toComponents() does not rewrite relative src/imageurl values — it has no item URL to resolve them against. That resolution (image/gallery/video/audio URLs made absolute against an item's <link> origin) only happens one layer up, as part of RSSFeed.build(). See RSS Feeds.

Clone this wiki locally