robots.txt: how to check what's blocked, and when blocking backfires
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 matter | Keep a page out of search results. Use noindex for that, and don't block the page |
| Point crawlers at your sitemap | Hide anything private. The file is public, and anyone can read it |
Apply to one host: https://example.com only | Cover 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:
User-agent: Googlebot group, the User-agent: * rules don't apply to Googlebot at all. If several groups name Googlebot, their rules are combined.Disallow: /search also blocks /searching and /search-tips. Add a slash, /search/, if you mean the folder.* 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.Disallow: /Basket/ doesn't block /basket/.With the example file above:
| URL | Rules that match | Result |
|---|---|---|
/basket/ | Disallow: /basket/ | Blocked |
/search?q=coats | Disallow: /search | Blocked |
/search/help | Disallow: /search, Allow: /search/help | Allowed: the Allow is longer |
/searchlight-coat | Disallow: /search | Blocked by accident |
/coats/wool | none | Allowed |
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
- To keep a page out of search, remove its Disallow rule and add
noindexinstead. Google has to crawl the page to see the noindex. - For files the page needs, remove the rule or add a more specific Allow, such as
Allow: /assets/*.css. - For rules that catch too much, add the trailing slash or use
$to end the match. - 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.liquidtemplate 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 realrobots.txtin the web root or a plugin may be what you're actually seeing. - Next.js: Add a static
app/robots.txt, or generate it withapp/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 Frog | Sitebulb | What it means |
|---|---|---|
| Response Codes: Internal Blocked by Robots.txt | Internal Disallowed URLs | robots.txt stops crawlers fetching the URL. |
| Response Codes: Internal Blocked Resource | Disallowed JavaScript file | robots.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
robots.txt blocks this page
https://example.com/robots.txt
1 | User-agent: * |
3 | Disallow: /search |
Matches this page's path, /searchlight-coat.