robots.txt: how to check what's blocked, and when blocking backfires

Short answer

robots.txt is a file at the root of a site, such as https://example.com/robots.txt, that tells crawlers which URLs they may fetch. It controls crawling, not indexing: a blocked page can still show up in Google, without a description, if other pages link to it. Google matches the most specific rule, and blocking the scripts and styles a page needs can stop Google seeing the page properly. Test a URL with the snippet below or in Search Console.

What robots.txt does, and doesn't

A robots.txt file is a list of rules, grouped by crawler:

User-agent: *
Disallow: /basket/
Disallow: /search
Allow: /search/help

Sitemap: https://example.com/sitemap.xml
robots.txt can…robots.txt can't…
Stop well-behaved crawlers fetching URLs, saving crawling for pages that matterKeep a page out of search results. Use noindex for that, and don't block the page
Point crawlers at your sitemapHide anything private. The file is public, and anyone can read it
Apply to one host: https://example.com onlyCover other subdomains, http:// or other ports. Each needs its own file

If robots.txt returns a 404 or another 4xx (except 429), Google treats the site as having no restrictions. If it returns a server error or 429, Google stops crawling the site for up to 12 hours, then uses its last good copy of the file for up to 30 days. That's why a broken robots.txt can be worse than a missing one. Google caches the file for up to 24 hours, so changes take time to apply.

In Search Console's Page indexing report, a URL robots.txt stops Google crawling shows as Blocked by robots.txt. One Google indexed anyway, from links, shows as Indexed, though blocked by robots.txt. To get that one out of results, unblock it and add noindex.

How the rules are matched

Most robots.txt mistakes come from assuming the rules are read top to bottom. They aren't:

One group applies Googlebot follows only the group with the most specific name that matches it. If there's a User-agent: Googlebot group, the User-agent: * rules don't apply to Googlebot at all. If several groups name Googlebot, their rules are combined.
Longest match wins Of the rules that match a URL, the one with the longest path wins, whatever order they're in. If an Allow and a Disallow are equally specific, the Allow wins.
Paths are prefixes Disallow: /search also blocks /searching and /search-tips. Add a slash, /search/, if you mean the folder.
Wildcards * matches any characters, $ marks the end of the URL: Disallow: /*.pdf$ blocks every PDF, except PDF URLs with a query string such as /guide.pdf?v=2.
Case matters Paths are case-sensitive: Disallow: /Basket/ doesn't block /basket/.

With the example file above:

URLRules that matchResult
/basket/Disallow: /basket/Blocked
/search?q=coatsDisallow: /searchBlocked
/search/helpDisallow: /search, Allow: /search/helpAllowed: the Allow is longer
/searchlight-coatDisallow: /searchBlocked by accident
/coats/woolnoneAllowed

The fourth row is the classic surprise: a short rule meant for one folder catches every URL that starts with the same letters.

Blocking files the page needs

Google renders pages the way a browser does, loading their scripts, styles and images. If robots.txt blocks those files, Google may see a broken, unstyled or empty page and judge it on that. Common causes:

  • Old rules such as Disallow: /js/, /css/ or /wp-includes/ from when blocking them was common advice.
  • Blocking a folder of "system" files that also holds the theme's scripts.
  • Blocking an API path the page calls to load its content.

Blocked images also can't appear in Google Images.

How to check by hand

1Test this page and its files 30 seconds

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

// Tests this page, and the files it loaded from this site, against robots.txt for Googlebot
const robotsUrl = location.origin + '/robots.txt';
let res = null;
try { res = await fetch(robotsUrl, { cache: 'no-store' }); } catch (e) {
  console.log('Could not read ' + robotsUrl + ' from this page (it may redirect to another host). Open it in a new tab instead.');
}
if (res && !res.ok) {
  console.log('robots.txt returned ' + res.status + (res.status >= 500 || res.status === 429
    ? ': Google pauses crawling the site for up to 12 hours, then uses its last good copy of the file'
    : ': Google treats that as no restrictions'));
} else if (res) {
  const groups = [];
  let group = null;
  (await res.text()).split(/\r?\n|\r/).forEach((raw, i) => {
    const line = raw.replace(/#.*$/, '').trim();
    const m = line.match(/^([a-z-]+)\s*:\s*(.*)$/i);
    if (!m) return;
    const field = m[1].toLowerCase(), value = m[2].trim();
    if (field === 'user-agent') {
      if (!group || group.rules.length) groups.push(group = { agents: [], rules: [] });
      // Google reads the name up to the first character that isn't a letter, - or _ (Googlebot/2.1 = googlebot)
      group.agents.push(value === '*' ? '*' : (value.match(/^[a-z_-]+/i) || [''])[0].toLowerCase());
    } else if ((field === 'allow' || field === 'disallow') && group && value) {
      // Google compares percent-encoded forms: encode non-ASCII characters, upper-case existing escapes
      const path = value.replace(/[^\x00-\x7F]+/g, s => encodeURI(s)).replace(/%[0-9a-f]{2}/gi, s => s.toUpperCase());
      group.rules.push({ allow: field === 'allow', path, line: i + 1, text: line });
    }
  });
  // Google combines every group that names Googlebot; if there are none, it uses the * groups
  const named = groups.filter(g => g.agents.includes('googlebot'));
  const rules = (named.length ? named : groups.filter(g => g.agents.includes('*'))).flatMap(g => g.rules);
  console.log('Using ' + (named.length ? 'the User-agent: Googlebot rules' : groups.some(g => g.agents.includes('*')) ? 'the User-agent: * rules' : 'no rules (no group applies to Googlebot)'));
  const matches = (pattern, path) => {
    const end = pattern.endsWith('$');
    const body = (end ? pattern.slice(0, -1) : pattern).replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
    return new RegExp('^' + body + (end ? '$' : '')).test(path);
  };
  // The longest matching rule wins; Allow wins a tie
  const verdict = url => {
    const u = new URL(url);
    let best = null;
    for (const r of rules) {
      if (matches(r.path, u.pathname + u.search) && (!best || r.path.length > best.path.length || (r.path.length === best.path.length && r.allow))) best = r;
    }
    return best && !best.allow ? 'BLOCKED by line ' + best.line + ': ' + best.text : 'allowed';
  };
  const entries = performance.getEntriesByType('resource');
  const files = [...new Map(entries.map(e => [e.name.split('#')[0], e.initiatorType]))]
    .filter(([url]) => url.startsWith(location.origin + '/') && url !== robotsUrl);
  const blocked = files.map(([url, type]) => ({ url, checked: type, result: verdict(url) })).filter(r => r.result !== 'allowed');
  console.table([{ url: location.href, checked: 'this page', result: verdict(location.href) }, ...blocked]);
  console.log(files.length + ' file(s) from this site checked, ' + blocked.length + ' blocked'
    + (entries.length >= 250 ? '. The browser stops listing files after 250, so some may be missing' : ''));
}

It reads the site's robots.txt using Google's documented rules, then says whether this page is blocked and lists any blocked scripts, styles, images or other files the page loaded from the same site, with the rule that blocks each one.

It checks Googlebot's rules only, for the same host as the page. Files from other hosts (a CDN, a subdomain) are governed by their own robots.txt. It sees only files the page has loaded so far, so scroll and click first if some load late.

2Ask Google 2 minutes

In Search Console, paste the URL into URL Inspection. "Crawl allowed?" says whether robots.txt blocks it. Under Settings, the robots.txt report (for domain-level properties) shows which robots.txt files Google found, when it last fetched them and any problems reading them.

3Read the file 2 minutes

Open /robots.txt on the site and read it with the matching rules above in mind. Look especially for short Disallow paths without a trailing slash, rules for folders of scripts and styles, and a User-agent: Googlebot group that quietly replaces the general rules.

How to fix it

  1. To keep a page out of search, remove its Disallow rule and add noindex instead. Google has to crawl the page to see the noindex.
  2. For files the page needs, remove the rule or add a more specific Allow, such as Allow: /assets/*.css.
  3. For rules that catch too much, add the trailing slash or use $ to end the match.
  4. After changing the file, check it in Search Console's robots.txt report. Google caches the file for up to 24 hours, or use Request a recrawl in that report.

On Shopify, WordPress and Next.js

  • Shopify: Add a robots.txt.liquid template to change the file. It replaces Shopify's generated version, so save a copy of the current file first and compare the output before you publish.
  • WordPress: With no physical file, WordPress generates the rules itself through do_robots(). A real robots.txt in the web root or a plugin may be what you're actually seeing.
  • Next.js: Add a static app/robots.txt, or generate it with app/robots.ts.

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
Response Codes: Internal Blocked by Robots.txtInternal Disallowed URLsrobots.txt stops crawlers fetching the URL.
Response Codes: Internal Blocked ResourceDisallowed JavaScript filerobots.txt blocks a file the page needs, which can change how it renders for Google.

Doing this on every page?

SEOSnip tests robots.txt for you

Open the extension on any page and it applies Googlebot's rules from the site's robots.txt. You get:

  • A flag when robots.txt blocks the page itself, quoting the rule, its line number and its user-agent group
  • Scripts, styles and images on the same site that robots.txt blocks, with the rule for each
  • A ticket with that evidence, and a "Check if fixed" button that re-runs the check