fetch-url-as-markdown
Fetch a web page (URL) and return clean Markdown via local trafilatura, with Exa MCP as a fallback for JS-rendered or anti-bot pages. Use when the user asks to read, fetch, scrape, summarize, or quote a URL — prefer this over the built-in WebFetch tool. Don't use for binary files
Install
npx skills add https://github.com/CodeAlive-AI/ai-driven-development/tree/main/skills/fetch-url-as-markdown
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install codealive-ai-ai-driven-development@llmmart
git clone https://github.com/CodeAlive-AI/ai-driven-development.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole codealive-ai/ai-driven-development collection as a plugin from our marketplace. Git is the plain clone.
README
fetch-url-as-markdown
Fetch any web URL and get clean, readable Markdown — main content only, no navigation, ads, or footer. Runs locally on trafilatura with a real-browser User-Agent and structured exit codes that tell the host agent when to fall back to a remote crawler (Exa MCP).
Install
npx skills add CodeAlive-AI/ai-driven-development@fetch-url-as-markdown -g -y
Prerequisites
Python 3.10+
trafilatura≥ 2.0:python3 -m pip install --break-system-packages trafilatura(The script exits with code
3and prints this hint if the import fails.)Optional — an Exa MCP server in your agent host (e.g.
mcp__exa__web_search_advanced_exa). Used only as a fallback when local extraction can't recover the page.
Quick start
After installing, ask your agent things like:
> Read https://github.com/adbar/trafilatura and summarize the README
> Fetch https://docs.python.org/3/library/json.html and quote the section on encoders
> Pull this blog post as Markdown so I can paste it into my notes
The agent will run the bundled script directly:
python3 ~/.claude/skills/fetch-url-as-markdown/scripts/fetch_url.py "https://example.com"
python3 ~/.claude/skills/fetch-url-as-markdown/scripts/fetch_url.py "https://example.com" --no-metadata --min-body 0
What it does
One entry point — a single CLI script with one job: URL → clean Markdown to stdout.
| Stage | Behaviour |
|---|---|
| Download | trafilatura.fetch_response() with a real Chrome User-Agent and 30 s timeout (config in scripts/settings.cfg) |
| Content-Type guard | Anything outside text/html \| application/xhtml+xml \| text/plain \| application/xml \| text/xml is rejected up-front (exit 4) so PDFs/images/archives don't get mis-parsed as HTML |
| Anti-stub guard | Sniffs the raw HTML for Cloudflare / "Please enable JavaScript" / Imperva / DataDome wall markers and bails with exit 2 instead of returning a useless 30-character "Just a moment…" page |
| Extract | trafilatura.extract(output_format="markdown", include_formatting=True, include_links=True, include_tables=True, favor_recall=True, deduplicate=True, with_metadata=True) — keeps headings/lists/code where the source HTML uses real <h1..h6>, with a YAML frontmatter (title, author, date, url, hostname) on top |
| Min-body guard | Bodies under 50 chars (configurable via --min-body N, 0 to disable) are treated as stubs → exit 2 |
Exit codes (the contract for the host agent)
| Code | Meaning | Recommended action |
|---|---|---|
0 |
Markdown printed to stdout | done |
1 |
DownloadError — network/HTTP/timeout/anti-bot block at fetch |
fall back to Exa MCP |
2 |
ExtractionError — empty extract, JS/Cloudflare wall, or stub body |
fall back to Exa MCP |
3 |
trafilatura not installed | install (see Prerequisites), then retry |
4 |
UnsupportedContentTypeError — URL is binary |
don't fall back to Exa; route to a content-specific skill (e.g. pdf for PDFs) |
SKILL.md instructs the agent on this fallback flow, so for the common case the user just says "fetch this URL" and gets Markdown — local first, Exa second, no manual orchestration.
Key features
- Local-first, free, no API key needed for the happy path — extraction runs entirely on
trafilatura≥ 2.0 - Real browser User-Agent baked into
settings.cfg— fixes the silent failure wheregithub.comand other anti-bot sites return empty bodies for trafilatura's default UA - Structured exit codes 0/1/2/3/4 — the script tells the host agent why it failed, so the fallback decision is mechanical, not interpretive
- Content-Type and anti-stub guards — prevent the classic "trafilatura returned 30 chars from a Cloudflare interstitial, so we silently passed garbage downstream" failure mode
- Defaults tuned for LLM-friendly output —
include_formatting=True,favor_recall=True,deduplicate=True, YAML metadata header on by default - Drop-in replacement for the built-in
WebFetch— the description inSKILL.mdinstructs the agent to prefer this skill whenever the user asks to "read / fetch / scrape / summarize / quote a URL"
Sources and methodology
- trafilatura by Adrien Barbaresi — GitHub, docs. Configuration patterns (
use_config,settings.cfg,USER_AGENTS) follow the official Settings and Downloads docs. - Extract flag selection — informed by Barbaresi 2021 (ACL anthology) and the Bevendorff et al. 2023 extraction benchmark, which rank trafilatura first among open-source extractors on ROUGE-LSum.
- Real-world reference implementation —
vakovalskii/searcharvester(simple_tavily_adapter/main.py) usestrafilatura.extract(output_format="markdown", include_formatting=True, include_links=True, include_tables=True, favor_recall=True)for its/extractand/searchendpoints — the same flag set we ship as default. - Anti-stub markers — collected from Cloudflare interstitial copy ("Just a moment…", "Verifying you are human"), Imperva (
Incapsula Incident ID), DataDome (captcha-delivery.com) and standard<noscript>patterns. Matched on a case-insensitive snippet of the first 8 KB of the response body.
File structure
skills/fetch-url-as-markdown/
├── SKILL.md # agent-facing contract (workflow, exit-code routing)
├── README.md # this file
└── scripts/
├── fetch_url.py # CLI entry point
└── settings.cfg # trafilatura config: real-browser UA, 30s timeout, retries
License
MIT
Skill manifest
URL to Markdown
Fetch any web URL and get clean, readable Markdown — main content only, no navigation/footer/ads. Local + free by default; smart fallback to Exa MCP when the page can't be extracted locally.
Workflow (the only thing the agent needs to remember)
Try trafilatura first:
python3 ~/.claude/skills/fetch-url-as-markdown/scripts/fetch_url.py "<URL>"If exit code is 1 or 2 → fall back to Exa MCP with the same URL:
mcp__exa__web_search_advanced_exa( query="<URL>", includeDomains=["<host of URL>"], numResults=1, textMaxCharacters=50000, type="auto" )(
mcp__exa__crawlingworks too if the server exposes it; theweb_search_advanced_exacall above is the always-available variant — pin the host withincludeDomainsand use the URL itself as the query.)Exit code
3means trafilatura is not installed — install once:python3 -m pip install --break-system-packages trafilatura
Exit codes (what they mean for the fallback decision)
| Code | Meaning | Action |
|---|---|---|
| 0 | Markdown printed to stdout | done |
| 1 | DownloadError — network/HTTP/timeout/anti-bot block at fetch | fall back to Exa |
| 2 | ExtractionError — empty extract, JS/Cloudflare wall, or stub body (<200 chars) | fall back to Exa |
| 3 | trafilatura missing | install (see above), then retry |
| 4 | UnsupportedContentTypeError — URL is binary (PDF, image, archive) | don't fall back to Exa; use the right specialized skill (e.g. pdf for PDFs) |
Defaults baked into the script
output_format="markdown",include_formatting=True— keeps headings/lists/code structure where the source HTML uses real<h1..h6>etc.include_links=True,include_tables=Truewith_metadata=True→ emits a YAML frontmatter (title,author,date,url,hostname)favor_recall=True,deduplicate=True— readable but trims duplicates- Real-browser User-Agent + 30s timeout configured in
scripts/settings.cfg - Anti-stub guards (built into the script):
- rejects
Content-Typeother thantext/html|application/xhtml+xml|text/plain|application/xml|text/xml→ exit4 - sniffs raw HTML for Cloudflare / "Please enable JavaScript" / Imperva / DataDome wall markers → exit
2 - rejects extracted bodies under 50 chars (configurable via
--min-body N,0to disable) → exit2
- rejects
Useful flags
... fetch_url.py "<URL>" --no-links # strip hyperlinks
... fetch_url.py "<URL>" --no-tables # strip tables
... fetch_url.py "<URL>" --no-metadata # omit YAML header
... fetch_url.py "<URL>" --comments # include user comments (off by default — usually noise)
... fetch_url.py "<URL>" --images # include image refs (experimental)
... fetch_url.py "<URL>" --precision # terser output, drops borderline content
When to choose what
| Situation | Tool |
|---|---|
| Article, blog post, docs, README, wiki | trafilatura (default) — local, free |
| JS-heavy SPA, login-walled, Cloudflare | Exa fallback (the script will signal exit 2) |
| Bulk / many URLs | trafilatura — no quota, no API key |
| Already failed twice on a domain | Exa directly |
Files (ai-driven-development)
-
scripts
-
fetch_url.py 8.3 KB
#!/usr/bin/env python3 """Convert a URL to Markdown using trafilatura (https://github.com/adbar/trafilatura). Default extractor for fetch-url-as-markdown skill. Runs entirely locally, no API key required. Sends a real-browser User-Agent (configured in settings.cfg next to this script) so anti-bot sites like github.com return real HTML instead of a stub. Exit codes: 0 success — Markdown printed to stdout 1 download failed (network, HTTP 4xx/5xx, timeout, anti-bot 403) 2 extraction failed — page downloaded but yielded no usable main content (SPA shell, JS-only render, captcha/Cloudflare wall, login wall) 4 unsupported content type (binary: PDF, image, archive, video, octet-stream) 3 trafilatura is not installed The skill's fallback (Exa MCP crawl) should be tried on exit 1 or 2 — not on 4. """ import argparse import re import sys from pathlib import Path try: import trafilatura from trafilatura.settings import use_config except ImportError: sys.stderr.write( "trafilatura not installed. Install with:\n" " python3 -m pip install --break-system-packages trafilatura\n" ) sys.exit(3) SETTINGS_PATH = Path(__file__).with_name("settings.cfg") _CONFIG = use_config(str(SETTINGS_PATH)) if SETTINGS_PATH.exists() else use_config() # Content types we accept. Anything outside this set triggers UnsupportedContentTypeError. _HTML_CT_RE = re.compile( r"^(text/html|application/xhtml\+xml|text/plain|application/xml|text/xml)\b", re.IGNORECASE, ) # Substrings that strongly indicate the page is a JS/anti-bot wall, not real content. # Matched on a case-insensitive normalized snippet of the *raw HTML*. _WALL_MARKERS = ( "please enable javascript", "javascript is required", "javascript is disabled", "enable javascript to run", "you need to enable javascript", "checking your browser before accessing", # Cloudflare interstitial "just a moment...", # Cloudflare interstitial "verifying you are human", # Cloudflare Turnstile "verify you are a human", "attention required! | cloudflare", "ddos protection by cloudflare", "access denied | cloudflare", "ray id:", # cloudflare error page "captcha-delivery.com", # DataDome captcha "incapsula incident id", # Imperva "<title>access denied</title>", "<title>403 forbidden</title>", "<title>error 1020</title>", # Cloudflare access denied ) # Below this length the extracted markdown is almost certainly a stub # (e.g. "Loading..." or "Please enable JS"). Tuned low enough to accept # tiny-but-valid pages like example.com (~110 body chars) while still # catching SPA shells. _MIN_MARKDOWN_BODY_CHARS = 50 class DownloadError(RuntimeError): """Network/HTTP/anti-bot failure — could not retrieve the page bytes.""" class UnsupportedContentTypeError(RuntimeError): """The URL points to a binary/non-HTML resource (PDF, image, archive, …).""" class ExtractionError(RuntimeError): """HTML retrieved, but contains no extractable main content (or a JS/anti-bot wall).""" def _looks_like_wall(html: str) -> str | None: """Return the marker that matched, or None if no wall detected.""" snippet = html[:8000].lower() for marker in _WALL_MARKERS: if marker in snippet: return marker return None def _markdown_body_chars(markdown: str, with_metadata: bool) -> int: """Count characters in the body of the Markdown, excluding the YAML frontmatter.""" if with_metadata and markdown.startswith("---"): # Strip the leading YAML block: from the first '---' to the next one end = markdown.find("\n---", 3) if end != -1: return len(markdown[end + 4 :].strip()) return len(markdown.strip()) def fetch_url_as_markdown( url: str, include_links: bool = True, include_tables: bool = True, include_images: bool = False, include_comments: bool = False, with_metadata: bool = True, favor_precision: bool = False, favor_recall: bool = True, min_body_chars: int = _MIN_MARKDOWN_BODY_CHARS, ) -> str: """Fetch a URL and return its main content as Markdown. Defaults aim at "clean readable article body": metadata header on, structural formatting (headings, lists) on, links on, recall favored. Raises: DownloadError: page could not be retrieved. UnsupportedContentTypeError: response is binary (PDF, image, …). ExtractionError: HTML retrieved but no usable main content (empty extract, JS/anti-bot wall, or below min_body_chars). """ response = trafilatura.fetch_response( url, with_headers=True, decode=True, config=_CONFIG ) if response is None or response.html is None: raise DownloadError(f"Failed to download: {url}") content_type = (response.headers or {}).get("content-type", "") if content_type and not _HTML_CT_RE.match(content_type): raise UnsupportedContentTypeError( f"Unsupported content-type {content_type!r} at {url} — " "this skill only handles HTML/XML." ) html = response.html wall_marker = _looks_like_wall(html) if wall_marker is not None: raise ExtractionError( f"Anti-bot or JS wall detected at {url} (marker: {wall_marker!r})" ) markdown = trafilatura.extract( html, url=url, output_format="markdown", include_formatting=True, include_links=include_links, include_tables=include_tables, include_images=include_images, include_comments=include_comments, with_metadata=with_metadata, favor_precision=favor_precision, favor_recall=favor_recall and not favor_precision, deduplicate=True, config=_CONFIG, ) if not markdown: raise ExtractionError(f"No extractable main content at: {url}") body_chars = _markdown_body_chars(markdown, with_metadata) if body_chars < min_body_chars: raise ExtractionError( f"Extracted body is too short ({body_chars} chars < {min_body_chars}) " f"at {url} — likely a JS-only render or stub page." ) return markdown def main() -> None: parser = argparse.ArgumentParser( description="Fetch a URL and print clean Markdown to stdout (trafilatura).", ) parser.add_argument("url", help="URL to fetch") parser.add_argument( "--no-links", action="store_true", help="Strip hyperlinks from output" ) parser.add_argument( "--no-tables", action="store_true", help="Strip tables from output" ) parser.add_argument( "--images", action="store_true", help="Include images (experimental)" ) parser.add_argument( "--comments", action="store_true", help="Include user comments" ) parser.add_argument( "--no-metadata", action="store_true", help="Skip YAML metadata header" ) parser.add_argument( "--precision", action="store_true", help="Favor precision over recall (terser, drops borderline content)", ) parser.add_argument( "--min-body", type=int, default=_MIN_MARKDOWN_BODY_CHARS, help=f"Minimum body chars for a successful extraction (default: {_MIN_MARKDOWN_BODY_CHARS}). " "Set to 0 to disable the stub-page check.", ) args = parser.parse_args() try: markdown = fetch_url_as_markdown( args.url, include_links=not args.no_links, include_tables=not args.no_tables, include_images=args.images, include_comments=args.comments, with_metadata=not args.no_metadata, favor_precision=args.precision, min_body_chars=args.min_body, ) except DownloadError as e: sys.stderr.write(f"DownloadError: {e}\n") sys.exit(1) except ExtractionError as e: sys.stderr.write(f"ExtractionError: {e}\n") sys.exit(2) except UnsupportedContentTypeError as e: sys.stderr.write(f"UnsupportedContentTypeError: {e}\n") sys.exit(4) sys.stdout.write(markdown) if not markdown.endswith("\n"): sys.stdout.write("\n") if __name__ == "__main__": main() -
settings.cfg 893 B
# Settings for trafilatura — all keys live under [DEFAULT]. # See https://trafilatura.readthedocs.io/en/latest/settings.html [DEFAULT] # Download DOWNLOAD_TIMEOUT = 30 MAX_FILE_SIZE = 20000000 MIN_FILE_SIZE = 10 # sleep between requests (used in batch mode; single-URL CLI usage rarely hits it) SLEEP_TIME = 2.0 # one line per user-agent — real-browser UA so anti-bot sites (github.com, # Cloudflare, etc.) don't return empty bodies for the default UA. USER_AGENTS = Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 COOKIE = MAX_REDIRECTS = 5 # Extraction MIN_EXTRACTED_SIZE = 200 MIN_EXTRACTED_COMM_SIZE = 1 MIN_OUTPUT_SIZE = 1 MIN_OUTPUT_COMM_SIZE = 1 MAX_TREE_SIZE = EXTRACTION_TIMEOUT = 30 # Deduplication MIN_DUPLCHECK_SIZE = 100 MAX_REPETITIONS = 2 EXTENSIVE_DATE_SEARCH = on EXTERNAL_URLS = off
-
-
README.md 6 KB
# fetch-url-as-markdown Fetch any web URL and get clean, readable Markdown — main content only, no navigation, ads, or footer. Runs locally on [trafilatura](https://github.com/adbar/trafilatura) with a real-browser User-Agent and structured exit codes that tell the host agent when to fall back to a remote crawler (Exa MCP). ## Install ```bash npx skills add CodeAlive-AI/ai-driven-development@fetch-url-as-markdown -g -y ``` ## Prerequisites - Python 3.10+ - [`trafilatura`](https://pypi.org/project/trafilatura/) ≥ 2.0: ```bash python3 -m pip install --break-system-packages trafilatura ``` (The script exits with code `3` and prints this hint if the import fails.) - *Optional* — an Exa MCP server in your agent host (e.g. `mcp__exa__web_search_advanced_exa`). Used only as a fallback when local extraction can't recover the page. ## Quick start After installing, ask your agent things like: ``` > Read https://github.com/adbar/trafilatura and summarize the README > Fetch https://docs.python.org/3/library/json.html and quote the section on encoders > Pull this blog post as Markdown so I can paste it into my notes ``` The agent will run the bundled script directly: ```bash python3 ~/.claude/skills/fetch-url-as-markdown/scripts/fetch_url.py "https://example.com" python3 ~/.claude/skills/fetch-url-as-markdown/scripts/fetch_url.py "https://example.com" --no-metadata --min-body 0 ``` ## What it does One entry point — a single CLI script with one job: *URL → clean Markdown to stdout.* | Stage | Behaviour | |---|---| | **Download** | `trafilatura.fetch_response()` with a real Chrome User-Agent and 30 s timeout (config in `scripts/settings.cfg`) | | **Content-Type guard** | Anything outside `text/html \| application/xhtml+xml \| text/plain \| application/xml \| text/xml` is rejected up-front (exit `4`) so PDFs/images/archives don't get mis-parsed as HTML | | **Anti-stub guard** | Sniffs the raw HTML for Cloudflare / "Please enable JavaScript" / Imperva / DataDome wall markers and bails with exit `2` instead of returning a useless 30-character "Just a moment…" page | | **Extract** | `trafilatura.extract(output_format="markdown", include_formatting=True, include_links=True, include_tables=True, favor_recall=True, deduplicate=True, with_metadata=True)` — keeps headings/lists/code where the source HTML uses real `<h1..h6>`, with a YAML frontmatter (title, author, date, url, hostname) on top | | **Min-body guard** | Bodies under 50 chars (configurable via `--min-body N`, `0` to disable) are treated as stubs → exit `2` | ### Exit codes (the contract for the host agent) | Code | Meaning | Recommended action | |---:|---|---| | `0` | Markdown printed to stdout | done | | `1` | `DownloadError` — network/HTTP/timeout/anti-bot block at fetch | fall back to Exa MCP | | `2` | `ExtractionError` — empty extract, JS/Cloudflare wall, or stub body | fall back to Exa MCP | | `3` | trafilatura not installed | install (see Prerequisites), then retry | | `4` | `UnsupportedContentTypeError` — URL is binary | **don't** fall back to Exa; route to a content-specific skill (e.g. `pdf` for PDFs) | `SKILL.md` instructs the agent on this fallback flow, so for the common case the user just says "fetch this URL" and gets Markdown — local first, Exa second, no manual orchestration. ## Key features - **Local-first, free, no API key needed for the happy path** — extraction runs entirely on `trafilatura` ≥ 2.0 - **Real browser User-Agent baked into `settings.cfg`** — fixes the silent failure where `github.com` and other anti-bot sites return empty bodies for trafilatura's default UA - **Structured exit codes 0/1/2/3/4** — the script tells the host agent *why* it failed, so the fallback decision is mechanical, not interpretive - **Content-Type and anti-stub guards** — prevent the classic "trafilatura returned 30 chars from a Cloudflare interstitial, so we silently passed garbage downstream" failure mode - **Defaults tuned for LLM-friendly output** — `include_formatting=True`, `favor_recall=True`, `deduplicate=True`, YAML metadata header on by default - **Drop-in replacement for the built-in `WebFetch`** — the description in `SKILL.md` instructs the agent to prefer this skill whenever the user asks to "read / fetch / scrape / summarize / quote a URL" ## Sources and methodology - **trafilatura** by Adrien Barbaresi — [GitHub](https://github.com/adbar/trafilatura), [docs](https://trafilatura.readthedocs.io). Configuration patterns (`use_config`, `settings.cfg`, `USER_AGENTS`) follow the official [Settings](https://trafilatura.readthedocs.io/en/latest/settings.html) and [Downloads](https://trafilatura.readthedocs.io/en/latest/downloads.html) docs. - **Extract flag selection** — informed by Barbaresi 2021 ([ACL anthology](https://aclanthology.org/2021.acl-demo.15/)) and the [Bevendorff et al. 2023 extraction benchmark](https://webis.de/downloads/publications/papers/bevendorff_2023b.pdf), which rank trafilatura first among open-source extractors on ROUGE-LSum. - **Real-world reference implementation** — [`vakovalskii/searcharvester`](https://github.com/vakovalskii/searcharvester) (`simple_tavily_adapter/main.py`) uses `trafilatura.extract(output_format="markdown", include_formatting=True, include_links=True, include_tables=True, favor_recall=True)` for its `/extract` and `/search` endpoints — the same flag set we ship as default. - **Anti-stub markers** — collected from Cloudflare interstitial copy ("Just a moment…", "Verifying you are human"), Imperva (`Incapsula Incident ID`), DataDome (`captcha-delivery.com`) and standard `<noscript>` patterns. Matched on a case-insensitive snippet of the first 8 KB of the response body. ## File structure ``` skills/fetch-url-as-markdown/ ├── SKILL.md # agent-facing contract (workflow, exit-code routing) ├── README.md # this file └── scripts/ ├── fetch_url.py # CLI entry point └── settings.cfg # trafilatura config: real-browser UA, 30s timeout, retries ``` ## License MIT -
SKILL.md 3.6 KB
--- name: fetch-url-as-markdown description: Fetch a web page (URL) and return clean Markdown via local trafilatura, with Exa MCP as a fallback for JS-rendered or anti-bot pages. Use when the user asks to read, fetch, scrape, summarize, or quote a URL — prefer this over the built-in WebFetch tool. Don't use for binary files (PDFs, images, archives) or for fetching API/JSON endpoints. --- # URL to Markdown Fetch any web URL and get clean, readable Markdown — main content only, no navigation/footer/ads. Local + free by default; smart fallback to Exa MCP when the page can't be extracted locally. ## Workflow (the only thing the agent needs to remember) 1. **Try trafilatura first**: ```bash python3 ~/.claude/skills/fetch-url-as-markdown/scripts/fetch_url.py "<URL>" ``` 2. **If exit code is 1 or 2 → fall back to Exa MCP** with the same URL: ``` mcp__exa__web_search_advanced_exa( query="<URL>", includeDomains=["<host of URL>"], numResults=1, textMaxCharacters=50000, type="auto" ) ``` (`mcp__exa__crawling` works too if the server exposes it; the `web_search_advanced_exa` call above is the always-available variant — pin the host with `includeDomains` and use the URL itself as the query.) 3. Exit code `3` means trafilatura is not installed — install once: ```bash python3 -m pip install --break-system-packages trafilatura ``` ## Exit codes (what they mean for the fallback decision) | Code | Meaning | Action | |---|---|---| | 0 | Markdown printed to stdout | done | | 1 | DownloadError — network/HTTP/timeout/anti-bot block at fetch | fall back to Exa | | 2 | ExtractionError — empty extract, JS/Cloudflare wall, or stub body (<200 chars) | fall back to Exa | | 3 | trafilatura missing | install (see above), then retry | | 4 | UnsupportedContentTypeError — URL is binary (PDF, image, archive) | **don't** fall back to Exa; use the right specialized skill (e.g. `pdf` for PDFs) | ## Defaults baked into the script - `output_format="markdown"`, `include_formatting=True` — keeps headings/lists/code structure where the source HTML uses real `<h1..h6>` etc. - `include_links=True`, `include_tables=True` - `with_metadata=True` → emits a YAML frontmatter (`title`, `author`, `date`, `url`, `hostname`) - `favor_recall=True`, `deduplicate=True` — readable but trims duplicates - Real-browser User-Agent + 30s timeout configured in `scripts/settings.cfg` - Anti-stub guards (built into the script): - rejects `Content-Type` other than `text/html|application/xhtml+xml|text/plain|application/xml|text/xml` → exit `4` - sniffs raw HTML for Cloudflare / "Please enable JavaScript" / Imperva / DataDome wall markers → exit `2` - rejects extracted bodies under 50 chars (configurable via `--min-body N`, `0` to disable) → exit `2` ## Useful flags ```bash ... fetch_url.py "<URL>" --no-links # strip hyperlinks ... fetch_url.py "<URL>" --no-tables # strip tables ... fetch_url.py "<URL>" --no-metadata # omit YAML header ... fetch_url.py "<URL>" --comments # include user comments (off by default — usually noise) ... fetch_url.py "<URL>" --images # include image refs (experimental) ... fetch_url.py "<URL>" --precision # terser output, drops borderline content ``` ## When to choose what | Situation | Tool | |---|---| | Article, blog post, docs, README, wiki | trafilatura (default) — local, free | | JS-heavy SPA, login-walled, Cloudflare | Exa fallback (the script will signal exit 2) | | Bulk / many URLs | trafilatura — no quota, no API key | | Already failed twice on a domain | Exa directly |
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.