[{"data":1,"prerenderedAt":2186},["ShallowReactive",2],{"api-blog-how-to-extract-brand-assets-in-bulk":3,"api-blog-related-how-to-extract-brand-assets-in-bulk":529},{"id":4,"title":5,"author":6,"body":7,"category":498,"date":499,"dateModified":500,"description":501,"extension":502,"faqs":503,"image":500,"meta":515,"navigation":85,"path":516,"readingTime":107,"relatedSlugs":517,"seo":521,"stem":522,"tags":523,"__hash__":528},"apiBlog/api-blog/how-to-extract-brand-assets-in-bulk.md","How to Extract Brand Assets from 1,000 URLs in Bulk (Python)","Soraia",{"type":8,"value":9,"toc":485},"minimark",[10,14,19,33,37,62,318,329,333,354,360,370,374,429,432,436,444,448,453,456,460,463,467,470,474,481],[11,12,13],"p",{},"The person with 1,000 URLs is never the person who wants to babysit 1,000 browser tabs. Bulk brand extraction in Python is an asyncio problem wearing a design-data hat: a semaphore for rate limits, retries for the 10% of URLs that misbehave, and checkpointing so a crash at URL 900 does not cost you the first 899. Here is the working script, then the failure math.",[15,16,18],"h2",{"id":17},"why-not-1000-playwright-launches","Why not 1,000 Playwright launches",[11,20,21,22,27,28,32],{},"The ",[23,24,26],"a",{"href":25},"/api/blog/how-to-extract-design-tokens-from-any-website-in-python","single-site Playwright approach"," does not scale linearly. A thousand headless Chromium sessions means gigabytes of RAM, cookie banners in six languages, and a long tail of sites that hang ",[29,30,31],"code",{},"networkidle"," forever. It is buildable - a browser pool, per-site timeouts, crash recovery - but at that point you are maintaining scraping infrastructure as a hobby. For bulk, put the browser problem on someone else's side of the API.",[15,34,36],{"id":35},"the-script","The script",[38,39,44],"pre",{"className":40,"code":41,"language":42,"meta":43,"style":43},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","pip install httpx\n","bash","",[29,45,46],{"__ignoreMap":43},[47,48,51,55,59],"span",{"class":49,"line":50},"line",1,[47,52,54],{"class":53},"sBMFI","pip",[47,56,58],{"class":57},"sfazB"," install",[47,60,61],{"class":57}," httpx\n",[38,63,67],{"className":64,"code":65,"language":66,"meta":43,"style":43},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import asyncio, csv, json\nimport httpx\n\nAPI = \"https://miromiro.app/api/v1/brand\"\nKEY = \"mm_live_your_key\"\nCONCURRENCY = 5          # stay under your plan's req/min cap\nRETRIES = 3\n\nasync def extract(client, sem, url):\n    async with sem:\n        for attempt in range(RETRIES):\n            try:\n                r = await client.get(\n                    API,\n                    params={\"url\": url, \"fields\": \"logo,palette\"},\n                    headers={\"Authorization\": f\"Bearer {KEY}\"},\n                    timeout=60,\n                )\n                if r.status_code == 429:            # rate limited: back off\n                    await asyncio.sleep(2 ** attempt * 5)\n                    continue\n                if r.status_code == 200:\n                    return {\"url\": url, \"ok\": True, \"data\": r.json()}\n                return {\"url\": url, \"ok\": False, \"error\": f\"HTTP {r.status_code}\"}\n            except httpx.HTTPError as e:\n                if attempt == RETRIES - 1:\n                    return {\"url\": url, \"ok\": False, \"error\": str(e)}\n                await asyncio.sleep(2 ** attempt)\n\nasync def main(urls):\n    sem = asyncio.Semaphore(CONCURRENCY)\n    async with httpx.AsyncClient() as client:\n        results = await asyncio.gather(*(extract(client, sem, u) for u in urls))\n    with open(\"brands.jsonl\", \"w\") as f:\n        for row in results:\n            f.write(json.dumps(row) + \"\\n\")\n    failed = [r for r in results if not r[\"ok\"]]\n    print(f\"{len(results) - len(failed)} ok, {len(failed)} failed\")\n\nif __name__ == \"__main__\":\n    urls = [row[0] for row in csv.reader(open(\"domains.csv\"))]\n    asyncio.run(main(urls))\n","python",[29,68,69,74,80,87,93,99,105,111,116,122,128,134,140,146,152,158,164,170,176,182,188,194,200,206,212,218,224,230,236,241,247,253,259,265,271,277,283,289,295,300,306,312],{"__ignoreMap":43},[47,70,71],{"class":49,"line":50},[47,72,73],{},"import asyncio, csv, json\n",[47,75,77],{"class":49,"line":76},2,[47,78,79],{},"import httpx\n",[47,81,83],{"class":49,"line":82},3,[47,84,86],{"emptyLinePlaceholder":85},true,"\n",[47,88,90],{"class":49,"line":89},4,[47,91,92],{},"API = \"https://miromiro.app/api/v1/brand\"\n",[47,94,96],{"class":49,"line":95},5,[47,97,98],{},"KEY = \"mm_live_your_key\"\n",[47,100,102],{"class":49,"line":101},6,[47,103,104],{},"CONCURRENCY = 5          # stay under your plan's req/min cap\n",[47,106,108],{"class":49,"line":107},7,[47,109,110],{},"RETRIES = 3\n",[47,112,114],{"class":49,"line":113},8,[47,115,86],{"emptyLinePlaceholder":85},[47,117,119],{"class":49,"line":118},9,[47,120,121],{},"async def extract(client, sem, url):\n",[47,123,125],{"class":49,"line":124},10,[47,126,127],{},"    async with sem:\n",[47,129,131],{"class":49,"line":130},11,[47,132,133],{},"        for attempt in range(RETRIES):\n",[47,135,137],{"class":49,"line":136},12,[47,138,139],{},"            try:\n",[47,141,143],{"class":49,"line":142},13,[47,144,145],{},"                r = await client.get(\n",[47,147,149],{"class":49,"line":148},14,[47,150,151],{},"                    API,\n",[47,153,155],{"class":49,"line":154},15,[47,156,157],{},"                    params={\"url\": url, \"fields\": \"logo,palette\"},\n",[47,159,161],{"class":49,"line":160},16,[47,162,163],{},"                    headers={\"Authorization\": f\"Bearer {KEY}\"},\n",[47,165,167],{"class":49,"line":166},17,[47,168,169],{},"                    timeout=60,\n",[47,171,173],{"class":49,"line":172},18,[47,174,175],{},"                )\n",[47,177,179],{"class":49,"line":178},19,[47,180,181],{},"                if r.status_code == 429:            # rate limited: back off\n",[47,183,185],{"class":49,"line":184},20,[47,186,187],{},"                    await asyncio.sleep(2 ** attempt * 5)\n",[47,189,191],{"class":49,"line":190},21,[47,192,193],{},"                    continue\n",[47,195,197],{"class":49,"line":196},22,[47,198,199],{},"                if r.status_code == 200:\n",[47,201,203],{"class":49,"line":202},23,[47,204,205],{},"                    return {\"url\": url, \"ok\": True, \"data\": r.json()}\n",[47,207,209],{"class":49,"line":208},24,[47,210,211],{},"                return {\"url\": url, \"ok\": False, \"error\": f\"HTTP {r.status_code}\"}\n",[47,213,215],{"class":49,"line":214},25,[47,216,217],{},"            except httpx.HTTPError as e:\n",[47,219,221],{"class":49,"line":220},26,[47,222,223],{},"                if attempt == RETRIES - 1:\n",[47,225,227],{"class":49,"line":226},27,[47,228,229],{},"                    return {\"url\": url, \"ok\": False, \"error\": str(e)}\n",[47,231,233],{"class":49,"line":232},28,[47,234,235],{},"                await asyncio.sleep(2 ** attempt)\n",[47,237,239],{"class":49,"line":238},29,[47,240,86],{"emptyLinePlaceholder":85},[47,242,244],{"class":49,"line":243},30,[47,245,246],{},"async def main(urls):\n",[47,248,250],{"class":49,"line":249},31,[47,251,252],{},"    sem = asyncio.Semaphore(CONCURRENCY)\n",[47,254,256],{"class":49,"line":255},32,[47,257,258],{},"    async with httpx.AsyncClient() as client:\n",[47,260,262],{"class":49,"line":261},33,[47,263,264],{},"        results = await asyncio.gather(*(extract(client, sem, u) for u in urls))\n",[47,266,268],{"class":49,"line":267},34,[47,269,270],{},"    with open(\"brands.jsonl\", \"w\") as f:\n",[47,272,274],{"class":49,"line":273},35,[47,275,276],{},"        for row in results:\n",[47,278,280],{"class":49,"line":279},36,[47,281,282],{},"            f.write(json.dumps(row) + \"\\n\")\n",[47,284,286],{"class":49,"line":285},37,[47,287,288],{},"    failed = [r for r in results if not r[\"ok\"]]\n",[47,290,292],{"class":49,"line":291},38,[47,293,294],{},"    print(f\"{len(results) - len(failed)} ok, {len(failed)} failed\")\n",[47,296,298],{"class":49,"line":297},39,[47,299,86],{"emptyLinePlaceholder":85},[47,301,303],{"class":49,"line":302},40,[47,304,305],{},"if __name__ == \"__main__\":\n",[47,307,309],{"class":49,"line":308},41,[47,310,311],{},"    urls = [row[0] for row in csv.reader(open(\"domains.csv\"))]\n",[47,313,315],{"class":49,"line":314},42,[47,316,317],{},"    asyncio.run(main(urls))\n",[11,319,320,321,324,325,328],{},"Swap the endpoint for ",[29,322,323],{},"/v1/extract"," if you want full token sets instead of brand data, and adjust ",[29,326,327],{},"fields="," to trim the response to what you store - it does not change the credit cost, but it makes the output files dramatically smaller.",[15,330,332],{"id":331},"the-three-things-that-make-bulk-runs-survive","The three things that make bulk runs survive",[11,334,335,339,340,344,345,348,349,353],{},[336,337,338],"strong",{},"1. The semaphore is the contract."," Your plan has a requests-per-minute cap (the ladder is in the ",[23,341,343],{"href":342},"/api/docs/rate-limits","rate limits docs","). Five concurrent with backoff on 429 stays comfortably inside the entry tiers; raise it as your plan allows. Blasting unbounded ",[29,346,347],{},"gather"," at any API gets you rate-limited into taking ",[350,351,352],"em",{},"longer"," than the polite version.",[11,355,356,359],{},[336,357,358],{},"2. Expect 5-15% failures and capture them."," On a real 1,000-domain list some are dead, parked, or behind bot walls. The script writes per-URL errors instead of dying, so the run always finishes and the failures are a re-runnable list, not a mystery.",[11,361,362,365,366,369],{},[336,363,364],{},"3. Re-runs are cheap because of caching."," Responses are cached for 24 hours and a cache hit costs 0 credits (",[29,367,368],{},"\"cached\": true"," in the usage object). Re-running today's failed batch only pays for URLs that actually re-extract - so retry freely.",[15,371,373],{"id":372},"budgeting-a-1000-url-run","Budgeting a 1,000-URL run",[375,376,377,392],"table",{},[378,379,380],"thead",{},[381,382,383,386,389],"tr",{},[384,385],"th",{},[384,387,388],{},"Brand data (15 cr/URL)",[384,390,391],{},"Design tokens (10 cr/URL)",[393,394,395,407,418],"tbody",{},[381,396,397,401,404],{},[398,399,400],"td",{},"100 URLs",[398,402,403],{},"1,500 credits",[398,405,406],{},"1,000 credits",[381,408,409,412,415],{},[398,410,411],{},"1,000 URLs",[398,413,414],{},"15,000 credits",[398,416,417],{},"10,000 credits",[381,419,420,423,426],{},[398,421,422],{},"Fits in",[398,424,425],{},"Growth (€79/mo, 30k)",[398,427,428],{},"Growth, or 2× Developer months",[11,430,431],{},"The free 100 credits cover a 6-10 URL pilot - do that first, on your ugliest URLs, before committing a plan to the full list. Wall-clock: the rate limiter sets the pace, not the extraction; expect tens of minutes for 1,000 URLs, not hours.",[15,433,435],{"id":434},"summary","Summary",[11,437,438,439,443],{},"Bulk extraction is a queue-discipline problem: bounded concurrency, backoff on 429, per-URL error capture, and a re-run list. The 24-hour cache turns retries from a cost into a free operation. Pilot on 10 URLs with a ",[23,440,442],{"href":441},"/api/dashboard/keys","free key",", then size the plan to the list.",[15,445,447],{"id":446},"frequently-asked-questions","Frequently asked questions",[449,450,452],"h3",{"id":451},"how-do-i-scrape-brand-colors-from-a-list-of-websites-in-python","How do I scrape brand colors from a list of websites in Python?",[11,454,455],{},"For a few sites, Playwright and computed styles. For hundreds or thousands, the async httpx script above against an extraction API.",[449,457,459],{"id":458},"how-long-does-1000-urls-take","How long does 1,000 URLs take?",[11,461,462],{},"Tens of minutes - your requests-per-minute cap is the bottleneck, not the extraction itself.",[449,464,466],{"id":465},"what-failure-rate-should-i-expect","What failure rate should I expect?",[11,468,469],{},"5-15% on real-world lists: dead domains, parked pages, bot walls. Capture and re-run rather than aiming for zero.",[449,471,473],{"id":472},"do-repeated-extractions-cost-credits-again","Do repeated extractions cost credits again?",[11,475,476,477,480],{},"Not within 24 hours - cache hits cost 0 credits and report ",[29,478,479],{},"cached: true",".",[482,483,484],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":43,"searchDepth":76,"depth":76,"links":486},[487,488,489,490,491,492],{"id":17,"depth":76,"text":18},{"id":35,"depth":76,"text":36},{"id":331,"depth":76,"text":332},{"id":372,"depth":76,"text":373},{"id":434,"depth":76,"text":435},{"id":446,"depth":76,"text":447,"children":493},[494,495,496,497],{"id":451,"depth":82,"text":452},{"id":458,"depth":82,"text":459},{"id":465,"depth":82,"text":466},{"id":472,"depth":82,"text":473},"Tutorials","2026-07-18",null,"Bulk-extract brand data and design tokens from thousands of domains in Python - async code with rate limiting and retries that survives a real URL list.","md",[504,506,509,512],{"question":452,"answer":505},"For a handful of sites, a Playwright script reading computed styles works. Past a few hundred URLs, run the extraction through an API with asyncio and httpx: a semaphore to respect rate limits, retries with backoff for the URLs that fail, and checkpointing so a crash does not restart the whole run.",{"question":507,"answer":508},"How long does it take to extract data from 1,000 URLs?","With an API at a typical plan rate limit, roughly 20-60 minutes for 1,000 URLs depending on your requests-per-minute cap - the limiter, not the extraction, sets the pace. A self-hosted browser farm is usually slower per URL once retries and crashes are counted.",{"question":510,"answer":511},"What percentage of URLs fail in a bulk extraction run?","Plan for 5-15% on a real-world list: dead domains, redirects to parked pages, bot walls, timeouts. This is why retries and per-URL error capture matter more than raw speed - a bulk script that stops on the first 404 never finishes.",{"question":513,"answer":514},"Do repeated extractions of the same URL cost credits again?","On MiroMiro, responses are cached for 24 hours and a cache hit costs 0 credits - the usage object reports cached: true. Re-running a failed batch the same day only pays for the URLs that actually re-extract.",{},"/api-blog/how-to-extract-brand-assets-in-bulk",[518,519,520],"best-brand-data-apis-2026","how-to-extract-design-tokens-from-any-website-in-python","add-design-extraction-to-claude-code-mcp",{"title":5,"description":501},"api-blog/how-to-extract-brand-assets-in-bulk",[524,66,525,526,527],"bulk extraction","async","brand data","automation","s_wWV-k0k55QCGaVhQYXcQuGKhALy3iIWTRlMgsjUVM",[530,939,1631],{"id":531,"title":532,"author":6,"body":533,"category":912,"date":499,"dateModified":500,"description":913,"extension":502,"faqs":914,"image":500,"meta":925,"navigation":85,"path":926,"readingTime":107,"relatedSlugs":927,"seo":931,"stem":932,"tags":933,"__hash__":938},"apiBlog/api-blog/best-brand-data-apis-2026.md","Best Brand Data APIs in 2026 (Logos, Colors, Fonts)",{"type":8,"value":534,"toc":898},[535,542,545,549,558,565,587,591,599,616,620,627,644,648,665,705,708,729,733,844,848,860,862,866,869,873,876,880,883,887,895],[11,536,537,538,541],{},"If you need to turn ",[29,539,540],{},"stripe.com"," into a logo, brand colors, and fonts inside your product, four options are worth your time in 2026. The short version: logos alone are a solved, free problem; the money question is how much design depth you need behind the logo.",[11,543,544],{},"The reason this category exists is not design tooling, it is conversion. Brandfetch's customer pages cite Typeform at +5% free-to-paid and Senja at +233% activation after wiring brand personalization into onboarding. \"Enter your URL, watch the product become yours\" is one of the few onboarding tricks with published numbers behind it.",[15,546,548],{"id":547},"_1-brandfetch-the-category-owner","1. Brandfetch - the category owner",[11,550,551,557],{},[23,552,556],{"href":553,"rel":554},"https://brandfetch.com",[555],"nofollow","Brandfetch"," is the default answer, with Meta, Typeform, and Lucid on its logo wall. One call returns logos in several formats, brand colors, fonts, and company info.",[11,559,560,561,564],{},"Their pricing (verified July 2026): free tier of 100 Brand API requests, then the Brand API at ",[336,562,563],{},"$99/mo for 2,500 calls"," with $0.10 overage. The Logo API alone is free up to 500k requests a month, which is quietly the best deal in the category.",[566,567,568,575,581],"ul",{},[569,570,571,574],"li",{},[336,572,573],{},"Good:"," the deepest logo coverage in the market, hand-curated for big brands, reliable.",[569,576,577,580],{},[336,578,579],{},"Bad:"," $99/mo is real money for 2,500 calls, and the design data stops at \"a few colors and font names\". You cannot theme a UI from it - there is no spacing, no type scale, no shadows.",[569,582,583,586],{},[336,584,585],{},"Use it when:"," logo quality and coverage matter more than design depth, and the budget is there.",[15,588,590],{"id":589},"_2-logodev-the-pragmatic-logo-endpoint","2. Logo.dev - the pragmatic logo endpoint",[11,592,593,598],{},[23,594,597],{"href":595,"rel":596},"https://logo.dev",[555],"Logo.dev"," does one thing: domain in, logo image out, with a generous free tier and simple img-tag integration.",[566,600,601,606,611],{},[569,602,603,605],{},[336,604,573],{}," dead simple, fast to integrate, free for most usage.",[569,607,608,610],{},[336,609,579],{}," logos only. No colors, no fonts, no JSON brand object.",[569,612,613,615],{},[336,614,585],{}," you literally just need company logos in a table or CRM.",[15,617,619],{"id":618},"_3-clearbit-logo-api-the-old-free-standby","3. Clearbit Logo API - the old free standby",[11,621,622,623,626],{},"Clearbit's ",[29,624,625],{},"logo.clearbit.com/{domain}"," endpoint has been free and everywhere for a decade. Clearbit is now part of HubSpot, and the endpoint has outlived several announcements about its future.",[566,628,629,634,639],{},[569,630,631,633],{},[336,632,573],{}," free, zero setup, still works.",[569,635,636,638],{},[336,637,579],{}," logos only, no SLA, and it lives at an acquirer's pleasure. I would not build a paid product on it in 2026.",[569,640,641,643],{},[336,642,585],{}," internal tools and prototypes.",[15,645,647],{"id":646},"_4-miromiro-brand-extract-api-identity-plus-the-design-system","4. MiroMiro brand + extract API - identity plus the design system",[11,649,650,651,655,656,659,660,664],{},"Our ",[23,652,654],{"href":653},"/api/docs/brand","/v1/brand"," endpoint returns the logo, palette, and fonts like a brand API. The difference is what sits next to it: ",[23,657,323],{"href":658},"/api/docs/design-tokens"," returns the site's working design system - ranked colors, type scale, spacing, radii, shadows, gradients, motion - and ",[23,661,663],{"href":662},"/api/docs/code","/v1/code"," turns a section into Tailwind/JSX/Vue. Same key, same call shape:",[38,666,668],{"className":40,"code":667,"language":42,"meta":43,"style":43},"curl \"https://miromiro.app/api/v1/brand?url=stripe.com&fields=logo,palette\" \\\n  -H \"Authorization: Bearer $MIROMIRO_API_KEY\"\n",[29,669,670,689],{"__ignoreMap":43},[47,671,672,675,679,682,685],{"class":49,"line":50},[47,673,674],{"class":53},"curl",[47,676,678],{"class":677},"sMK4o"," \"",[47,680,681],{"class":57},"https://miromiro.app/api/v1/brand?url=stripe.com&fields=logo,palette",[47,683,684],{"class":677},"\"",[47,686,688],{"class":687},"sTEyZ"," \\\n",[47,690,691,694,696,699,702],{"class":49,"line":76},[47,692,693],{"class":57},"  -H",[47,695,678],{"class":677},[47,697,698],{"class":57},"Authorization: Bearer ",[47,700,701],{"class":687},"$MIROMIRO_API_KEY",[47,703,704],{"class":677},"\"\n",[11,706,707],{},"Pricing: 100 free credits a month with no card, then from €19/mo for 5,000 credits. A brand call costs 15 credits, a token extraction 10.",[566,709,710,719,724],{},[569,711,712,714,715,718],{},[336,713,573],{}," the only option here that returns enough to actually ",[350,716,717],{},"theme"," a product, not just badge it. Built for the \"enter your URL\" onboarding flow.",[569,720,721,723],{},[336,722,579],{}," our logo curation is not Brandfetch's - we extract from the live site rather than maintain a hand-checked logo library, so obscure-brand edge cases favor them. Static HTML + CSS parsing; no JS execution.",[569,725,726,728],{},[336,727,585],{}," the goal is brand-personalized UI (onboarding, generated apps, email builders), not a logo lookup.",[15,730,732],{"id":731},"side-by-side","Side by side",[375,734,735,760],{},[378,736,737],{},[381,738,739,742,745,748,751,754,757],{},[384,740,741],{},"API",[384,743,744],{},"Logo",[384,746,747],{},"Colors",[384,749,750],{},"Fonts",[384,752,753],{},"Full design system",[384,755,756],{},"Free tier",[384,758,759],{},"Paid entry",[393,761,762,784,803,822],{},[381,763,764,766,769,772,775,778,781],{},[398,765,556],{},[398,767,768],{},"Excellent",[398,770,771],{},"Basic",[398,773,774],{},"Names",[398,776,777],{},"No",[398,779,780],{},"100 req",[398,782,783],{},"$99/mo (2,500 calls)",[381,785,786,788,791,793,795,797,800],{},[398,787,597],{},[398,789,790],{},"Good",[398,792,777],{},[398,794,777],{},[398,796,777],{},[398,798,799],{},"Generous",[398,801,802],{},"Cheap",[381,804,805,808,810,812,814,816,819],{},[398,806,807],{},"Clearbit Logo",[398,809,790],{},[398,811,777],{},[398,813,777],{},[398,815,777],{},[398,817,818],{},"Unlimited-ish",[398,820,821],{},"n/a",[381,823,824,827,829,832,835,838,841],{},[398,825,826],{},"MiroMiro",[398,828,790],{},[398,830,831],{},"Ranked, full palette",[398,833,834],{},"Families + weights + sizes",[398,836,837],{},"Yes (tokens + section code)",[398,839,840],{},"100 credits/mo",[398,842,843],{},"€19/mo (5,000 credits)",[15,845,847],{"id":846},"the-verdict","The verdict",[11,849,850,851,854,855,859],{},"Logos only: Logo.dev, or Clearbit if it is internal. Maximum logo coverage with budget: Brandfetch. Theming a product to the user's brand: that is the job we built for - a logo plus three colors will not carry an onboarding flow that promises \"your brand, applied\". You can ",[23,852,853],{"href":441},"get a free key"," and test your own domain in the ",[23,856,858],{"href":857},"/api/demo","playground"," in under a minute.",[15,861,447],{"id":446},[449,863,865],{"id":864},"what-is-a-brand-data-api","What is a brand data API?",[11,867,868],{},"Domain in, structured brand assets out: logo, colors, fonts. Products use them for onboarding personalization, CRM enrichment, and auto-theming.",[449,870,872],{"id":871},"is-there-a-free-logo-api","Is there a free logo API?",[11,874,875],{},"Yes, two: Brandfetch's Logo API tier (free to 500k req/mo) and Clearbit's endpoint. For logos alone, pay nothing.",[449,877,879],{"id":878},"why-fetch-brand-data-during-onboarding","Why fetch brand data during onboarding?",[11,881,882],{},"Published numbers: +5% free-to-paid at Typeform, +233% activation at Senja (per Brandfetch's customer pages). Users activate faster inside a product that already looks like theirs.",[449,884,886],{"id":885},"brand-api-vs-design-token-api","Brand API vs design token API?",[11,888,889,890,894],{},"Identity vs system. A logo and five hex codes badge a UI; a token set (palette, type scale, spacing, shadows, motion) is what you need to build one. The ",[23,891,893],{"href":892},"/api/blog/design-token-types-reference","design token types reference"," breaks down the difference.",[482,896,897],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":43,"searchDepth":76,"depth":76,"links":899},[900,901,902,903,904,905,906],{"id":547,"depth":76,"text":548},{"id":589,"depth":76,"text":590},{"id":618,"depth":76,"text":619},{"id":646,"depth":76,"text":647},{"id":731,"depth":76,"text":732},{"id":846,"depth":76,"text":847},{"id":446,"depth":76,"text":447,"children":907},[908,909,910,911],{"id":864,"depth":82,"text":865},{"id":871,"depth":82,"text":872},{"id":878,"depth":82,"text":879},{"id":885,"depth":82,"text":886},"Comparisons","Brandfetch, Logo.dev, Clearbit, and MiroMiro compared honestly on depth and pricing - when a logo API is enough and when you need the full design system.",[915,917,919,922],{"question":865,"answer":916},"An API that takes a domain name and returns the company's brand assets - typically the logo, brand colors, and fonts - as structured data. Products use them to personalize onboarding, enrich CRM records, and theme UIs to a customer's brand automatically.",{"question":872,"answer":918},"Yes. Brandfetch's Logo API tier is free up to 500,000 requests a month, and Clearbit's long-standing logo endpoint (now part of HubSpot) has been free for years. For logos alone, you should not be paying.",{"question":920,"answer":921},"Why do products fetch brand data during onboarding?","Because personalization measurably converts. Brandfetch's own customer pages cite Typeform seeing +5% free-to-paid and Senja +233% activation after adding brand personalization to onboarding. A user who sees their own brand in your product is closer to activated.",{"question":923,"answer":924},"What is the difference between a brand API and a design token API?","A brand API returns identity: logo, a handful of colors, maybe fonts. A design token API returns the working design system: the full palette with usage ranking, type scale, spacing, radii, shadows, and motion. If you are theming a UI, identity alone is not enough to build with.",{},"/api-blog/best-brand-data-apis-2026",[928,929,930],"best-design-token-extraction-tools-2026","best-firecrawl-alternatives-2026","how-to-extract-brand-assets-in-bulk",{"title":532,"description":913},"api-blog/best-brand-data-apis-2026",[934,935,936,937],"brand api","brandfetch","logo api","comparison","6oZS0krURBt4VY9_ej0hmKJd6CMEe4ynuxFS-SMRqIw",{"id":940,"title":941,"author":6,"body":942,"category":498,"date":499,"dateModified":500,"description":1608,"extension":502,"faqs":1609,"image":500,"meta":1619,"navigation":85,"path":1620,"readingTime":113,"relatedSlugs":1621,"seo":1624,"stem":1625,"tags":1626,"__hash__":1630},"apiBlog/api-blog/how-to-extract-design-tokens-from-any-website-in-python.md","How to Extract Design Tokens from Any Website in Python",{"type":8,"value":943,"toc":1592},[944,951,954,958,961,985,988,1154,1157,1165,1180,1184,1187,1226,1229,1233,1236,1275,1279,1282,1344,1348,1428,1435,1439,1442,1472,1502,1511,1513,1523,1525,1529,1532,1536,1549,1553,1556,1560,1563,1567,1589],[11,945,946,947,950],{},"The fastest way to extract design tokens from a website in Python is Playwright plus ",[29,948,949],{},"getComputedStyle()",": load the page in a real browser, collect the computed styles of every element, and count what repeats. The counting is the important part. A site's design system is whatever it uses everywhere, not every value that appears once in a forgotten footer.",[11,952,953],{},"Parsing CSS files without a browser is the approach most tutorials suggest, and it is the one that works worst on modern sites. More on that below.",[15,955,957],{"id":956},"extract-colors-with-playwright","Extract colors with Playwright",[11,959,960],{},"First, install Playwright and a browser:",[38,962,964],{"className":40,"code":963,"language":42,"meta":43,"style":43},"pip install playwright\nplaywright install chromium\n",[29,965,966,975],{"__ignoreMap":43},[47,967,968,970,972],{"class":49,"line":50},[47,969,54],{"class":53},[47,971,58],{"class":57},[47,973,974],{"class":57}," playwright\n",[47,976,977,980,982],{"class":49,"line":76},[47,978,979],{"class":53},"playwright",[47,981,58],{"class":57},[47,983,984],{"class":57}," chromium\n",[11,986,987],{},"Then collect styles from every element and count them:",[38,989,991],{"className":64,"code":990,"language":66,"meta":43,"style":43},"from collections import Counter\nfrom playwright.sync_api import sync_playwright\n\nCOLLECT = \"\"\"\n() => {\n  const els = [...document.querySelectorAll('body *')].slice(0, 5000);\n  const out = { colors: [], fonts: [], sizes: [], radii: [], shadows: [] };\n  for (const el of els) {\n    const s = getComputedStyle(el);\n    out.colors.push(s.color, s.backgroundColor, s.borderTopColor);\n    out.fonts.push(s.fontFamily);\n    out.sizes.push(s.fontSize);\n    out.radii.push(s.borderTopLeftRadius);\n    out.shadows.push(s.boxShadow);\n  }\n  return out;\n}\n\"\"\"\n\ndef top(values, n=8, skip=(\"\", \"none\", \"normal\", \"auto\", \"0px\", \"rgba(0, 0, 0, 0)\")):\n    return Counter(v for v in values if v not in skip).most_common(n)\n\nwith sync_playwright() as p:\n    browser = p.chromium.launch()\n    page = browser.new_page(viewport={\"width\": 1440, \"height\": 900})\n    page.goto(\"https://stripe.com\", wait_until=\"networkidle\")\n    raw = page.evaluate(COLLECT)\n    browser.close()\n\nfor name in (\"colors\", \"fonts\", \"sizes\", \"radii\", \"shadows\"):\n    print(f\"\\n== {name} ==\")\n    for value, count in top(raw[name]):\n        print(f\"{count:>5}  {value}\")\n",[29,992,993,998,1003,1007,1012,1017,1022,1027,1032,1037,1042,1047,1052,1057,1062,1067,1072,1077,1082,1086,1091,1096,1100,1105,1110,1115,1120,1125,1130,1134,1139,1144,1149],{"__ignoreMap":43},[47,994,995],{"class":49,"line":50},[47,996,997],{},"from collections import Counter\n",[47,999,1000],{"class":49,"line":76},[47,1001,1002],{},"from playwright.sync_api import sync_playwright\n",[47,1004,1005],{"class":49,"line":82},[47,1006,86],{"emptyLinePlaceholder":85},[47,1008,1009],{"class":49,"line":89},[47,1010,1011],{},"COLLECT = \"\"\"\n",[47,1013,1014],{"class":49,"line":95},[47,1015,1016],{},"() => {\n",[47,1018,1019],{"class":49,"line":101},[47,1020,1021],{},"  const els = [...document.querySelectorAll('body *')].slice(0, 5000);\n",[47,1023,1024],{"class":49,"line":107},[47,1025,1026],{},"  const out = { colors: [], fonts: [], sizes: [], radii: [], shadows: [] };\n",[47,1028,1029],{"class":49,"line":113},[47,1030,1031],{},"  for (const el of els) {\n",[47,1033,1034],{"class":49,"line":118},[47,1035,1036],{},"    const s = getComputedStyle(el);\n",[47,1038,1039],{"class":49,"line":124},[47,1040,1041],{},"    out.colors.push(s.color, s.backgroundColor, s.borderTopColor);\n",[47,1043,1044],{"class":49,"line":130},[47,1045,1046],{},"    out.fonts.push(s.fontFamily);\n",[47,1048,1049],{"class":49,"line":136},[47,1050,1051],{},"    out.sizes.push(s.fontSize);\n",[47,1053,1054],{"class":49,"line":142},[47,1055,1056],{},"    out.radii.push(s.borderTopLeftRadius);\n",[47,1058,1059],{"class":49,"line":148},[47,1060,1061],{},"    out.shadows.push(s.boxShadow);\n",[47,1063,1064],{"class":49,"line":154},[47,1065,1066],{},"  }\n",[47,1068,1069],{"class":49,"line":160},[47,1070,1071],{},"  return out;\n",[47,1073,1074],{"class":49,"line":166},[47,1075,1076],{},"}\n",[47,1078,1079],{"class":49,"line":172},[47,1080,1081],{},"\"\"\"\n",[47,1083,1084],{"class":49,"line":178},[47,1085,86],{"emptyLinePlaceholder":85},[47,1087,1088],{"class":49,"line":184},[47,1089,1090],{},"def top(values, n=8, skip=(\"\", \"none\", \"normal\", \"auto\", \"0px\", \"rgba(0, 0, 0, 0)\")):\n",[47,1092,1093],{"class":49,"line":190},[47,1094,1095],{},"    return Counter(v for v in values if v not in skip).most_common(n)\n",[47,1097,1098],{"class":49,"line":196},[47,1099,86],{"emptyLinePlaceholder":85},[47,1101,1102],{"class":49,"line":202},[47,1103,1104],{},"with sync_playwright() as p:\n",[47,1106,1107],{"class":49,"line":208},[47,1108,1109],{},"    browser = p.chromium.launch()\n",[47,1111,1112],{"class":49,"line":214},[47,1113,1114],{},"    page = browser.new_page(viewport={\"width\": 1440, \"height\": 900})\n",[47,1116,1117],{"class":49,"line":220},[47,1118,1119],{},"    page.goto(\"https://stripe.com\", wait_until=\"networkidle\")\n",[47,1121,1122],{"class":49,"line":226},[47,1123,1124],{},"    raw = page.evaluate(COLLECT)\n",[47,1126,1127],{"class":49,"line":232},[47,1128,1129],{},"    browser.close()\n",[47,1131,1132],{"class":49,"line":238},[47,1133,86],{"emptyLinePlaceholder":85},[47,1135,1136],{"class":49,"line":243},[47,1137,1138],{},"for name in (\"colors\", \"fonts\", \"sizes\", \"radii\", \"shadows\"):\n",[47,1140,1141],{"class":49,"line":249},[47,1142,1143],{},"    print(f\"\\n== {name} ==\")\n",[47,1145,1146],{"class":49,"line":255},[47,1147,1148],{},"    for value, count in top(raw[name]):\n",[47,1150,1151],{"class":49,"line":261},[47,1152,1153],{},"        print(f\"{count:>5}  {value}\")\n",[11,1155,1156],{},"Run it and you get something like:",[38,1158,1163],{"className":1159,"code":1161,"language":1162},[1160],"language-text","== colors ==\n 3021  rgb(255, 255, 255)\n 1204  rgb(26, 27, 37)\n  412  rgb(99, 91, 255)\n  118  rgb(66, 84, 102)\n","text",[29,1164,1161],{"__ignoreMap":43},[11,1166,1167,1168,1171,1172,1175,1176,1179],{},"That third line is the brand color, found by nothing smarter than counting. ",[29,1169,1170],{},"getComputedStyle"," has already done the hard work for you: it resolved every CSS variable chain, every ",[29,1173,1174],{},"var(--a, var(--b))"," fallback, every cascade conflict, and turned Tailwind utility soup into plain ",[29,1177,1178],{},"rgb()"," values.",[15,1181,1183],{"id":1182},"fonts-and-the-type-scale","Fonts and the type scale",[11,1185,1186],{},"The same collection already has them. Font families come back as full stacks, so take the first entry:",[38,1188,1190],{"className":64,"code":1189,"language":66,"meta":43,"style":43},"families = Counter(f.split(\",\")[0].strip('\"\\' ') for f in raw[\"fonts\"])\nprint(families.most_common(5))\n\nsizes = sorted(\n    {float(s.replace(\"px\", \"\")) for s, c in top(raw[\"sizes\"], n=20)},\n)\nprint(sizes)  # e.g. [12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 36.0, 56.0]\n",[29,1191,1192,1197,1202,1206,1211,1216,1221],{"__ignoreMap":43},[47,1193,1194],{"class":49,"line":50},[47,1195,1196],{},"families = Counter(f.split(\",\")[0].strip('\"\\' ') for f in raw[\"fonts\"])\n",[47,1198,1199],{"class":49,"line":76},[47,1200,1201],{},"print(families.most_common(5))\n",[47,1203,1204],{"class":49,"line":82},[47,1205,86],{"emptyLinePlaceholder":85},[47,1207,1208],{"class":49,"line":89},[47,1209,1210],{},"sizes = sorted(\n",[47,1212,1213],{"class":49,"line":95},[47,1214,1215],{},"    {float(s.replace(\"px\", \"\")) for s, c in top(raw[\"sizes\"], n=20)},\n",[47,1217,1218],{"class":49,"line":101},[47,1219,1220],{},")\n",[47,1222,1223],{"class":49,"line":107},[47,1224,1225],{},"print(sizes)  # e.g. [12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 36.0, 56.0]\n",[11,1227,1228],{},"That sorted list is the site's type scale. If it has fifteen entries instead of seven, you are looking at a site without a design system, which is also useful to know.",[15,1230,1232],{"id":1231},"from-raw-values-to-actual-tokens","From raw values to actual tokens",[11,1234,1235],{},"Raw counts are not tokens yet. Three cleanup steps matter:",[1237,1238,1239,1249,1262],"ol",{},[569,1240,1241,1244,1245,1248],{},[336,1242,1243],{},"Drop the defaults."," ",[29,1246,1247],{},"rgb(0, 0, 0)"," near the top does not mean the brand is black. Browser resets and inherited defaults pollute every element, so check whether a value actually appears on visible, styled elements before promoting it.",[569,1250,1251,1244,1254,1257,1258,1261],{},[336,1252,1253],{},"Merge near-duplicates.",[29,1255,1256],{},"rgb(99, 91, 255)"," and ",[29,1259,1260],{},"rgb(99, 92, 255)"," are the same token with a rounding disagreement. Snap channels to a small tolerance before counting, or you will split one token's count across five entries.",[569,1263,1264,1267,1268,1271,1272,1274],{},[336,1265,1266],{},"Name by role, not by value."," The most frequent saturated color on buttons and links is ",[29,1269,1270],{},"primary",". The dominant text color is ",[29,1273,1162],{},". Frequency plus element context gets you there; k-means clustering is overkill for this and I would skip it.",[15,1276,1278],{"id":1277},"common-problems","Common problems",[11,1280,1281],{},"Real gotchas, in the order they will bite you:",[566,1283,1284,1294,1300,1317,1330],{},[569,1285,1286,1289,1290,1293],{},[336,1287,1288],{},"Your viewport is a filter."," Computed styles reflect the current viewport only. A ",[29,1291,1292],{},"@media (max-width: 640px)"," override is invisible at 1440px. Extract at two viewports (mobile and desktop) if you care about responsive tokens.",[569,1295,1296,1299],{},[336,1297,1298],{},"Cookie banners poison the palette."," Consent overlays and chat widgets bring their own colors and fonts, and they are often thousands of DOM nodes. Dismiss them or exclude their containers before collecting.",[569,1301,1302,1244,1305,1308,1309,1312,1313,1316],{},[336,1303,1304],{},"Fonts can lie.",[29,1306,1307],{},"font-family"," lists what CSS asked for, not what loaded. With ",[29,1310,1311],{},"font-display: swap"," the brand font may never arrive in a headless run. Await ",[29,1314,1315],{},"document.fonts.ready"," before collecting if exact loaded fonts matter.",[569,1318,1319,1325,1326,1329],{},[336,1320,1321,1322,480],{},"Gradients are not in ",[29,1323,1324],{},"backgroundColor"," They live in ",[29,1327,1328],{},"backgroundImage",". If a brand is gradient-heavy, collect that property too or you will report their hero as plain white.",[569,1331,1332,1335,1336,1339,1340,1343],{},[336,1333,1334],{},"Shadow DOM and iframes are invisible"," to ",[29,1337,1338],{},"querySelectorAll",". Web-component-heavy sites need you to walk ",[29,1341,1342],{},"shadowRoot","s explicitly.",[15,1345,1347],{"id":1346},"diy-vs-parsing-css-vs-an-api","DIY vs parsing CSS vs an API",[375,1349,1350,1372],{},[378,1351,1352],{},[381,1353,1354,1357,1360,1363,1366,1369],{},[384,1355,1356],{},"Approach",[384,1358,1359],{},"Tailwind / utility CSS",[384,1361,1362],{},"Runtime CSS-in-JS",[384,1364,1365],{},"Resolves var() and cascade",[384,1367,1368],{},"Media queries",[384,1370,1371],{},"Maintenance",[393,1373,1374,1392,1410],{},[381,1375,1376,1379,1382,1384,1386,1389],{},[398,1377,1378],{},"Playwright + computed styles (this guide)",[398,1380,1381],{},"Yes",[398,1383,1381],{},[398,1385,1381],{},[398,1387,1388],{},"Current viewport only",[398,1390,1391],{},"Browser infra is yours to run",[381,1393,1394,1397,1400,1402,1404,1407],{},[398,1395,1396],{},"Parse CSS files yourself (tinycss2, no browser)",[398,1398,1399],{},"Painful",[398,1401,777],{},[398,1403,777],{},[398,1405,1406],{},"Declared, but unused values included",[398,1408,1409],{},"Low, but wrong on modern sites",[381,1411,1412,1415,1417,1420,1422,1425],{},[398,1413,1414],{},"MiroMiro extract API",[398,1416,1381],{},[398,1418,1419],{},"No (static HTML + CSS, no JS execution)",[398,1421,1381],{},[398,1423,1424],{},"Yes, all declared breakpoints",[398,1426,1427],{},"None",[11,1429,1430,1431,1434],{},"Use the Playwright approach if you are extracting from a handful of sites, need runtime-injected styles, and want full control. Skip the hand-rolled CSS-parsing route: resolving the cascade, specificity, and nested ",[29,1432,1433],{},"var()"," fallbacks yourself is a project, not a script.",[15,1436,1438],{"id":1437},"or-use-one-api-call","Or use one API call",[11,1440,1441],{},"The dedupe, the ranking, the var() chains, the resets, and every declared breakpoint - that is what our extract endpoint does for a living:",[38,1443,1445],{"className":40,"code":1444,"language":42,"meta":43,"style":43},"curl \"https://miromiro.app/api/v1/extract?url=stripe.com\" \\\n  -H \"Authorization: Bearer $MIROMIRO_API_KEY\"\n",[29,1446,1447,1460],{"__ignoreMap":43},[47,1448,1449,1451,1453,1456,1458],{"class":49,"line":50},[47,1450,674],{"class":53},[47,1452,678],{"class":677},[47,1454,1455],{"class":57},"https://miromiro.app/api/v1/extract?url=stripe.com",[47,1457,684],{"class":677},[47,1459,688],{"class":687},[47,1461,1462,1464,1466,1468,1470],{"class":49,"line":76},[47,1463,693],{"class":57},[47,1465,678],{"class":677},[47,1467,698],{"class":57},[47,1469,701],{"class":687},[47,1471,704],{"class":677},[11,1473,1474,1475,1478,1479,1478,1482,1478,1485,1478,1488,1478,1491,1478,1494,1497,1498,1501],{},"The JSON response has ",[29,1476,1477],{},"colors",", ",[29,1480,1481],{},"fontFamilies",[29,1483,1484],{},"fontSizes",[29,1486,1487],{},"fontWeights",[29,1489,1490],{},"spacing",[29,1492,1493],{},"borderRadiuses",[29,1495,1496],{},"boxShadows",", and more, already deduped and ranked. Trim it with ",[29,1499,1500],{},"?fields=colors,spacing"," if you only need part of it.",[11,1503,1504,1505,1508,1509,480],{},"If your script from this guide breaks on some site's cookie banner at 2am, that is roughly the moment an API earns its keep. You can ",[23,1506,1507],{"href":441},"get a free API key"," with 100 credits a month, no card, or try it without signing up in the ",[23,1510,858],{"href":857},[15,1512,435],{"id":434},[11,1514,1515,1516,1518,1519,1522],{},"Playwright plus ",[29,1517,1170],{}," plus a ",[29,1520,1521],{},"Counter"," is the honest way to extract design tokens in Python, and the frequency counting matters more than any clever clustering. Parse-the-CSS approaches are not worth your time on modern sites. I hope this saved you a debugging evening.",[15,1524,447],{"id":446},[449,1526,1528],{"id":1527},"can-i-extract-design-tokens-without-a-headless-browser","Can I extract design tokens without a headless browser?",[11,1530,1531],{},"You can parse the site's CSS with a library like tinycss2, but on modern sites this works badly: CSS-in-JS generates styles at runtime, Tailwind buries the palette in utility classes, and you cannot tell which declared values are actually used. Computed styles from a real browser are the ground truth.",[449,1533,1535],{"id":1534},"how-do-i-extract-tokens-from-a-tailwind-site-where-everything-is-utility-classes","How do I extract tokens from a Tailwind site where everything is utility classes?",[11,1537,1538,1539,1541,1542,1335,1545,1548],{},"Ignore the class names entirely and read computed styles. ",[29,1540,1170],{}," resolves ",[29,1543,1544],{},"bg-indigo-600",[29,1546,1547],{},"rgb(79, 70, 229)"," like any other CSS, so nothing in this guide changes.",[449,1550,1552],{"id":1551},"why-do-my-extracted-colors-include-values-the-site-never-visibly-uses","Why do my extracted colors include values the site never visibly uses?",[11,1554,1555],{},"Three usual suspects: browser defaults on elements you collected, cookie banners and chat widgets injecting their own palette, and transparent or inherited values you forgot to filter.",[449,1557,1559],{"id":1558},"how-many-pages-should-i-sample","How many pages should I sample?",[11,1561,1562],{},"One landing page is usually enough for brand color and typography. Add one product or app page for functional colors (success, error, info). More than three pages rarely changes the top tokens.",[15,1564,1566],{"id":1565},"related-reading","Related reading",[566,1568,1569,1576,1582],{},[569,1570,1571,1575],{},[23,1572,1574],{"href":1573},"/api/design-tokens-api","Design tokens API"," - the endpoint this post's shortcut uses",[569,1577,1578,1581],{},[23,1579,1580],{"href":658},"Design tokens docs"," - full response shape and field list",[569,1583,1584,1588],{},[23,1585,1587],{"href":1586},"/api/use-cases/ai-agent-ground-truth","Ground truth for AI agents"," - why agents need real tokens instead of guessing from screenshots",[482,1590,1591],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}",{"title":43,"searchDepth":76,"depth":76,"links":1593},[1594,1595,1596,1597,1598,1599,1600,1601,1607],{"id":956,"depth":76,"text":957},{"id":1182,"depth":76,"text":1183},{"id":1231,"depth":76,"text":1232},{"id":1277,"depth":76,"text":1278},{"id":1346,"depth":76,"text":1347},{"id":1437,"depth":76,"text":1438},{"id":434,"depth":76,"text":435},{"id":446,"depth":76,"text":447,"children":1602},[1603,1604,1605,1606],{"id":1527,"depth":82,"text":1528},{"id":1534,"depth":82,"text":1535},{"id":1551,"depth":82,"text":1552},{"id":1558,"depth":82,"text":1559},{"id":1565,"depth":76,"text":1566},"Extract colors, fonts, spacing, and shadows from any website with Playwright in Python - working code, the gotchas that bite, and the one-API-call shortcut.",[1610,1612,1614,1616],{"question":1528,"answer":1611},"You can parse the site's CSS files with a library like tinycss2, but on modern sites this works badly. CSS-in-JS generates styles at runtime, utility-class sites like Tailwind bury the palette in thousands of class rules, and you cannot tell which declared values are actually used. Computed styles from a real browser are the ground truth.",{"question":1535,"answer":1613},"Ignore the class names entirely and read computed styles. getComputedStyle resolves bg-indigo-600 to rgb(79, 70, 229) the same way it resolves any other CSS, so the frequency-counting approach in this guide works unchanged.",{"question":1552,"answer":1615},"Three usual suspects: browser default styles on elements you collected, cookie banners and chat widgets injecting their own palette, and transparent or inherited values you forgot to filter. Count frequency, filter defaults, and dismiss overlays before collecting.",{"question":1617,"answer":1618},"How many pages should I sample to get a reliable palette?","One page is usually enough for brand color and typography if it is the homepage or a landing page. Add one product or app page for the functional palette (success, error, info states). More than three pages rarely changes the top tokens.",{},"/api-blog/how-to-extract-design-tokens-from-any-website-in-python",[928,1622,1623],"design-token-types-reference","how-to-extract-website-color-palette-programmatically",{"title":941,"description":1608},"api-blog/how-to-extract-design-tokens-from-any-website-in-python",[1627,66,979,1628,1629],"design tokens","css","web extraction","L5qnHk0I6Xw2LZ4KpLx52C6CU18QlatcV45aNNbr8F0",{"id":1632,"title":1633,"author":6,"body":1634,"category":2160,"date":499,"dateModified":500,"description":2161,"extension":502,"faqs":2162,"image":500,"meta":2174,"navigation":85,"path":2175,"readingTime":95,"relatedSlugs":2176,"seo":2178,"stem":2179,"tags":2180,"__hash__":2185},"apiBlog/api-blog/add-design-extraction-to-claude-code-mcp.md","Add Design Extraction to Claude Code with MCP",{"type":8,"value":1635,"toc":2145},[1636,1639,1707,1710,1721,1725,1728,1734,1738,1748,1752,1763,1802,1812,1816,1852,1855,1866,1870,1877,2021,2028,2032,2062,2066,2069,2097,2103,2105,2109,2118,2122,2125,2129,2132,2136,2142],[11,1637,1638],{},"Two commands and Claude Code can read any website's design system instead of guessing it from a screenshot. Here is the whole setup:",[38,1640,1642],{"className":40,"code":1641,"language":42,"meta":43,"style":43},"claude mcp add miromiro -s user \\\n  --env MIROMIRO_API_KEY=mm_live_your_key \\\n  -- npx -y miromiro-mcp\n\nclaude mcp list   # look for \"✓ Connected\"\n",[29,1643,1644,1666,1676,1690,1694],{"__ignoreMap":43},[47,1645,1646,1649,1652,1655,1658,1661,1664],{"class":49,"line":50},[47,1647,1648],{"class":53},"claude",[47,1650,1651],{"class":57}," mcp",[47,1653,1654],{"class":57}," add",[47,1656,1657],{"class":57}," miromiro",[47,1659,1660],{"class":57}," -s",[47,1662,1663],{"class":57}," user",[47,1665,688],{"class":687},[47,1667,1668,1671,1674],{"class":49,"line":76},[47,1669,1670],{"class":57},"  --env",[47,1672,1673],{"class":57}," MIROMIRO_API_KEY=mm_live_your_key",[47,1675,688],{"class":687},[47,1677,1678,1681,1684,1687],{"class":49,"line":82},[47,1679,1680],{"class":57},"  --",[47,1682,1683],{"class":57}," npx",[47,1685,1686],{"class":57}," -y",[47,1688,1689],{"class":57}," miromiro-mcp\n",[47,1691,1692],{"class":49,"line":89},[47,1693,86],{"emptyLinePlaceholder":85},[47,1695,1696,1698,1700,1703],{"class":49,"line":95},[47,1697,1648],{"class":53},[47,1699,1651],{"class":57},[47,1701,1702],{"class":57}," list",[47,1704,1706],{"class":1705},"sHwdD","   # look for \"✓ Connected\"\n",[11,1708,1709],{},"That is genuinely it. The rest of this post is what those commands do, the Cursor/Windsurf versions, and how to actually use it in a session.",[11,1711,1712,1713,1716,1717,480],{},"No terminal, or you use Claude in the browser? There is also a hosted server - paste ",[29,1714,1715],{},"https://miromiro.app/mcp"," into any client that takes a URL. Full walkthrough for Claude web, Cursor, ChatGPT and Codex in ",[23,1718,1720],{"href":1719},"/api/blog/connect-miromiro-mcp-claude-cursor-chatgpt","how to connect the MiroMiro MCP",[15,1722,1724],{"id":1723},"why-bother","Why bother",[11,1726,1727],{},"Ask a coding agent to \"make it look like stripe.com\" and it works from a screenshot at best, memory at worst. Either way it guesses: colors eyeballed from pixels, spacing approximated, fonts invented. The output is a plausible imitation with none of the real values.",[11,1729,1730,1731,480],{},"With the MCP server connected, the agent stops guessing. It calls the extraction tools itself, gets the site's actual ranked palette, type scale, spacing, and shadows as data, and builds with those. Same prompt, real values. We wrote more about this gap in ",[23,1732,1733],{"href":1586},"ground truth for AI agents",[15,1735,1737],{"id":1736},"step-1-get-a-key","Step 1: get a key",[11,1739,1740,1741,1744,1745,480],{},"Sign in and mint one at ",[23,1742,1743],{"href":441},"miromiro.app/api/dashboard/keys",". 100 free credits a month, no card. Keys look like ",[29,1746,1747],{},"mm_live_...",[15,1749,1751],{"id":1750},"step-2-add-the-server","Step 2: add the server",[11,1753,1754,1755,1758,1759,1762],{},"The server is a local stdio process (",[29,1756,1757],{},"npx miromiro-mcp",") that reads your key from ",[29,1760,1761],{},"MIROMIRO_API_KEY",". Run this in your terminal - not inside a Claude session; you are configuring the server before the conversation starts:",[38,1764,1766],{"className":40,"code":1765,"language":42,"meta":43,"style":43},"claude mcp add miromiro -s user \\\n  --env MIROMIRO_API_KEY=mm_live_your_key \\\n  -- npx -y miromiro-mcp\n",[29,1767,1768,1784,1792],{"__ignoreMap":43},[47,1769,1770,1772,1774,1776,1778,1780,1782],{"class":49,"line":50},[47,1771,1648],{"class":53},[47,1773,1651],{"class":57},[47,1775,1654],{"class":57},[47,1777,1657],{"class":57},[47,1779,1660],{"class":57},[47,1781,1663],{"class":57},[47,1783,688],{"class":687},[47,1785,1786,1788,1790],{"class":49,"line":76},[47,1787,1670],{"class":57},[47,1789,1673],{"class":57},[47,1791,688],{"class":687},[47,1793,1794,1796,1798,1800],{"class":49,"line":82},[47,1795,1680],{"class":57},[47,1797,1683],{"class":57},[47,1799,1686],{"class":57},[47,1801,1689],{"class":57},[11,1803,1804,1807,1808,1811],{},[29,1805,1806],{},"-s user"," registers it for every project; drop it to scope to the current one. The ",[29,1809,1810],{},"-y"," matters: it lets npx start the package unattended, so the editor can spawn the server without an install prompt.",[15,1813,1815],{"id":1814},"step-3-verify-and-use-it","Step 3: verify and use it",[38,1817,1819],{"className":40,"code":1818,"language":42,"meta":43,"style":43},"claude mcp list\n# miromiro ... ✓ Connected\n\nclaude\n> Use the miromiro server to get the brand colors and fonts of stripe.com\n",[29,1820,1821,1830,1835,1839,1844],{"__ignoreMap":43},[47,1822,1823,1825,1827],{"class":49,"line":50},[47,1824,1648],{"class":53},[47,1826,1651],{"class":57},[47,1828,1829],{"class":57}," list\n",[47,1831,1832],{"class":49,"line":76},[47,1833,1834],{"class":1705},"# miromiro ... ✓ Connected\n",[47,1836,1837],{"class":49,"line":82},[47,1838,86],{"emptyLinePlaceholder":85},[47,1840,1841],{"class":49,"line":89},[47,1842,1843],{"class":53},"claude\n",[47,1845,1846,1849],{"class":49,"line":95},[47,1847,1848],{"class":677},">",[47,1850,1851],{"class":687}," Use the miromiro server to get the brand colors and fonts of stripe.com\n",[11,1853,1854],{},"All seven extraction tools appear in Claude's toolbox automatically: design tokens, brand, images, SVGs, fonts, Lottie, and section-to-code. You do not name tools in prompts - ask design-shaped questions and the agent picks the right one. Prompts that work well:",[566,1856,1857,1860,1863],{},[569,1858,1859],{},"\"Extract linear.app's design tokens and restyle our Button component to match\"",[569,1861,1862],{},"\"Get vercel.com's hero section as Tailwind and adapt it for our landing page\"",[569,1864,1865],{},"\"Pull the brand palette from ourcustomer.com and generate a theme file\"",[15,1867,1869],{"id":1868},"cursor-and-windsurf","Cursor and Windsurf",[11,1871,1872,1873,1876],{},"Both take the standard ",[29,1874,1875],{},"mcpServers"," JSON block:",[38,1878,1882],{"className":1879,"code":1880,"language":1881,"meta":43,"style":43},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"mcpServers\": {\n    \"miromiro\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"miromiro-mcp\"],\n      \"env\": { \"MIROMIRO_API_KEY\": \"mm_live_your_key\" }\n    }\n  }\n}\n","json",[29,1883,1884,1889,1905,1919,1942,1975,2008,2013,2017],{"__ignoreMap":43},[47,1885,1886],{"class":49,"line":50},[47,1887,1888],{"class":677},"{\n",[47,1890,1891,1894,1897,1899,1902],{"class":49,"line":76},[47,1892,1893],{"class":677},"  \"",[47,1895,1875],{"class":1896},"spNyl",[47,1898,684],{"class":677},[47,1900,1901],{"class":677},":",[47,1903,1904],{"class":677}," {\n",[47,1906,1907,1910,1913,1915,1917],{"class":49,"line":82},[47,1908,1909],{"class":677},"    \"",[47,1911,1912],{"class":53},"miromiro",[47,1914,684],{"class":677},[47,1916,1901],{"class":677},[47,1918,1904],{"class":677},[47,1920,1921,1924,1928,1930,1932,1934,1937,1939],{"class":49,"line":89},[47,1922,1923],{"class":677},"      \"",[47,1925,1927],{"class":1926},"sbssI","command",[47,1929,684],{"class":677},[47,1931,1901],{"class":677},[47,1933,678],{"class":677},[47,1935,1936],{"class":57},"npx",[47,1938,684],{"class":677},[47,1940,1941],{"class":677},",\n",[47,1943,1944,1946,1949,1951,1953,1956,1958,1960,1962,1965,1967,1970,1972],{"class":49,"line":95},[47,1945,1923],{"class":677},[47,1947,1948],{"class":1926},"args",[47,1950,684],{"class":677},[47,1952,1901],{"class":677},[47,1954,1955],{"class":677}," [",[47,1957,684],{"class":677},[47,1959,1810],{"class":57},[47,1961,684],{"class":677},[47,1963,1964],{"class":677},",",[47,1966,678],{"class":677},[47,1968,1969],{"class":57},"miromiro-mcp",[47,1971,684],{"class":677},[47,1973,1974],{"class":677},"],\n",[47,1976,1977,1979,1982,1984,1986,1989,1991,1994,1996,1998,2000,2003,2005],{"class":49,"line":101},[47,1978,1923],{"class":677},[47,1980,1981],{"class":1926},"env",[47,1983,684],{"class":677},[47,1985,1901],{"class":677},[47,1987,1988],{"class":677}," {",[47,1990,678],{"class":677},[47,1992,1761],{"class":1993},"swJcz",[47,1995,684],{"class":677},[47,1997,1901],{"class":677},[47,1999,678],{"class":677},[47,2001,2002],{"class":57},"mm_live_your_key",[47,2004,684],{"class":677},[47,2006,2007],{"class":677}," }\n",[47,2009,2010],{"class":49,"line":107},[47,2011,2012],{"class":677},"    }\n",[47,2014,2015],{"class":49,"line":113},[47,2016,1066],{"class":677},[47,2018,2019],{"class":49,"line":118},[47,2020,1076],{"class":677},[11,2022,2023,2024,2027],{},"Cursor: ",[29,2025,2026],{},"~/.cursor/mcp.json",". Windsurf: Settings → MCP. Restart the editor after saving; both show the server's tools in their MCP panel when connected.",[15,2029,2031],{"id":2030},"things-worth-knowing","Things worth knowing",[566,2033,2034,2040,2046],{},[569,2035,2036,2039],{},[336,2037,2038],{},"It reads static HTML + CSS."," No JavaScript execution, so a fully client-rendered app can return little. The tools say so rather than inventing a result, which is the behavior you want in an agent pipeline.",[569,2041,2042,2045],{},[336,2043,2044],{},"Credits:"," a token extraction is 10 credits, brand 15, section-to-code 25. The free 100/month covers real experimentation; the agent sees remaining quota in each response's usage object.",[569,2047,2048,2051,2052,2055,2056,2059,2060,480],{},[336,2049,2050],{},"Server not connecting?"," It is almost always the key: check ",[29,2053,2054],{},"claude mcp list",", and confirm the env var made it in (",[29,2057,2058],{},"claude mcp get miromiro","). Node 18+ is required for ",[29,2061,1936],{},[15,2063,2065],{"id":2064},"or-skip-mcp-entirely","Or skip MCP entirely",[11,2067,2068],{},"The same engine is a plain REST API if your agent framework speaks HTTP instead:",[38,2070,2071],{"className":40,"code":1444,"language":42,"meta":43,"style":43},[29,2072,2073,2085],{"__ignoreMap":43},[47,2074,2075,2077,2079,2081,2083],{"class":49,"line":50},[47,2076,674],{"class":53},[47,2078,678],{"class":677},[47,2080,1455],{"class":57},[47,2082,684],{"class":677},[47,2084,688],{"class":687},[47,2086,2087,2089,2091,2093,2095],{"class":49,"line":76},[47,2088,693],{"class":57},[47,2090,678],{"class":677},[47,2092,698],{"class":57},[47,2094,701],{"class":687},[47,2096,704],{"class":677},[11,2098,2099,2100,480],{},"Full setup docs, including the agent quickstart, live at ",[23,2101,2102],{"href":2102},"/api/docs/install-mcp",[15,2104,447],{"id":446},[449,2106,2108],{"id":2107},"how-do-i-add-an-mcp-server-to-claude-code","How do I add an MCP server to Claude Code?",[11,2110,2111,2114,2115,2117],{},[29,2112,2113],{},"claude mcp add \u003Cname> ... -- \u003Ccommand>"," in your terminal, then ",[29,2116,2054],{}," to confirm it connected. Exact MiroMiro command at the top of this post.",[449,2119,2121],{"id":2120},"what-tools-does-the-server-expose","What tools does the server expose?",[11,2123,2124],{},"Seven: design tokens, brand, images, SVGs, fonts, Lottie, and section-to-code. They register automatically; the agent picks per task.",[449,2126,2128],{"id":2127},"why-tokens-instead-of-a-screenshot","Why tokens instead of a screenshot?",[11,2130,2131],{},"Screenshots make the model guess every value. Tokens are the site's real CSS - the difference between imitation and ground truth.",[449,2133,2135],{"id":2134},"does-it-work-with-cursor-and-windsurf","Does it work with Cursor and Windsurf?",[11,2137,2138,2139,2141],{},"Yes - standard ",[29,2140,1875],{}," JSON (above), restart, done.",[482,2143,2144],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":43,"searchDepth":76,"depth":76,"links":2146},[2147,2148,2149,2150,2151,2152,2153,2154],{"id":1723,"depth":76,"text":1724},{"id":1736,"depth":76,"text":1737},{"id":1750,"depth":76,"text":1751},{"id":1814,"depth":76,"text":1815},{"id":1868,"depth":76,"text":1869},{"id":2030,"depth":76,"text":2031},{"id":2064,"depth":76,"text":2065},{"id":446,"depth":76,"text":447,"children":2155},[2156,2157,2158,2159],{"id":2107,"depth":82,"text":2108},{"id":2120,"depth":82,"text":2121},{"id":2127,"depth":82,"text":2128},{"id":2134,"depth":82,"text":2135},"AI Engineering","Install the MiroMiro MCP server in two commands and Claude Code can pull any website's tokens, brand, fonts, and section code instead of guessing.",[2163,2165,2168,2171],{"question":2108,"answer":2164},"Run claude mcp add \u003Cname> with the command that starts the server, in your terminal before starting a session. For MiroMiro: claude mcp add miromiro -s user --env MIROMIRO_API_KEY=your_key -- npx -y miromiro-mcp. Verify with claude mcp list - you want to see Connected.",{"question":2166,"answer":2167},"What does the MiroMiro MCP server let Claude do?","Seven extraction tools appear in Claude's toolbox automatically: design tokens, brand data (logo, palette, fonts), images, SVGs, fonts, Lottie animations, and section-to-code. Claude calls them by itself when you ask design questions about a URL - no manual API calls in your prompts.",{"question":2169,"answer":2170},"Why give a coding agent design tokens instead of a screenshot?","Screenshots make the model guess values - it eyeballs colors, approximates spacing, and invents font names. Tokens are the site's real CSS values, so 'build it like stripe.com' produces the actual palette and type scale instead of a plausible imitation.",{"question":2172,"answer":2173},"Does the MiroMiro MCP server work with Cursor and Windsurf?","Yes. Both take the standard mcpServers JSON block: command npx, args ['-y', 'miromiro-mcp'], and your key in the env. In Cursor it goes in ~/.cursor/mcp.json; in Windsurf under Settings, MCP.",{},"/api-blog/add-design-extraction-to-claude-code-mcp",[2177,519,1622],"connect-miromiro-mcp-claude-cursor-chatgpt",{"title":1633,"description":2161},"api-blog/add-design-extraction-to-claude-code-mcp",[2181,2182,2183,1627,2184],"mcp","claude code","ai agents","cursor","5ZSlTLfRpZyWbN6QBl48G2MrATv3P0HPlFNY504YFvY",1784751617509]