Spaces:
Sleeping
Sleeping
| /** | |
| * Minimal, conservative robots.txt handling. | |
| * - Honors Disallow/Allow for `User-agent: *` and for our own UA token. | |
| * - Supports `*` wildcards and `$` end anchors in paths. | |
| * - Honors Crawl-delay (capped by caller). | |
| * - On fetch failure, allows crawling (standard convention) but the | |
| * caller's politeness delay still applies. | |
| * If a rule is ambiguous, we err on the side of NOT crawling. | |
| */ | |
| export async function fetchRobots(origin, userAgent) { | |
| try { | |
| const res = await fetch(new URL('/robots.txt', origin), { | |
| headers: { 'user-agent': userAgent }, | |
| signal: AbortSignal.timeout(10000), | |
| }); | |
| if (!res.ok) return parseRobots('', userAgent); | |
| return parseRobots(await res.text(), userAgent); | |
| } catch { | |
| return parseRobots('', userAgent); | |
| } | |
| } | |
| export function parseRobots(text, userAgent) { | |
| const uaToken = (userAgent.split('/')[0] || '*').toLowerCase(); | |
| const groups = []; // { agents: [], rules: [{type, path}], crawlDelay } | |
| let current = null; | |
| let lastWasAgent = false; | |
| for (let line of text.split(/\r?\n/)) { | |
| line = line.replace(/#.*$/, '').trim(); | |
| if (!line) continue; | |
| const m = line.match(/^([a-zA-Z-]+)\s*:\s*(.*)$/); | |
| if (!m) continue; | |
| const field = m[1].toLowerCase(); | |
| const value = m[2].trim(); | |
| if (field === 'user-agent') { | |
| if (!lastWasAgent) { | |
| current = { agents: [], rules: [], crawlDelay: null }; | |
| groups.push(current); | |
| } | |
| current.agents.push(value.toLowerCase()); | |
| lastWasAgent = true; | |
| } else if (current && (field === 'disallow' || field === 'allow')) { | |
| if (value) current.rules.push({ type: field, path: value }); | |
| else if (field === 'disallow') current.rules.push({ type: 'allow-all', path: '' }); | |
| lastWasAgent = false; | |
| } else if (current && field === 'crawl-delay') { | |
| const d = parseFloat(value); | |
| if (!Number.isNaN(d)) current.crawlDelay = d; | |
| lastWasAgent = false; | |
| } else { | |
| lastWasAgent = false; | |
| } | |
| } | |
| // Most specific matching group wins: our UA token, else *. | |
| const own = groups.find((g) => g.agents.some((a) => a !== '*' && uaToken.includes(a))); | |
| const star = groups.find((g) => g.agents.includes('*')); | |
| const group = own ?? star ?? { rules: [], crawlDelay: null }; | |
| const compiled = group.rules | |
| .filter((r) => r.type !== 'allow-all') | |
| .map((r) => ({ type: r.type, path: r.path, re: pathToRegex(r.path), len: r.path.length })); | |
| return { | |
| crawlDelay: group.crawlDelay, | |
| isAllowed(pathname) { | |
| // Longest-match wins; Allow beats Disallow on equal length. | |
| let best = null; | |
| for (const r of compiled) { | |
| if (r.re.test(pathname)) { | |
| if (!best || r.len > best.len || (r.len === best.len && r.type === 'allow')) best = r; | |
| } | |
| } | |
| return !best || best.type === 'allow'; | |
| }, | |
| }; | |
| } | |
| function pathToRegex(rulePath) { | |
| const escaped = rulePath | |
| .split('*') | |
| .map((s) => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')) | |
| .join('.*'); | |
| const anchored = escaped.endsWith('\\$') ? escaped.slice(0, -2) + '$' : escaped; | |
| return new RegExp('^' + anchored); | |
| } | |