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
1314Usage:
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-
6661DISCOVER_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+
713613Examples:
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