
Documentation Scraper
- 59 installs
- 49 repo stars
- Updated February 11, 2026
- ratacat/claude-skills
Helps with documentation tasks.
About
documentation-scraper is a Claude Code skill for documentation. It helps solo builders move faster with AI-assisted coding.
- documentation-scraper
- Documentation
- AI-coding skill
Documentation Scraper by the numbers
- 59 all-time installs (skills.sh)
- Ranked #769 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ratacat/claude-skills --skill documentation-scraperAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 49 |
| Last updated | February 11, 2026 |
| Repository | ratacat/claude-skills ↗ |
What it does
Helps with documentation tasks.
Files
Documentation Scraper with slurp-ai
Overview
slurp-ai scrapes documentation websites and compiles them into a single markdown file optimized for AI agent context. It is lightweight, fast, and deterministic - it does NOT use AI to scrape, it is FOR AI consumption.
CRITICAL: Run Outside Sandbox
All commands in this skill MUST be run outside the sandbox. Use dangerouslyDisableSandbox: true for all Bash commands including:
which slurp(installation check)node analyze-sitemap.js(sitemap analysis)slurp(scraping)- File inspection commands (
wc,head,cat, etc.)
The sandbox blocks network access and file operations required for web scraping.
Pre-Flight: Check Installation
Before scraping, verify slurp-ai is installed:
which slurp || echo "NOT INSTALLED"If not installed, ask the user to run:
npm install -g slurp-aiRequires: Node.js v20+
Do NOT proceed with scraping until slurp-ai is confirmed installed.
Commands
| Command | Purpose |
|---|---|
slurp <url> | Fetch and compile in one step |
slurp fetch <url> [version] | Download docs to partials only |
slurp compile | Compile partials into single file |
slurp read <package> [version] | Read local documentation |
Output: Creates slurp_compiled/compiled_docs.md from partials in slurp_partials/.
CRITICAL: Analyze Sitemap First
Before running slurp, ALWAYS analyze the sitemap. This reveals the complete site structure and informs your --base-path and --max decisions.
Step 1: Run Sitemap Analysis
Use the included analyze-sitemap.js script:
node analyze-sitemap.js https://docs.example.comThis outputs:
- Total page count (informs
--max) - URLs grouped by section (informs
--base-path) - Suggested slurp commands with appropriate flags
- Sample URLs to understand naming patterns
Step 2: Interpret the Output
Example output:
📊 Total URLs in sitemap: 247
📁 URLs by top-level section:
/docs 182 pages
/api 45 pages
/blog 20 pages
🎯 Suggested --base-path options:
https://docs.example.com/docs/guides/ (67 pages)
https://docs.example.com/docs/reference/ (52 pages)
https://docs.example.com/api/ (45 pages)
💡 Recommended slurp commands:
# Just "/docs/guides" section (67 pages)
slurp https://docs.example.com/docs/guides/ --base-path https://docs.example.com/docs/guides/ --max 80Step 3: Choose Scope Based on Analysis
| Sitemap Shows | Action |
|---|---|
| < 50 pages total | Scrape entire site: slurp <url> --max 60 |
| 50-200 pages | Scope to relevant section with --base-path |
| 200+ pages | Must scope down - pick specific subsection |
| No sitemap found | Start with --max 30, inspect partials, adjust |
Step 4: Frame the Slurp Command
With sitemap data, you can now set accurate parameters:
# From sitemap: /docs/api has 45 pages
slurp https://docs.example.com/docs/api/intro \
--base-path https://docs.example.com/docs/api/ \
--max 55Key insight: Starting URL is where crawling begins. Base path filters which links get followed. They can differ (useful when base path itself returns 404).
Common Scraping Patterns
Library Documentation (versioned)
# Express.js 4.x docs
slurp https://expressjs.com/en/4x/api.html --base-path https://expressjs.com/en/4x/
# React docs (latest)
slurp https://react.dev/learn --base-path https://react.dev/learnAPI Reference Only
slurp https://docs.example.com/api/introduction --base-path https://docs.example.com/api/Full Documentation Site
slurp https://docs.example.com/CLI Options
| Flag | Default | Purpose |
|---|---|---|
--max <n> | 20 | Maximum pages to scrape |
--concurrency <n> | 5 | Parallel page requests |
--headless <bool> | true | Use headless browser |
--base-path <url> | start URL | Filter links to this prefix |
--output <dir> | ./slurp_partials | Output directory for partials |
--retry-count <n> | 3 | Retries for failed requests |
--retry-delay <ms> | 1000 | Delay between retries |
--yes | - | Skip confirmation prompts |
Compile Options
| Flag | Default | Purpose |
|---|---|---|
--input <dir> | ./slurp_partials | Input directory |
--output <file> | ./slurp_compiled/compiled_docs.md | Output file |
--preserve-metadata | true | Keep metadata blocks |
--remove-navigation | true | Strip nav elements |
--remove-duplicates | true | Eliminate duplicates |
--exclude <json> | - | JSON array of regex patterns to exclude |
When to Disable Headless Mode
Use --headless false for:
- Static HTML documentation sites
- Faster scraping when JS rendering not needed
Default is headless (true) - works for most modern doc sites including SPAs.
Output Structure
slurp_partials/ # Intermediate files
└── page1.md
└── page2.md
slurp_compiled/ # Final output
└── compiled_docs.md # Compiled resultQuick Reference
# 1. ALWAYS analyze sitemap first
node analyze-sitemap.js https://docs.example.com
# 2. Scrape with informed parameters (from sitemap analysis)
slurp https://docs.example.com/docs/ --base-path https://docs.example.com/docs/ --max 80
# 3. Skip prompts for automation
slurp https://docs.example.com/ --yes
# 4. Check output
cat slurp_compiled/compiled_docs.md | head -100Common Issues
| Problem | Cause | Solution |
|---|---|---|
Wrong --max value | Guessing page count | Run analyze-sitemap.js first |
| Too few pages scraped | --max limit (default 20) | Set --max based on sitemap analysis |
| Missing content | JS not rendering | Ensure --headless true (default) |
| Crawl stuck/slow | Rate limiting | Reduce --concurrency 3 |
| Duplicate sections | Similar content | Use --remove-duplicates (default) |
| Wrong pages included | Base path too broad | Use sitemap to find correct --base-path |
| Prompts blocking automation | Interactive mode | Add --yes flag |
Post-Scrape Usage
The output markdown is designed for AI context injection:
# Check file size (context budget)
wc -c slurp_compiled/compiled_docs.md
# Preview structure
grep "^#" slurp_compiled/compiled_docs.md | head -30
# Use with Claude Code - reference in prompt or via @fileWhen NOT to Use
- API specs in OpenAPI/Swagger: Use dedicated parsers instead
- GitHub READMEs: Fetch directly via raw.githubusercontent.com
- npm package docs: Often better to read source + README
- Frequently updated docs: Consider caching strategy
#!/usr/bin/env node
/**
* Sitemap Analyzer for Documentation Scraping
*
* Fetches and analyzes a site's sitemap to help determine optimal slurp parameters.
* Usage: node analyze-sitemap.js <base-url>
*
* Example: node analyze-sitemap.js https://docs.example.com
*/
const https = require('https');
const http = require('http');
const { URL } = require('url');
const SITEMAP_PATHS = [
'/sitemap.xml',
'/sitemap_index.xml',
'/docs/sitemap.xml',
'/en/sitemap.xml',
];
function fetch(url) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith('https') ? https : http;
const req = protocol.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
return fetch(res.headers.location).then(resolve).catch(reject);
}
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode}`));
}
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
});
req.on('error', reject);
req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
});
}
function extractUrls(xml) {
const urls = [];
// Match <loc> tags in sitemap
const locMatches = xml.matchAll(/<loc>\s*(.*?)\s*<\/loc>/gi);
for (const match of locMatches) {
urls.push(match[1].trim());
}
return urls;
}
function analyzePaths(urls, baseUrl) {
const base = new URL(baseUrl);
const sections = {};
const depths = {};
for (const url of urls) {
try {
const parsed = new URL(url);
if (parsed.host !== base.host) continue;
const path = parsed.pathname;
const parts = path.split('/').filter(Boolean);
// Count by first path segment (section)
const section = '/' + (parts[0] || '');
sections[section] = (sections[section] || 0) + 1;
// Count by depth
const depth = parts.length;
depths[depth] = (depths[depth] || 0) + 1;
} catch (e) {
// Skip malformed URLs
}
}
return { sections, depths };
}
function findCommonPrefixes(urls, minCount = 3) {
const prefixes = {};
for (const url of urls) {
try {
const parsed = new URL(url);
const parts = parsed.pathname.split('/').filter(Boolean);
// Build progressive prefixes
let prefix = '';
for (let i = 0; i < Math.min(parts.length, 4); i++) {
prefix += '/' + parts[i];
prefixes[prefix] = (prefixes[prefix] || 0) + 1;
}
} catch (e) {}
}
// Filter to significant prefixes
return Object.entries(prefixes)
.filter(([_, count]) => count >= minCount)
.sort((a, b) => b[1] - a[1])
.slice(0, 15);
}
async function checkRobotsTxt(baseUrl) {
try {
const robotsUrl = new URL('/robots.txt', baseUrl).href;
const content = await fetch(robotsUrl);
const sitemapMatch = content.match(/Sitemap:\s*(.+)/i);
return sitemapMatch ? sitemapMatch[1].trim() : null;
} catch (e) {
return null;
}
}
async function findSitemap(baseUrl) {
// Check robots.txt first
const robotsSitemap = await checkRobotsTxt(baseUrl);
if (robotsSitemap) {
try {
const content = await fetch(robotsSitemap);
return { url: robotsSitemap, content };
} catch (e) {}
}
// Try common paths
for (const path of SITEMAP_PATHS) {
try {
const url = new URL(path, baseUrl).href;
const content = await fetch(url);
if (content.includes('<urlset') || content.includes('<sitemapindex')) {
return { url, content };
}
} catch (e) {}
}
return null;
}
async function main() {
const baseUrl = process.argv[2];
if (!baseUrl) {
console.log('Usage: node analyze-sitemap.js <base-url>');
console.log('Example: node analyze-sitemap.js https://docs.example.com');
process.exit(1);
}
console.log(`\n🔍 Analyzing sitemap for: ${baseUrl}\n`);
const sitemap = await findSitemap(baseUrl);
if (!sitemap) {
console.log('❌ No sitemap found. Checked:');
console.log(' - robots.txt for Sitemap directive');
SITEMAP_PATHS.forEach(p => console.log(` - ${p}`));
console.log('\n💡 Without a sitemap, try:');
console.log(` slurp ${baseUrl} --max 50`);
console.log(' Then inspect slurp_partials/ to understand structure.\n');
process.exit(0);
}
console.log(`✅ Found sitemap: ${sitemap.url}\n`);
// Check for sitemap index
if (sitemap.content.includes('<sitemapindex')) {
console.log('📑 This is a sitemap index. Child sitemaps:');
const childUrls = extractUrls(sitemap.content);
childUrls.slice(0, 10).forEach(u => console.log(` ${u}`));
if (childUrls.length > 10) console.log(` ... and ${childUrls.length - 10} more`);
console.log('\n💡 Fetch a specific child sitemap for detailed analysis.\n');
// Try to fetch first child for analysis
if (childUrls.length > 0) {
try {
const childContent = await fetch(childUrls[0]);
sitemap.content = childContent;
console.log(`📄 Analyzing first child sitemap: ${childUrls[0]}\n`);
} catch (e) {
process.exit(0);
}
}
}
const urls = extractUrls(sitemap.content);
console.log(`📊 Total URLs in sitemap: ${urls.length}\n`);
if (urls.length === 0) {
console.log('No URLs found in sitemap.');
process.exit(0);
}
const { sections, depths } = analyzePaths(urls, baseUrl);
console.log('📁 URLs by top-level section:');
Object.entries(sections)
.sort((a, b) => b[1] - a[1])
.forEach(([section, count]) => {
console.log(` ${section.padEnd(30)} ${count} pages`);
});
console.log('\n📏 URLs by path depth:');
Object.entries(depths)
.sort((a, b) => Number(a[0]) - Number(b[0]))
.forEach(([depth, count]) => {
console.log(` Depth ${depth}: ${count} pages`);
});
const prefixes = findCommonPrefixes(urls);
console.log('\n🎯 Suggested --base-path options:');
prefixes.slice(0, 8).forEach(([prefix, count]) => {
const fullPath = new URL(prefix, baseUrl).href;
console.log(` ${fullPath.padEnd(50)} (${count} pages)`);
});
console.log('\n💡 Recommended slurp commands:\n');
// Full site recommendation
if (urls.length <= 50) {
console.log(` # Full site (${urls.length} pages - manageable size)`);
console.log(` slurp ${baseUrl} --max ${Math.ceil(urls.length * 1.2)}\n`);
} else {
console.log(` # Full site (${urls.length} pages - consider scoping down)`);
console.log(` slurp ${baseUrl} --max ${urls.length}\n`);
}
// Section-specific recommendations
if (prefixes.length > 0) {
const [topPrefix, topCount] = prefixes[0];
const topPath = new URL(topPrefix, baseUrl).href;
console.log(` # Just "${topPrefix}" section (${topCount} pages)`);
console.log(` slurp ${topPath} --base-path ${topPath} --max ${Math.ceil(topCount * 1.2)}\n`);
}
// Sample URLs for context
console.log('📝 Sample URLs from sitemap:');
urls.slice(0, 8).forEach(u => console.log(` ${u}`));
if (urls.length > 8) console.log(` ... and ${urls.length - 8} more\n`);
}
main().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});