#5384·cheerio

prop('href')/prop('src') ignore <base href> and skip area/base/script/embed/track/input

Author: vojtisprime11Created Jul 30, 2026Updated Jul 30, 2026

Describe the bug

prop('href') and prop('src') diverge from the DOM in two ways:

  1. An in-document <base href> is ignored. The HTML spec resolves URL-reflecting attributes against the document base URL, which is the first <base href> in the document when present, and the document URL otherwise. Cheerio always resolves against options.baseURI, so every scraped URL on a page that uses <base href> comes out pointing at the wrong path.

  2. Only 6 of the 13 elements that reflect these attributes are resolved. Today href is resolved for a and link, and src for img, iframe, audio, video and source (attributes.ts). The DOM also reflects href on area and base, and src on script, embed, track and input. For those elements prop() silently returns the raw relative attribute instead of a resolved URL, which is easy to miss because the same call works for <a>.

Steps to reproduce

javascript
import { load } from 'cheerio';

const html = `
  <head><base href="/sub/dir/"></head>
  <body>
    <a href="p.html">a</a>
    <area href="p.html">
    <script src="j.js"></script>
  </body>`;

const $ = load(html, { baseURI: 'https://example.com/base/page.html' });

$('a').prop('href');      // 'https://example.com/base/p.html'   <- ignores <base>
$('area').prop('href');   // 'p.html'                            <- not resolved at all
$('script').prop('src');  // 'j.js'                              <- not resolved at all

Same document in jsdom (which follows the spec here):

javascript
import { JSDOM } from 'jsdom';

const d = new JSDOM(html, { url: 'https://example.com/base/page.html' }).window.document;

d.querySelector('a').href;     // 'https://example.com/sub/dir/p.html'
d.querySelector('area').href;  // 'https://example.com/sub/dir/p.html'
d.querySelector('script').src; // 'https://example.com/sub/dir/j.js'

Expected behavior

prop('href') / prop('src') should match the DOM:

  • resolve against the first <base href> in the document when there is one, falling back to options.baseURI (a <base>'s own href resolves against the document URL, not itself);
  • resolve href for a, area, base, link and src for audio, embed, iframe, img, input, script, source, track, video;
  • keep ignoring <base> in XML mode, where the element carries no HTML semantics.

Invalid or missing <base href> values should be ignored rather than throwing, and the first <base href> wins when a document contains several, as in the spec.

Environment

  • cheerio version: main (f8f3393) and 1.2.0
  • Node.js: 26
  • verified against jsdom 28 for DOM parity

Happy to open a PR — I have the fix and tests ready, following CONTRIBUTING (issue first).