The HTML base tag: how one line can redirect every relative link

Short answer

The <base href="…"> tag sets the address that every relative URL on the page is resolved against: links, images, scripts, and relative canonical and hreflang URLs. Most sites don't need one. If you have one, make it a full https:// URL on your own site, keep only one, and put it before anything else in the head that uses a URL. A wrong base can silently point every relative link at another site.

What the base tag does

Normally a relative link such as href="care-guide" is read relative to the page's own address. A base tag replaces that starting point:

Page https://example.com/coats/woolhref="care-guide" goes to
No base taghttps://example.com/coats/care-guide
<base href="https://example.com/">https://example.com/care-guide: a different page
<base href="https://staging.example.com/">https://staging.example.com/care-guide: a different site
<base href="http://example.com/">http://example.com/care-guide: insecure, via a redirect

Links that start with / keep the path but take the base's protocol and host, and full URLs (https://…) aren't affected at all. Only the first <base href> on the page counts; any others are ignored.

It also changes in-page links. With a base tag, href="#reviews" points to #reviews on the base URL, not on the current page, so a "jump to reviews" link can load a different page instead of scrolling.

How it goes wrong

Left over from staging A base tag naming the test site goes live, and every relative link, image and canonical quietly points back at staging.
HTTP after a move to HTTPS The base still says http://, so relative links go through a redirect, and relative scripts and styles can be blocked as mixed content.
Two base tags A theme and a plugin each add one. Only the first counts, which may not be the one you changed.
Too late in the head Files above the base tag, such as stylesheets, have already loaded from the page's own address, but tools that read the finished page apply the base to everything. The same tag then points in two places. Put the base before every other element with a URL.
Not a valid URL A typo, a javascript: or data: value, or a bare word. Browsers ignore javascript: and data: values. A URL that can't be read at all breaks every relative link in Chrome.

Browsers resolve relative canonical and hreflang URLs against the base too, and search engines are likely to do the same. A wrong base could point every page's canonical somewhere else. Google already recommends full URLs in canonicals, so write them in full.

How to check by hand

1See the base and what it changes 30 seconds

Open DevTools (F12), go to the Console and paste:

// Shows the page's <base> URL(s), and which relative links point somewhere different because of it
const parse = (value, against) => { try { return new URL(value, against); } catch (e) { return null; } };
const bases = [...document.querySelectorAll('base[href]')];
const used = bases[0];
const urlEls = [...document.querySelectorAll('[href], [src]')].filter(el => el.tagName !== 'BASE');
console.table(bases.map((b, i) => {
  const url = parse(b.getAttribute('href'), location.href);
  const before = urlEls.filter(el => el.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING).length;
  return {
    base: b.getAttribute('href'),
    used: i === 0 ? 'yes: the first one wins' : 'NO: ignored',
    'in the head': b.closest('head') ? 'yes' : 'NO',
    'URL elements before it': before,
    problem: !url ? 'NOT A VALID URL'
      : !/^https?:$/.test(url.protocol) ? 'NOT http(s)'
      : url.host !== location.host ? 'DIFFERENT HOST: ' + url.host
      : url.protocol !== location.protocol ? 'DIFFERENT PROTOCOL: ' + url.protocol
      : '',
  };
}));
if (!bases.length) console.log('No <base href>: relative URLs resolve against the page address');
// Only HTML <a> and <link> (not SVG links); anything without a scheme is relative, including //host/path
const relative = [...document.querySelectorAll('a[href], link[href]')]
  .filter(el => el.namespaceURI === 'http://www.w3.org/1999/xhtml')
  .filter(el => !/^[a-z][a-z0-9+.-]*:/i.test(el.getAttribute('href').trim()));
const changed = relative
  .map(el => ({
    href: el.getAttribute('href'),
    'goes to': (parse(el.getAttribute('href'), document.baseURI) || { href: 'nowhere: the base is broken' }).href,
    'without the base': (parse(el.getAttribute('href'), location.href) || {}).href,
    note: used && (el.compareDocumentPosition(used) & Node.DOCUMENT_POSITION_FOLLOWING) ? 'before the base tag' : '',
  }))
  .filter(r => r['goes to'] !== r['without the base']);
if (changed.length) console.table(changed.slice(0, 20));
console.log(changed.length + ' of ' + relative.length + ' relative links point somewhere different because of the base');

It lists each base tag, which one is used, whether it's in the head, how many URL elements come before it, and whether it points at another host or protocol. Then it lists relative links that go somewhere different because of the base, with where they'd go without it.

2Check the source 30 seconds

Press Ctrl+U and search for <base. If it's there, it should be near the top of the head, before any <link>, <script> or <style>, and appear once.

3Hover over links 1 minute

Hover over a few navigation and in-page links and check the address in the bottom corner of the browser. Links that unexpectedly show another host, or an in-page link that shows a different page, point to a base tag problem.

How to fix it

  1. Remove it if nothing relies on it.
  2. If you need it, make it a full https:// URL on the live site, and put it first in the head, straight after <meta charset>.
  3. Keep one. Find which template or plugin adds the second and remove it there.
  4. Write canonical and hreflang URLs in full, so they don't depend on the base at all.

How crawlers report this

If a crawl sent you here, these are the labels to look for. Our guide to Screaming Frog and Sitebulb issues covers other common ones.

Screaming FrogSitebulbWhat it means
No direct matchBase URL malformed or emptyThe base tag has no usable URL, which can break every relative link on the page.
No direct matchMultiple base URLsThe page has more than one base tag. Only the first counts.

Doing this on every page?

SEOSnip checks the base tag for you

Open the extension on any page and it reads every base tag. You get:

  • A flag for more than one base URL, listing each with where it was found
  • A flag for a base URL that isn't a valid http or https address, with the tag shown (a base on another host or on http isn't flagged; the snippet above shows those)
  • Base tags in the body, through the metadata-outside-the-head check
  • A ticket with that evidence, and a "Check if fixed" button that re-runs the check