Appearance
Brand Style Guide Generator Cookbook
Updated Feb 2026Build a Node.js application that automatically extracts brand identity from any website and generates a professional PDF style guide. Uses Firecrawl's branding format to pull colors, typography, spacing, logos, and theme data.
What Gets Extracted
Firecrawl's branding format returns a comprehensive design system profile:
| Category | Fields |
|---|---|
| Colors | Primary, secondary, accent, background, and text colors (hex values) |
| Typography | Font families (primary, heading, code), size scale (h1-h6, body), font weights |
| Spacing | Base unit measurements, border radius specifications |
| Images | Logo URL, favicon URL, OG image URL |
| Theme | Color scheme designation (light or dark mode) |
Setup
Initialize the Project
bash
mkdir brand-style-generator
cd brand-style-generator
npm init -y
npm i @mendable/firecrawl-js pdfkit
npm i -D typescript tsx @types/node @types/pdfkitEnvironment Variable
env
FIRECRAWL_API_KEY=fc-your-api-keyTypeScript Config
json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}Step 1: Extract Brand Data
The core extraction uses Firecrawl's branding format -- a single API call returns the full brand profile.
typescript
// src/extract.ts
import FirecrawlApp from "@mendable/firecrawl-js";
const firecrawl = new FirecrawlApp({
apiKey: process.env.FIRECRAWL_API_KEY!,
});
export interface BrandProfile {
colors: {
primary: string;
secondary: string;
accent: string;
background: string;
text: string;
};
typography: {
primaryFont: string;
headingFont: string;
codeFont: string;
sizes: {
h1: string;
h2: string;
h3: string;
h4: string;
h5: string;
h6: string;
body: string;
};
weights: Record<string, string>;
};
spacing: {
baseUnit: string;
borderRadius: string;
};
images: {
logo: string | null;
favicon: string | null;
ogImage: string | null;
};
theme: {
colorScheme: "light" | "dark";
};
}
export async function extractBrand(url: string): Promise<BrandProfile> {
const result = await firecrawl.scrapeUrl(url, {
formats: ["branding"],
});
return result.branding as BrandProfile;
}Usage:
typescript
const brand = await extractBrand("https://stripe.com");
console.log(brand.colors.primary); // "#635BFF"
console.log(brand.typography.primaryFont); // "Inter"
console.log(brand.theme.colorScheme); // "light"Step 2: Generate the PDF
Use PDFKit to create a styled A4 document with visual swatches and typography samples.
typescript
// src/generate-pdf.ts
import PDFDocument from "pdfkit";
import fs from "fs";
import { BrandProfile } from "./extract";
export function generateStyleGuide(
brand: BrandProfile,
outputPath: string,
websiteUrl: string
) {
const doc = new PDFDocument({ size: "A4", margin: 50 });
const stream = fs.createWriteStream(outputPath);
doc.pipe(stream);
// --- Header ---
doc
.rect(0, 0, doc.page.width, 100)
.fill(brand.colors.primary);
doc
.fontSize(28)
.fillColor("#FFFFFF")
.text("Brand Style Guide", 50, 35);
doc
.fontSize(12)
.fillColor("#FFFFFF")
.text(websiteUrl, 50, 70);
doc.moveDown(4);
// --- Colors Section ---
doc
.fontSize(20)
.fillColor(brand.colors.text)
.text("Colors", 50);
doc.moveDown(0.5);
const colorEntries = [
{ name: "Primary", hex: brand.colors.primary },
{ name: "Secondary", hex: brand.colors.secondary },
{ name: "Accent", hex: brand.colors.accent },
{ name: "Background", hex: brand.colors.background },
{ name: "Text", hex: brand.colors.text },
];
let xPos = 50;
for (const color of colorEntries) {
// Color swatch
doc
.rect(xPos, doc.y, 80, 50)
.fill(color.hex);
doc
.fontSize(10)
.fillColor(brand.colors.text)
.text(color.name, xPos, doc.y + 55, { width: 80, align: "center" })
.text(color.hex, xPos, doc.y + 5, { width: 80, align: "center" });
xPos += 100;
}
doc.moveDown(6);
// --- Typography Section ---
doc
.fontSize(20)
.fillColor(brand.colors.text)
.text("Typography", 50);
doc.moveDown(0.5);
doc
.fontSize(12)
.text(`Primary Font: ${brand.typography.primaryFont}`)
.text(`Heading Font: ${brand.typography.headingFont}`)
.text(`Code Font: ${brand.typography.codeFont}`);
doc.moveDown(1);
// Size scale
const sizeEntries = Object.entries(brand.typography.sizes);
for (const [level, size] of sizeEntries) {
doc
.fontSize(10)
.fillColor(brand.colors.text)
.text(`${level.toUpperCase()}: ${size}`, 50);
}
doc.moveDown(2);
// --- Spacing Section ---
doc
.fontSize(20)
.fillColor(brand.colors.text)
.text("Spacing & Layout", 50);
doc.moveDown(0.5);
doc
.fontSize(12)
.text(`Base Unit: ${brand.spacing.baseUnit}`)
.text(`Border Radius: ${brand.spacing.borderRadius}`);
doc.moveDown(2);
// --- Theme Section ---
doc
.fontSize(20)
.fillColor(brand.colors.text)
.text("Theme", 50);
doc.moveDown(0.5);
doc
.fontSize(12)
.text(`Color Scheme: ${brand.theme.colorScheme}`);
// --- Images Section ---
if (brand.images.logo || brand.images.favicon) {
doc.moveDown(2);
doc
.fontSize(20)
.fillColor(brand.colors.text)
.text("Brand Assets", 50);
doc.moveDown(0.5);
if (brand.images.logo) {
doc.fontSize(12).text(`Logo: ${brand.images.logo}`);
}
if (brand.images.favicon) {
doc.fontSize(12).text(`Favicon: ${brand.images.favicon}`);
}
if (brand.images.ogImage) {
doc.fontSize(12).text(`OG Image: ${brand.images.ogImage}`);
}
}
doc.end();
return new Promise<void>((resolve) => {
stream.on("finish", resolve);
});
}Step 3: Main Script
Tie the extraction and PDF generation together.
typescript
// src/index.ts
import { extractBrand } from "./extract";
import { generateStyleGuide } from "./generate-pdf";
async function main() {
const url = process.argv[2] || "https://stripe.com";
const output = process.argv[3] || "style-guide.pdf";
console.log(`Extracting brand data from ${url}...`);
const brand = await extractBrand(url);
console.log("Brand data extracted:");
console.log(` Primary color: ${brand.colors.primary}`);
console.log(` Primary font: ${brand.typography.primaryFont}`);
console.log(` Theme: ${brand.theme.colorScheme}`);
console.log(`\nGenerating PDF: ${output}...`);
await generateStyleGuide(brand, output, url);
console.log("Done! Style guide generated.");
}
main().catch(console.error);Run It
bash
npx tsx src/index.ts https://stripe.com stripe-style-guide.pdf
npx tsx src/index.ts https://vercel.com vercel-style-guide.pdf
npx tsx src/index.ts https://linear.app linear-style-guide.pdfBatch Processing Multiple Websites
Generate style guides for multiple sites in a single run.
typescript
// src/batch.ts
import { extractBrand } from "./extract";
import { generateStyleGuide } from "./generate-pdf";
const websites = [
"https://stripe.com",
"https://vercel.com",
"https://linear.app",
"https://figma.com",
"https://notion.so",
];
async function batchGenerate() {
for (const url of websites) {
const domain = new URL(url).hostname.replace("www.", "");
const outputFile = `${domain}-style-guide.pdf`;
console.log(`Processing ${domain}...`);
try {
const brand = await extractBrand(url);
await generateStyleGuide(brand, outputFile, url);
console.log(` Saved: ${outputFile}`);
} catch (error) {
console.error(` Failed: ${error}`);
}
}
}
batchGenerate();Extension Ideas
| Extension | Description |
|---|---|
| JSON export | Save brand data as JSON for design system tooling |
| Component style docs | Generate CSS variables and utility classes from the brand profile |
| Comparison report | Extract two sites and generate a side-by-side comparison |
| Custom PDF themes | Style the PDF itself using the extracted brand colors |
| Image downloads | Fetch and embed logos/favicons directly in the PDF |
| Web dashboard | Build a Next.js frontend to input URLs and preview results |
Python Alternative
python
from firecrawl import FirecrawlApp
app = FirecrawlApp(api_key="fc-YOUR_API_KEY")
result = app.scrape_url(
"https://stripe.com",
params={"formats": ["branding"]},
)
brand = result["branding"]
print(f"Primary color: {brand['colors']['primary']}")
print(f"Primary font: {brand['typography']['primaryFont']}")
print(f"Theme: {brand['theme']['colorScheme']}")TIP
The branding format works best on marketing pages and homepages where CSS custom properties and design tokens are most visible. For more accurate results, target the main homepage rather than documentation or blog pages.