All writing

Strivelabs engineering · Design systems

Read once, used everywhere: a brand kit for every Strive customer

How a client’s nudge turned into a brand kit for the whole platform. Reading a brand off a website, the checks that keep a guessed colour out of a report, why the kit is a file and not a prompt, and the page where customers make it their own.

Published
Stack
React 19 · TypeScript · scrape.do · Firecrawl · one LLM call
Read
~10 minutes

The brand kit extractor is how Strive learns what a customer’s brand looks like. During onboarding we read their website, and keep its colours, type, corners and the rules for using them as one file in our knowledge base. Any agent can read it. Reports were the first to.

Extract once, read anywhere.

A little bit of backstory

Every report Strive Studio wrote came out in our own brand: orange #F97316, slate surfaces and Inter. Not by choice. The report skill’s 411-line prompt had them hardcoded.

Then one of our clients told us, a little vaguely, that the artifacts they’d made with Claude looked better than our reports. On checking, we felt that applying their brand context would close most of that gap.

So I benchmarked how a similar product builds its brand kits, read through the open-source tools, and designed the experience end to end, from onboarding to the page where a customer edits their kit. I got to build all of it too: the client, the server, the design and the prompt engineering in the skill. Our CTO reviewed the code and helped sharpen the backend along the way.

What we were after. Drag to compare: the same report in our orange, then in a customer’s own brand kit.

The one-liner

Read the customer’s website once, at onboarding. Turn it into a small design spec with one LLM call, check every value on the server, and save it as a knowledge-base file that any agent can read.

Two phases meet at one file. Something writes it; anything can read it.

It’s a plain server pipeline rather than one more agent task in onboarding. Nothing unpredictable sits between the scrape and the write, and the same code serves onboarding and the refresh button.

A platform module, not a reports feature

It would have been easy to build this as a report setting. But nothing about a brand is specific to reports. A chat answer, an artifact or an email should look like the customer just as much.

So the kit is a standard knowledge-base file, brand/design.md, and the file is the entire interface.

Read the brand once. Every surface after that reads the same file.

Reading a brand off a website

This was the hardest part to get right, and where I learned the most.

Let the model read, not a parser

The obvious first idea is to parse the CSS. It finds hundreds of hex codes, and it can’t tell a button fill from a border. A model can, if you tell it where to look.

So one LLM call reads the raw page and its stylesheets, with a confidence rule borrowed from dembrandt: colours on the logo or a call to action count most, the nav counts a little, and browser chrome doesn’t count at all.

A parser counts colours. A model can tell you which one is the brand.

Count every colour on the page

My first pipeline sliced the page before the model saw it. That held until one of our clients’ sites, built on Framer, shipped a 616 KB <head> inside a 2.9 MB document. The slice sent the model zero colours, and it confidently invented a jade green for a blue brand.

So the server now counts every literal colour (#hex, rgb() and hsl()) across the whole document and its stylesheets, and hands the model the ranked list. The head and body are found in the full page and trimmed afterwards, never sliced first. The same count later became the evidence every returned colour is checked against.

Hand the model the evidence, not a prefix.

Logos hide in odd places

Sites that draw their mark as an inline SVG have no logo-like <img> at all. So logo candidates are ranked: the site’s own JSON-LD Organization.logo first, then logo-like images, then og:image last, with a warning in the prompt that share images are usually banners.

Fetching the page as it really is

Our existing scrape action cleaned the HTML, which stripped exactly the <head>, styles and stylesheet links this needed. So I added a second action, scrape_raw_website, to the same app, reached through the app manager like every other app action. Importing the scraper straight into the server would have been quicker, and would have broken the boundary the apps exist for.

Under it, scrape.do with rendering goes first: it runs the page’s JavaScript, only bills for successful calls, and worked out about four to five times cheaper. Firecrawl is the fallback.

What we passed on

A few ready-made options came close. None of them was the whole answer.

Ready-made options we weighed
OptionWhat it doesWhy we passed
dembrandtAn open-source CLI that extracts a site’s tokens and can write a DESIGN.mdNeeds a Playwright browser our server doesn’t have, and still needs an LLM pass after. Its confidence tiers and DESIGN.md sections inspired our prompt
Context.devA brand API: logo, colours and fonts from a domainPricing was fine. Coverage of long-tail small-business domains was the worry, and it didn’t beat a live scrape on reliability. A trial is parked for later
BrandfetchLogo and colour lookup by domainNo usage rules, and one more vendor for a subset of what one LLM call already returns

A guessed colour never ships

A wrong brand colour is worse than no brand colour. Our orange at least looks intentional. So between the model and the file sit a few checks, and any of them can say no.

Better our orange than a wrong brand.

Degrade, don’t reject

The same rules run again on every write to the file, whether it comes from the seeder, the editor or the API. That way a report can trust the kit at rest, instead of every reader re-checking it.

Degrade, don’t reject. Only the primary can fail a write.

The stylesheet fetch

One miss worth owning. My first version had our server fetch the stylesheet links it found in the scraped page. Those URLs come from a page the customer controls, and the bytes flowed into the prompt and could come back out through the saved usage rules.

Now both fetches happen inside the scrape action, so the server never fetches a page-supplied URL itself. Every stylesheet link also passes the same URL safety check our blog crawler already used, which blocks private ranges and cloud metadata endpoints.

Customised kits are left alone

A kit marked custom is never overwritten by an automated run. It’s checked before the scrape, so a customer who has made it theirs costs us nothing, and checked again inside the writer. Every rejection path is tested, along with the status it records.

A file, not a prompt

My first version composed the report skill per customer when it loaded, baking their tokens into the prompt. It worked, until I remembered skills are also mirrored into each sandbox as SKILL.md. The copy the agent could see showed our orange, while the one we served showed the customer’s brand.

So the skill went back to being one static document, with one new section: read knowledge_hub/brand/design.md before writing the report, and here’s how to map what you find.

Deliver context the way you deliver knowledge: as a file the agent reads.

The section is explicit about the three cases, because a vague instruction here means a report that stalls or asks:

Tokens drive the report’s CSS. The prose under them, in ## Colors, ## Typography and ## Logo usage sections loosely following the DESIGN.md convention, tells the agent how the brand actually uses them. Here’s the shape of a kit:

brand/design.md, roughly
---display_name: Design Pagesource: extracted                       # default · extracted · customextracted_from: yourbrand.comtheme: lightbrand_primary_hex: "#4F46E5"brand_primary_rgb: "79, 70, 229"        # derived on the serverbrand_primary_strong_hex: "#3932A5"     # derived, 28% darkerbrand_secondary_hex: "#0EA5E9"brand_accent_hex: null                  # dropped: too close to primarybackground_hex: "#FAFAFF"text_hex: "#1E1B4B"font_primary: Manropefont_heading: null                      # empty → Interborder_radius: lglogo_url: https://yourbrand.com/logo.svg---## ColorsIndigo leads every call to action, and nothing else.Sky blue supports charts, never buttons.

The usage rules apply inside the skill’s own visual contract, which wins on a conflict. Brand colour still goes in four places and no more, and no customer rule can widen that.

Make it the platform’s, not the feature’s

The backend went through the same lesson. My first cut gave the feature its own router, its own status endpoint and an environment flag. Each was reasonable on its own. Together they made a brand kit a special case.

Onboarding runs it as a background job, which retries only infrastructure failures, never a rejection, since that would repeat the same answer at full cost. A refresh from the page runs it on demand, in about 15 to 40 seconds.

The page where a brand becomes yours

This was my favourite part to design. A kit read off a website is a good first guess. The page is where a customer makes it right.

I designed the page the way I’d whiteboard today: in code, in Claude Design, with our design system loaded and a live report beside every tweak.

Modern whiteboarding: exploring the kit editor in Claude Design, with report, email and kit previews a step ahead of what shipped.
Edit a token, and the report beside it repaints. What you see is what a report gets.

Adding a second standardised page also forced a small client refactor: six places hardcoded the brand-details page, and now one registry does. It fixed a latent bug on the way, where the new page’s placeholder row would have opened brand details instead.

Existing customers weren’t backfilled. They start on the default kit, and can fill theirs in from this page, or refresh it from their website, whenever they like.

What I’d keep

  1. Build it for the platform, ship it for one reader. Reports read the kit first; anything else can read the same file tomorrow.
  2. Measure the whole page before you trim it. A prefix once handed the model zero colours, and it filled the gap with a guess.
  3. Let the model judge, never let it count. It picks the brand colour. The server derives the rest and checks it.
  4. Degrade, don’t reject. One bad token drops to empty, and the rest of the brand still lands.
  5. Context belongs in a file, not a prompt. The skill stays one reviewable document, and the agent reads the kit like any other knowledge.
  6. Make the preview tell the truth. Run the server’s checks in the editor, so what you see is what the report gets.

What’s next

Reports just happened to ask first. Every surface that comes after gets the brand for free.

Thanks

Thanks to the client whose nudge started this, and to our CTO for the careful reviews that made the backend sharper than I’d have managed alone. And to the whole team at Strivelabs, for building the platform this plugs into.

Back to the shelf