Skip to content

Commit 0fd798c

Browse files
lroolleclaude
andcommitted
feat: drop jina.ai dependency - direct .md for support, freeze blog
support.claude.com now serves a .md variant for every article (verified 2026-07-08: 200 text/markdown on all sitemap URLs), so support articles fetch directly like platform/code/mcp docs - no proxy, no rate limits, 355/355 instead of 4/355 through jina. anthropic.com blog (engineering/research/news/product) remains HTML-only (no llms.txt, no .md, no content negotiation), and the jina path had been ~95% dead from rate limiting anyway. Freeze it: content/blog/ stays as a static archive, sources disabled in sources.json with dated notes, --section choices trimmed. Full run after: 3231/3231 downloaded, 0 failed (was 2907/3638, 731 failed). Also removes the JINA_API_KEY workflow plumbing added earlier on this branch - no secret needed anymore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 58be5e6 commit 0fd798c

4 files changed

Lines changed: 213 additions & 188 deletions

File tree

.github/workflows/fetch-claude-docs.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ jobs:
4545
- name: Fetch all Anthropic docs
4646
env:
4747
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
48-
JINA_API_KEY: ${{ secrets.JINA_API_KEY }}
4948
run: uv run scripts/fetcher.py
5049

5150
- name: Setup GitHub CLI and Git
@@ -104,11 +103,9 @@ jobs:
104103
- `content/en/api/` - API reference (1500+ docs)
105104
- `content/en/build-with-claude/` - Platform features
106105
- `content/mcp/` - MCP protocol spec
107-
- `content/blog/engineering/` - Anthropic engineering posts
108-
- `content/blog/research/` - Research papers
109-
- `content/blog/news/` - Model releases, announcements
106+
- `content/blog/` - FROZEN archive (anthropic.com is HTML-only; no longer fetched)
110107
- `content/github/` - GitHub repos (cookbooks, skills, plugins, courses)
111-
- `content/support/` - Support articles
108+
- `content/support/` - Support articles (sitemap + .md)
112109
113110
## YOUR WORKFLOW
114111

CLAUDE.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
55
## Repository Purpose
66

77
Comprehensive archive of everything Anthropic publishes for building with
8-
Claude. 2,900+ docs from 11 sources, auto-updated every 6 hours.
8+
Claude. 2,900+ docs from 11 sources; active sources auto-updated four
9+
times daily (blog archive frozen, see Fetcher).
910

1011
## Fetcher
1112

1213
`scripts/fetcher.py` -- single-file multi-source fetcher.
1314

1415
Sources: code.claude.com, platform.claude.com, modelcontextprotocol.io,
15-
anthropic.com (engineering/research/news), support.claude.com,
16-
github.com/anthropics/* (10 repos).
16+
support.claude.com (sitemap + .md), github.com/anthropics/* (10 repos).
17+
anthropic.com blog (engineering/research/news) is a FROZEN archive as of
18+
2026-07: the site is HTML-only and the jina.ai proxy path was removed.
1719

1820
```bash
1921
uv run scripts/fetcher.py # Fetch everything
@@ -22,8 +24,7 @@ uv run scripts/fetcher.py --tree # Show sources
2224
uv run scripts/fetcher.py --discover # Probe for new sources
2325
```
2426

25-
Sections: `claude-code`, `api`, `platform`, `mcp`, `blog`, `engineering`,
26-
`research`, `news`, `github`, `support`, `all`
27+
Sections: `claude-code`, `api`, `platform`, `mcp`, `github`, `support`, `all`
2728

2829
Source registry: `sources.json`
2930
Architecture: `REFACTOR.md`
@@ -105,7 +106,7 @@ Use these paths to reference documentation when helping users:
105106
- `content/mcp/seps/` - Specification Enhancement Proposals
106107
- `content/mcp/community/` - Governance, working groups
107108

108-
#### Engineering & Research (from anthropic.com)
109+
#### Engineering & Research (from anthropic.com) — FROZEN archive, not auto-updated
109110
- `content/blog/engineering/` - "Building Effective Agents", tool use, harness design
110111
- `content/blog/research/` - Research papers
111112
- `content/blog/news/` - Model releases, announcements

scripts/fetcher.py

Lines changed: 20 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
- platform.claude.com -> API/platform docs (sitemap + .md suffix)
77
- code.claude.com -> Claude Code + Agent SDK (llms.txt + .md suffix)
88
- modelcontextprotocol.io -> MCP spec (sitemap + .md suffix)
9-
- anthropic.com -> Engineering, research, news (jina.ai proxy)
10-
- support.claude.com -> Help articles (jina.ai proxy)
9+
- support.claude.com -> Help articles (sitemap + .md suffix)
10+
- anthropic.com blog -> FROZEN 2026-07 (HTML-only, no .md variant;
11+
the jina.ai proxy path was removed)
1112
- github.com/anthropics/* -> Repos (raw.githubusercontent.com)
1213
1314
Usage:
@@ -57,12 +58,6 @@
5758
("anthropics/anthropic-sdk-typescript","main", [".md"]),
5859
]
5960

60-
NEWS_KEYWORDS = [
61-
"claude", "model", "api", "agent", "mcp", "sdk", "opus", "sonnet",
62-
"haiku", "code", "bedrock", "vertex", "foundry", "tool", "safety",
63-
"computer-use",
64-
]
65-
6661
DISCOVER_DOMAINS = [
6762
("anthropic.com", "Main site"),
6863
("platform.claude.com", "API platform docs"),
@@ -89,7 +84,6 @@ def __init__(
8984

9085
self.platform_sitemap_url = "https://platform.claude.com/sitemap.xml"
9186
self.claude_code_llms_url = "https://code.claude.com/docs/llms.txt"
92-
self.anthropic_sitemap_url = "https://www.anthropic.com/sitemap.xml"
9387
self.mcp_sitemap_url = "https://modelcontextprotocol.io/sitemap.xml"
9488
self.support_sitemap_url = "https://support.claude.com/sitemap.xml"
9589

@@ -107,20 +101,11 @@ async def fetch_text(self, session: aiohttp.ClientSession, url: str) -> str:
107101
r.raise_for_status()
108102
return await r.text()
109103

110-
async def fetch_bytes(
111-
self, session: aiohttp.ClientSession, url: str,
112-
headers: Optional[Dict] = None,
113-
) -> bytes:
114-
async with session.get(url, headers=headers) as r:
104+
async def fetch_bytes(self, session: aiohttp.ClientSession, url: str) -> bytes:
105+
async with session.get(url) as r:
115106
r.raise_for_status()
116107
return await r.read()
117108

118-
def _jina_headers(self) -> Optional[Dict]:
119-
# Without a key, r.jina.ai rate-limits hard (~95% failures at 10
120-
# concurrent). Set JINA_API_KEY to lift blog/support success rates.
121-
key = os.environ.get("JINA_API_KEY")
122-
return {"Authorization": f"Bearer {key}"} if key else None
123-
124109
def extract_sitemap_urls(self, xml: str, must_contain: str = "") -> List[str]:
125110
urls = []
126111
for line in xml.split("\n"):
@@ -139,26 +124,9 @@ async def fetch_claude_code_urls(self, session: aiohttp.ClientSession) -> List[s
139124
urls.append(match[1:-4]) # strip parens and .md
140125
return urls
141126

142-
def filter_blog_urls(self, sitemap_xml: str) -> Dict[str, List[str]]:
143-
result = {"engineering": [], "research": [], "news": [], "product": []}
144-
for line in sitemap_xml.split("\n"):
145-
if "<loc>" not in line:
146-
continue
147-
url = line.split("<loc>")[1].split("</loc>")[0]
148-
if "/engineering/" in url and url != "https://www.anthropic.com/engineering":
149-
result["engineering"].append(url)
150-
elif "/product/" in url:
151-
result["product"].append(url)
152-
elif "/news/" in url and url != "https://www.anthropic.com/news":
153-
slug = url.rsplit("/", 1)[-1].lower()
154-
if any(kw in slug for kw in NEWS_KEYWORDS):
155-
result["news"].append(url)
156-
elif "/research/" in url and url != "https://www.anthropic.com/research":
157-
if "/research/team/" not in url:
158-
result["research"].append(url)
159-
return result
160-
161127
def extract_support_urls(self, sitemap_xml: str) -> List[str]:
128+
# Articles serve a .md variant directly (since ~2026-07), so plain
129+
# download_doc applies; sitemap covers more articles than llms.txt.
162130
return [
163131
url for url in self.extract_sitemap_urls(sitemap_xml)
164132
if "/en/articles/" in url
@@ -210,55 +178,6 @@ async def download_doc(self, session, url, semaphore) -> Dict:
210178
self.stats["failed"] += 1
211179
return {"url": url, "status": "failed", "error": str(e)}
212180

213-
async def download_via_jina(self, session, url, output_subdir, semaphore) -> Dict:
214-
async with semaphore:
215-
slug = url.split("anthropic.com/")[1] if "anthropic.com" in url else url.rsplit("/", 1)[-1]
216-
output_path = self.output_dir / "blog" / f"{slug}.md"
217-
if output_subdir:
218-
output_path = self.output_dir / output_subdir / f"{slug}.md"
219-
if self.incremental and output_path.exists():
220-
self.stats["skipped"] += 1
221-
return {"url": url, "status": "skipped"}
222-
try:
223-
content = await self.fetch_bytes(
224-
session, f"https://r.jina.ai/{url}", headers=self._jina_headers())
225-
output_path.parent.mkdir(parents=True, exist_ok=True)
226-
async with aiofiles.open(output_path, "wb") as f:
227-
await f.write(content)
228-
self.stats["downloaded"] += 1
229-
return {
230-
"url": url, "status": "success",
231-
"path": str(output_path.relative_to(self.output_dir)),
232-
"sha256": hashlib.sha256(content).hexdigest(),
233-
"size": len(content),
234-
}
235-
except Exception as e:
236-
self.stats["failed"] += 1
237-
return {"url": url, "status": "failed", "error": str(e)}
238-
239-
async def download_support_article(self, session, url, semaphore) -> Dict:
240-
async with semaphore:
241-
output_path = self.get_output_path(url)
242-
if self.incremental and output_path.exists():
243-
self.stats["skipped"] += 1
244-
return {"url": url, "status": "skipped"}
245-
try:
246-
content = await self.fetch_bytes(
247-
session, f"https://r.jina.ai/{url}", headers=self._jina_headers())
248-
output_path.parent.mkdir(parents=True, exist_ok=True)
249-
async with aiofiles.open(output_path, "wb") as f:
250-
await f.write(content)
251-
self.stats["downloaded"] += 1
252-
return {
253-
"url": url, "status": "success",
254-
"path": str(output_path.relative_to(self.output_dir)),
255-
"sha256": hashlib.sha256(content).hexdigest(),
256-
"size": len(content),
257-
}
258-
except Exception as e:
259-
self.stats["failed"] += 1
260-
return {"url": url, "status": "failed", "error": str(e)}
261-
262181
async def download_github_file(self, session, repo, branch, filepath, semaphore) -> Dict:
263182
async with semaphore:
264183
repo_short = repo.split("/")[1]
@@ -345,7 +264,6 @@ async def fetch_all(self):
345264

346265
tasks = []
347266
semaphore = asyncio.Semaphore(self.jobs)
348-
jina_semaphore = asyncio.Semaphore(min(self.jobs, 10))
349267
counts = {}
350268

351269
# -- Platform docs --
@@ -382,20 +300,9 @@ async def fetch_all(self):
382300
for url in urls:
383301
tasks.append(self.download_doc(session, url, semaphore))
384302

385-
# -- Blog: engineering, research, news, product --
386-
if self.want("blog", "engineering", "research", "news"):
387-
print("Source: anthropic.com/sitemap.xml")
388-
xml = await self.fetch_text(session, self.anthropic_sitemap_url)
389-
blog = self.filter_blog_urls(xml)
390-
391-
for category, urls in blog.items():
392-
if not self.want("blog", category):
393-
continue
394-
counts[f"blog/{category}"] = len(urls)
395-
print(f" {len(urls)} {category}")
396-
for url in urls:
397-
tasks.append(self.download_via_jina(
398-
session, url, None, jina_semaphore))
303+
# -- Blog (anthropic.com): FROZEN 2026-07 --
304+
# HTML-only upstream (no llms.txt / .md variant); the jina.ai
305+
# proxy path was removed. content/blog/ stays as a static archive.
399306

400307
# -- Support articles --
401308
if self.want("support"):
@@ -405,8 +312,7 @@ async def fetch_all(self):
405312
counts["support"] = len(urls)
406313
print(f" {len(urls)} articles")
407314
for url in urls:
408-
tasks.append(self.download_support_article(
409-
session, url, jina_semaphore))
315+
tasks.append(self.download_doc(session, url, semaphore))
410316

411317
# -- GitHub repos --
412318
if self.want("github"):
@@ -558,10 +464,8 @@ async def show_tree(self):
558464
cc_urls = await self.fetch_claude_code_urls(session)
559465
mcp_urls = self.extract_sitemap_urls(
560466
await self.fetch_text(session, self.mcp_sitemap_url))
561-
anthropic_xml = await self.fetch_text(session, self.anthropic_sitemap_url)
562-
blog = self.filter_blog_urls(anthropic_xml)
563-
support_xml = await self.fetch_text(session, self.support_sitemap_url)
564-
support_urls = self.extract_support_urls(support_xml)
467+
support_urls = self.extract_support_urls(
468+
await self.fetch_text(session, self.support_sitemap_url))
565469

566470
def show_grouped(title, urls, strip_prefix):
567471
print(f"{title} ({len(urls)})")
@@ -579,16 +483,12 @@ def show_grouped(title, urls, strip_prefix):
579483
show_grouped("platform.claude.com", platform_urls, "https://platform.claude.com/docs/en/")
580484
show_grouped("modelcontextprotocol.io", mcp_urls, "https://modelcontextprotocol.io/")
581485

582-
print(f"anthropic.com blog")
583-
print("-" * 50)
584-
for cat, urls in blog.items():
585-
print(f" {cat}: {len(urls)}")
586-
print()
587486
print(f"support.claude.com: {len(support_urls)} articles")
487+
print("anthropic.com blog: frozen archive (not fetched)")
588488
print(f"GitHub repos: {len(GITHUB_REPOS)} repos configured")
589489
print()
590490

591-
total = len(cc_urls) + len(platform_urls) + len(mcp_urls) + sum(len(v) for v in blog.values()) + len(support_urls)
491+
total = len(cc_urls) + len(platform_urls) + len(mcp_urls) + len(support_urls)
592492
print(f"Total fetchable: {total}+ (excludes GitHub repos)")
593493

594494
# -- Discovery ---------------------------------------------------------
@@ -702,19 +602,18 @@ async def main():
702602
claude-code Claude Code + Agent SDK docs (code.claude.com)
703603
api/platform API and platform docs (platform.claude.com)
704604
mcp MCP protocol spec (modelcontextprotocol.io)
705-
blog All blog content (engineering + research + news + product)
706-
engineering Engineering blog only
707-
research Research posts only
708-
news Filtered news (model releases, Claude updates)
709605
github All configured GitHub repos
710-
support Support articles (support.claude.com)
606+
support Support articles (support.claude.com, sitemap + .md)
711607
all Everything (default)
712608
609+
Note: content/blog/ (anthropic.com engineering/research/news) is a
610+
frozen archive as of 2026-07 — the site is HTML-only and the jina.ai
611+
proxy path was removed.
612+
713613
Examples:
714614
fetcher.py Fetch everything
715615
fetcher.py --section mcp MCP spec only
716616
fetcher.py --section github GitHub repos only
717-
fetcher.py --section engineering Engineering blog only
718617
fetcher.py --tree Show all sources
719618
fetcher.py --discover Probe domains for new sources
720619
fetcher.py --incremental Skip existing files
@@ -728,7 +627,6 @@ async def main():
728627
"--section", "-s",
729628
choices=[
730629
"claude-code", "api", "platform", "mcp",
731-
"blog", "engineering", "research", "news",
732630
"github", "support", "all",
733631
],
734632
)

0 commit comments

Comments
 (0)