AI-Powered Web Agency Pipeline

Extract a design system with Firecrawl and deploy a Next.js site via Claude Code.

Introduction

30 Second Summary

Every professional website follows a design system of colors, fonts, and spacing that makes the brand feel consistent. Extracting those patterns from an existing site and turning them into a new codebase usually takes a team days of manual work.

In this project, you will build an autonomous pipeline that pulls a design system from a live website, feeds it to an AI coding agent, and deploys a fully-styled marketing site with editable content in minutes.

What You'll Build

A complete end-to-end web agency pipeline that extracts design tokens from Linear.app using Firecrawl, generates a Next.js application with Claude Code, stores content in MongoDB Atlas, and auto-deploys to Vercel on every push.

By the end of this project, you'll have:

  • A Firecrawl extraction script that pulls structured design tokens (colors, fonts, spacing) from any live website.
  • A CLAUDE.md configuration that instructs Claude Code to autonomously scaffold a styled Next.js application from those tokens.
  • A production deployment pipeline that auto-deploys to Vercel on every GitHub push, with dynamic content served from MongoDB Atlas.
  • Secret Mission: a content update API endpoint that writes to MongoDB and triggers automatic Vercel redeployment.

Are there any prerequisites?

This is an advanced project. You need an active Claude Code CLI subscription (Pro or Max), a Firecrawl API key on a paid tier, a MongoDB Atlas account with a Flex cluster, a Vercel Pro account linked to GitHub, and Node.js 22+ installed.

If any of these are missing, set them up before starting. Step 1 will walk you through project-specific configuration only.

Set Up Your Environment and Project

Before Claude Code can generate a website autonomously, it needs a clear blueprint to follow. That blueprint includes a structured config file, access to your database, and the right dependencies.

In this step, you'll create the project workspace, install the tools your pipeline depends on, and wire up the configuration that tells Claude Code exactly how to build.

In this step, get ready to:
  • Initialize a Node.js project with Firecrawl, MongoDB, and dotenv dependencies.
  • Create a CLAUDE.md configuration file that defines architecture and coding standards.
  • Set up a MongoDB connection utility and verify connectivity.
Initialize the project workspace and install dependencies

Every pipeline starts with a clean workspace. You'll create a dedicated folder, initialize it as a Node.js project, and install the three packages your extraction and database scripts need.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir autonomous-web-agency
cd autonomous-web-agency
  • Initialize a Node.js project by running:
npm init -y
  • Confirm the files exist by running:
ls

You should see package.json in the output.

  • Install the project dependencies by running:
npm install firecrawl mongodb dotenv

What do these packages do?

  • firecrawl is the SDK for extracting structured data (including branding/design tokens) from live websites.
  • mongodb is the official Node.js driver for connecting to your MongoDB Atlas cluster.
  • dotenv loads environment variables from a .env file into process.env.
  • Verify the install succeeded by running:
ls node_modules

You should see folders for firecrawl, mongodb, and dotenv listed.

Install failed or hanging?

  • Make sure you're running Node.js 22 or higher. Firecrawl's SDK requires Node.js >= 22.
  • Check your internet connection. npm install needs to download packages from the npm registry.

Ask for help: Why is npm install failing for firecrawl, mongodb, or dotenv?

Next, you need to enable ES module syntax so your scripts can use import statements instead of require.

  • Open package.json in VS Code.
  • Find the opening { brace and add this line right after it:
"type": "module",

Why set type to module?

By default, Node.js treats .js files as CommonJS. Setting "type": "module" tells Node to use ES module syntax (import/export) in all .js and .mjs files. Your extraction script and seed script both use ES module imports.

  • Save package.json.

Now create the environment file that holds your API keys and connection strings.

  • Create a file called .env in your project root and paste this content:
FIRECRAWL_API_KEY=fc-your-api-key-here
MONGODB_URI=mongodb+srv://your-user:your-password-here@your-cluster.mongodb.net/agency_cms?retryWrites=true&w=majority
VERCEL_DEPLOY_HOOK_URL=https://api.vercel.com/v1/integrations/deploy/your-hook-id/your-hook-token
CONTENT_UPDATE_TOKEN=your-secure-bearer-token-here

What goes in each variable?

  • FIRECRAWL_API_KEY is your Firecrawl API key from your Firecrawl dashboard.
  • MONGODB_URI is the connection string from your MongoDB Atlas Flex cluster. Find it in your cluster's Connect dialog.
  • VERCEL_DEPLOY_HOOK_URL will be created in a later step. Leave the placeholder for now.
  • CONTENT_UPDATE_TOKEN is a bearer token you'll use in the Secret Mission. Leave the placeholder for now.
  • Replace the FIRECRAWL_API_KEY placeholder with your actual Firecrawl API key.
  • Replace the MONGODB_URI placeholder with your actual MongoDB Atlas connection string.
  • Save .env.

Not sure where to find your MongoDB connection string?

In MongoDB Atlas, click Connect on your Flex cluster, then select Drivers. Copy the connection string and replace <password> with your database user's password.

Ask for help: How do I find my MongoDB Atlas connection string?

Configure the CLAUDE.md file for autonomous generation

Claude Code reads a CLAUDE.md file at the project root to understand architecture decisions and coding standards. This file acts as the blueprint that tells Claude Code how to structure the generated site.

  • Create a file called CLAUDE.md in your project root and paste this content:
# Project: Autonomous Web Agency Pipeline

## Architecture
- Framework: Next.js 15 (App Router)
- Styling: Tailwind CSS configured from design-system.json tokens
- Database: MongoDB Atlas (agency_cms database, pages collection)
- Deployment: Vercel (auto-deploy on push to main)

## Coding Standards
- TypeScript strict mode
- Use Next.js App Router conventions (src/app/ directory)
- Server Components by default, Client Components only when interactivity is required
- All page content fetched from /api/content endpoint (no hardcoded strings)
- Tailwind classes only, no inline styles or CSS modules

## Design System Integration
- Read design-system.json and map colors to tailwind.config.ts theme.extend.colors
- Map fonts to theme.extend.fontFamily
- Use the extracted spacing scale for consistent padding/margins
- Respect the colorScheme (dark/light) for default backgrounds

## File Structure
- src/app/layout.tsx - Root layout with font imports and global styles
- src/app/page.tsx - Homepage with hero, features, CTA sections
- src/app/api/content/route.ts - GET endpoint serving page content from MongoDB
- src/lib/mongodb.ts - MongoDB connection singleton
- src/components/ - Reusable UI components (Hero, FeatureCard, CTA)

## Commands
- Dev: npm run dev
- Build: npm run build
- Seed DB: node seed-content.mjs

What does CLAUDE.md do?

When you prompt Claude Code to generate a site, it reads CLAUDE.md first. This file locks in decisions like which framework to use, where files go, and how styling works. Without it, Claude Code would make its own (potentially inconsistent) choices on every run.

The Design System Integration section is critical. It tells Claude Code to read the design tokens you'll extract from Linear.app in the next step and map them directly to Tailwind CSS configuration.

  • Save CLAUDE.md.
Connect to MongoDB Atlas and verify

Your pipeline serves all page content from MongoDB. You need a connection utility that handles connection pooling in both development and production environments.

  • Create the directory structure for the utility by running:
mkdir -p src/lib
  • Create a file called src/lib/mongodb.ts and paste this content:
import { MongoClient } from 'mongodb';

if (!process.env.MONGODB_URI) {
  throw new Error('MONGODB_URI environment variable is not set');
}

const uri = process.env.MONGODB_URI;
const options = {};

let client: MongoClient;
let clientPromise: Promise<MongoClient>;

declare global {
  var _mongoClientPromise: Promise<MongoClient> | undefined;
}

if (process.env.NODE_ENV === 'development') {
  if (!global._mongoClientPromise) {
    client = new MongoClient(uri, options);
    global._mongoClientPromise = client.connect();
  }
  clientPromise = global._mongoClientPromise;
} else {
  client = new MongoClient(uri, options);
  clientPromise = client.connect();
}

export default clientPromise;

Why use a global variable for the connection?

In development, Next.js hot-reloads your code frequently. Without the global cache, every reload would open a new database connection, eventually exhausting your cluster's connection limit.

The global._mongoClientPromise pattern stores the connection promise on the Node.js global object so it persists across hot reloads. In production, a fresh connection is created once per cold start.

  • Save src/lib/mongodb.ts.

Now verify that your MongoDB Atlas cluster is reachable with your connection string.

  • Test your database connectivity by running this command:
node --env-file=.env -e "import('mongodb').then(m => new m.MongoClient(process.env.MONGODB_URI).connect().then(() => console.log('Connected')))"

You should see Connected printed in your terminal.

Seeing a connection error?

  • Double-check your MONGODB_URI in .env. Make sure you replaced the password placeholder with your actual database user password.
  • In MongoDB Atlas, go to Network Access and make sure your current IP address is on the allow list. Adding 0.0.0.0/0 allows access from anywhere (fine for development).
  • Verify your cluster is a Flex tier (not paused or terminated).

Ask for help: My MongoDB connection test is failing. How do I debug this?

✔️ Awesome, I've got everything!

Great. Make sure all your files are saved before moving on.

ⓧ I'd like to double check the full code

Here is what each file should look like at this point.

.env:

FIRECRAWL_API_KEY=fc-your-api-key-here
MONGODB_URI=mongodb+srv://your-user:your-password-here@your-cluster.mongodb.net/agency_cms?retryWrites=true&w=majority
VERCEL_DEPLOY_HOOK_URL=https://api.vercel.com/v1/integrations/deploy/your-hook-id/your-hook-token
CONTENT_UPDATE_TOKEN=your-secure-bearer-token-here

CLAUDE.md:

# Project: Autonomous Web Agency Pipeline

## Architecture
- Framework: Next.js 15 (App Router)
- Styling: Tailwind CSS configured from design-system.json tokens
- Database: MongoDB Atlas (agency_cms database, pages collection)
- Deployment: Vercel (auto-deploy on push to main)

## Coding Standards
- TypeScript strict mode
- Use Next.js App Router conventions (src/app/ directory)
- Server Components by default, Client Components only when interactivity is required
- All page content fetched from /api/content endpoint (no hardcoded strings)
- Tailwind classes only, no inline styles or CSS modules

## Design System Integration
- Read design-system.json and map colors to tailwind.config.ts theme.extend.colors
- Map fonts to theme.extend.fontFamily
- Use the extracted spacing scale for consistent padding/margins
- Respect the colorScheme (dark/light) for default backgrounds

## File Structure
- src/app/layout.tsx - Root layout with font imports and global styles
- src/app/page.tsx - Homepage with hero, features, CTA sections
- src/app/api/content/route.ts - GET endpoint serving page content from MongoDB
- src/lib/mongodb.ts - MongoDB connection singleton
- src/components/ - Reusable UI components (Hero, FeatureCard, CTA)

## Commands
- Dev: npm run dev
- Build: npm run build
- Seed DB: node seed-content.mjs

src/lib/mongodb.ts:

import { MongoClient } from 'mongodb';

if (!process.env.MONGODB_URI) {
  throw new Error('MONGODB_URI environment variable is not set');
}

const uri = process.env.MONGODB_URI;
const options = {};

let client: MongoClient;
let clientPromise: Promise<MongoClient>;

declare global {
  var _mongoClientPromise: Promise<MongoClient> | undefined;
}

if (process.env.NODE_ENV === 'development') {
  if (!global._mongoClientPromise) {
    client = new MongoClient(uri, options);
    global._mongoClientPromise = client.connect();
  }
  clientPromise = global._mongoClientPromise;
} else {
  client = new MongoClient(uri, options);
  clientPromise = client.connect();
}

export default clientPromise;

Your environment is locked and loaded. Next up, you'll run the Firecrawl extraction script to pull a complete design system from Linear.app.

Extract the Design System with Firecrawl

Your environment is configured and MongoDB is connected. Now it is time to extract the raw design DNA from a real website.

Instead of manually inspecting CSS and guessing hex codes, you will use Firecrawl's branding format to pull a complete design system from Linear's homepage. This gives Claude Code a structured blueprint to work from.

In this step, get ready to:
  • Write an extraction script that scrapes design tokens from a live site.
  • Run the script to pull Linear.app's branding profile.
  • Validate the processed design system JSON for Claude Code.
Write the extraction script using Firecrawl's branding format

Firecrawl's branding format analyzes a page's CSS, fonts, and visual elements. It returns structured tokens for colors, typography, spacing, and UI components.

This is the data that will drive every styling decision in your generated site.

  • Create a new file called extract-design.mjs in your project root by running:
touch extract-design.mjs
  • Open extract-design.mjs in VS Code and paste the following extraction script:
import { Firecrawl } from 'firecrawl';
import { writeFileSync } from 'fs';
import 'dotenv/config';

// Initialize Firecrawl with the API key from .env
const firecrawl = new Firecrawl({ apiKey: process.env.FIRECRAWL_API_KEY });

async function extractDesignSystem() {
  console.log('Extracting design system from Linear.app...');

  // Scrape Linear.app using branding and markdown formats
  const result = await firecrawl.scrape('https://linear.app', {
    formats: ['branding', 'markdown'],
  });

  // Save the raw branding profile for reference
  writeFileSync('raw-branding.json', JSON.stringify(result.branding, null, 2));
  console.log('Raw branding profile saved.');

  // Process raw output into clean design tokens with fallbacks
  const designSystem = {
    colors: {
      primary: result.branding?.colors?.primary || '#5E6AD2',
      secondary: result.branding?.colors?.secondary || '#1F2023',
      accent: result.branding?.colors?.accent || '#8A8F98',
      background: result.branding?.colors?.background || '#000000',
      foreground: result.branding?.colors?.foreground || '#FFFFFF',
    },
    fonts: result.branding?.fonts || ['Inter', 'system-ui', 'sans-serif'],
    typography: {
      headingScale: result.branding?.typography?.heading || { fontSize: '3rem', fontWeight: '600' },
      bodyScale: result.branding?.typography?.body || { fontSize: '1rem', lineHeight: '1.6' },
    },
    spacing: result.branding?.spacing || { base: '0.25rem', scale: [0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8] },
    borderRadius: result.branding?.components?.borderRadius || '0.5rem',
    colorScheme: result.branding?.colorScheme || 'dark',
  };

  // Write the processed design system and log a summary
  writeFileSync('design-system.json', JSON.stringify(designSystem, null, 2));
  console.log('Processed design system saved to design-system.json');
  console.log(`  Colors: ${Object.keys(designSystem.colors).length} tokens`);
  console.log(`  Fonts: ${designSystem.fonts.join(', ')}`);
  console.log(`  Scheme: ${designSystem.colorScheme}`);
}

extractDesignSystem().catch(console.error);

What does this script do?

The script does two things: scrapes and processes.

  • firecrawl.scrape() hits Linear.app with the branding and markdown formats. The branding format extracts color scheme, colors with semantic roles, fonts, typography details, spacing, and UI component styles.
  • The raw branding output is saved to raw-branding.json for reference.
  • The processing step maps the raw output into a clean design-system.json with fallback values. If Firecrawl does not return a specific token, the script uses Linear's known defaults (like #5E6AD2 for primary purple).
  • Save extract-design.mjs.

File not saving?

Make sure you are saving to the project root directory, not inside the src/ folder. The file should sit alongside your package.json and CLAUDE.md.

help me check my project file structure

Run the extraction and process the output

The script is ready. Running it will hit the Firecrawl API, scrape Linear.app's branding profile, and write two JSON files to your project root.

  • Execute the extraction script by running:
node extract-design.mjs

What should I see?

The script takes a few seconds to complete. You should see output like:

Extracting design system from Linear.app...
Raw branding profile saved.
Processed design system saved to design-system.json
  Colors: 5 tokens
  Fonts: Inter, system-ui, sans-serif
  Scheme: dark

This confirms Firecrawl successfully extracted a dark color scheme with 5 color tokens and the Inter font family.

  • Confirm the output files exist by running:
ls *.json
  • You should see design-system.json, package.json, and raw-branding.json in the output.

Seeing an authentication error?

Double check that your .env file has your actual Firecrawl API key replacing the placeholder. The key should start with fc-.

Also confirm the dotenv package is installed. The import 'dotenv/config' line at the top of the script loads your .env file automatically.

help me debug my Firecrawl API connection

Validate the extracted design system

The extraction wrote a processed design-system.json file. This is what Claude Code will consume to drive all Tailwind CSS configuration.

  • View the contents of your design system file by running:
cat design-system.json
  • Confirm the JSON contains a colors object with 5 keys (primary, secondary, accent, background, foreground).
  • Confirm a fonts array is present.
  • Confirm a typography object has headingScale and bodyScale entries.
  • Confirm colorScheme is set to "dark".

Why fallback values?

The script uses fallback values (like '#5E6AD2' for primary) because Firecrawl's extraction depends on how CSS is structured on the target page. If a site uses CSS variables or dynamically loaded styles, some tokens might not be detected.

The fallbacks ensure Claude Code always has complete design tokens to work with, even if extraction is partial.

✔️ Awesome, I've got everything!

Great. Make sure you have saved extract-design.mjs and that both design-system.json and raw-branding.json are present in your project root.

ⓧ I'd like to double check the full code

Here is the complete extract-design.mjs file:

import { Firecrawl } from 'firecrawl';
import { writeFileSync } from 'fs';
import 'dotenv/config';

const firecrawl = new Firecrawl({ apiKey: process.env.FIRECRAWL_API_KEY });

async function extractDesignSystem() {
  console.log('Extracting design system from Linear.app...');

  const result = await firecrawl.scrape('https://linear.app', {
    formats: ['branding', 'markdown'],
  });

  writeFileSync('raw-branding.json', JSON.stringify(result.branding, null, 2));
  console.log('Raw branding profile saved.');

  const designSystem = {
    colors: {
      primary: result.branding?.colors?.primary || '#5E6AD2',
      secondary: result.branding?.colors?.secondary || '#1F2023',
      accent: result.branding?.colors?.accent || '#8A8F98',
      background: result.branding?.colors?.background || '#000000',
      foreground: result.branding?.colors?.foreground || '#FFFFFF',
    },
    fonts: result.branding?.fonts || ['Inter', 'system-ui', 'sans-serif'],
    typography: {
      headingScale: result.branding?.typography?.heading || { fontSize: '3rem', fontWeight: '600' },
      bodyScale: result.branding?.typography?.body || { fontSize: '1rem', lineHeight: '1.6' },
    },
    spacing: result.branding?.spacing || { base: '0.25rem', scale: [0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8] },
    borderRadius: result.branding?.components?.borderRadius || '0.5rem',
    colorScheme: result.branding?.colorScheme || 'dark',
  };

  writeFileSync('design-system.json', JSON.stringify(designSystem, null, 2));
  console.log('Processed design system saved to design-system.json');
  console.log(`  Colors: ${Object.keys(designSystem.colors).length} tokens`);
  console.log(`  Fonts: ${designSystem.fonts.join(', ')}`);
  console.log(`  Scheme: ${designSystem.colorScheme}`);
}

extractDesignSystem().catch(console.error);

And here is what your design-system.json output should look like:

{
  "colors": {
    "primary": "#5E6AD2",
    "secondary": "#1F2023",
    "accent": "#8A8F98",
    "background": "#000000",
    "foreground": "#FFFFFF"
  },
  "fonts": ["Inter", "system-ui", "sans-serif"],
  "typography": {
    "headingScale": { "fontSize": "3rem", "fontWeight": "600" },
    "bodyScale": { "fontSize": "1rem", "lineHeight": "1.6" }
  },
  "spacing": { "base": "0.25rem", "scale": [0.25, 0.5, 1, 1.5, 2, 3, 4, 6, 8] },
  "borderRadius": "0.5rem",
  "colorScheme": "dark"
}

Why Firecrawl over manual extraction?

Firecrawl's branding format runs a Chrome DevTools Protocol script that collects raw CSS values, typography, images, and color schemes in-page. It then uses LLM-assisted classification to assign semantic roles (primary, accent, background) to colors and clean up font stacks.

This produces structured tokens that map directly to a Tailwind config. Manual extraction would require inspecting dozens of elements, guessing semantic roles, and formatting everything by hand.

Your design system is captured and structured. Next, you will feed these tokens to Claude Code and let it autonomously generate a full Next.js application styled to match Linear's design language.

Generate the Site Autonomously with Claude Code

You have a complete design system extracted from Linear.app sitting in design-system.json. Now it is time to feed those tokens to Claude Code and let it autonomously scaffold an entire Next.js application.

Claude Code reads the CLAUDE.md blueprint and the design tokens, then generates every file the app needs. Your role shifts from writing code to reviewing and accepting what the agent produces.

In this step, get ready to:
  • Scaffold a Next.js 15 application using Claude Code.
  • Review and verify the generated code.
  • Seed MongoDB with content and test the site locally.
Scaffold the Next.js application using Claude Code

Claude Code can read files in your project directory and generate entire application structures from a single prompt. Because you already set up CLAUDE.md with architecture decisions and design-system.json with extracted tokens, Claude Code has everything it needs to produce a fully styled site.

  • Launch Claude Code with the scaffolding prompt by running this command from your project root:
claude "Read CLAUDE.md and design-system.json. Scaffold a Next.js 15 App Router project with Tailwind CSS. Use the extracted design tokens to configure tailwind.config.ts colors, fonts, and spacing. Create a homepage with hero, features, and CTA sections. All text content should be fetched from MongoDB via a /api/content route."

What does this command do?

The claude "..." syntax starts an interactive Claude Code session with an initial prompt already submitted. Claude Code reads your project files (including CLAUDE.md and design-system.json) and begins generating the application structure.

You will see Claude Code propose file creations and edits as diffs in your terminal. It will ask for permission before writing each file to disk.

Allow file edits when prompted

Claude Code will ask permission to create files like next.config.mjs, tailwind.config.ts, src/app/layout.tsx, src/app/page.tsx, and component files. Accept each proposed change. If Claude Code asks to run shell commands (like npm install), allow those as well.

Claude Code will generate the following files: next.config.mjs, tailwind.config.ts (with your extracted design tokens mapped to the theme), src/app/layout.tsx, src/app/page.tsx, src/app/api/content/route.ts, and reusable components in src/components/.

Claude Code not generating files?

Make sure you are running the command from your project root directory where CLAUDE.md and design-system.json are located. Claude Code needs to find these files to follow the blueprint.

If Claude Code asks which model to use or requires login, follow the on-screen prompts to authenticate with your Anthropic account.

Help me troubleshoot Claude Code not reading my project files.

Review and accept the generated code

After Claude Code finishes generating, you need to verify that the key files match your design system and architecture requirements.

  • Check that tailwind.config.ts maps your extracted colors (#5E6AD2 primary, #1F2023 secondary, #000000 background) and fonts (Inter) from design-system.json.
  • Confirm that src/app/api/content/route.ts imports and uses the MongoDB connection singleton from @/lib/mongodb.
  • Verify that src/app/page.tsx fetches content from the /api/content endpoint rather than using hardcoded strings.

The API route should look like this:

import { NextResponse } from 'next/server';
import clientPromise from '@/lib/mongodb';

export async function GET() {
  const client = await clientPromise;
  const db = client.db('agency_cms');
  const page = await db.collection('pages').findOne({ slug: 'homepage' });

  if (!page) {
    return NextResponse.json({ error: 'Content not found' }, { status: 404 });
  }

  return NextResponse.json(page);
}

What does this API route do?

This is a Next.js App Router API route that responds to GET requests. It connects to the agency_cms database, looks up the document with slug: 'homepage', and returns it as JSON.

The homepage component fetches this endpoint at render time, which means all page content lives in MongoDB and can be updated without redeploying code.

Why fetch content from an API instead of hardcoding it?

This is the headless CMS pattern. By separating content (stored in MongoDB) from presentation (the Next.js frontend), you can update text, images, and structure without touching the codebase.

Later in this project, you will trigger automatic redeployments when content changes. This architecture makes that possible.

  • If Claude Code's generated API route does not match the code above, ask Claude Code to update it by typing a follow-up prompt in the same session:
Update src/app/api/content/route.ts to use clientPromise from @/lib/mongodb, query the agency_cms database pages collection for slug 'homepage', and return it as JSON with a 404 fallback.

Generated code does not match?

Claude Code's output may vary slightly from the reference above. The key things to confirm are: the import path uses @/lib/mongodb, the database name is agency_cms, and the collection name is pages. Minor differences in error handling or response formatting are acceptable.

Help me verify my Claude Code generated files match the expected architecture.

Seed MongoDB with initial content and test locally

The API route is ready, but the database is empty. You need a seed script that inserts the homepage content document so the site has something to display.

  • Create a file called seed-content.mjs in your project root by running this in your terminal:
touch seed-content.mjs
  • Open seed-content.mjs in VS Code and paste the following code:
import { MongoClient } from 'mongodb';
import 'dotenv/config';

const client = new MongoClient(process.env.MONGODB_URI);

async function seedContent() {
  await client.connect();
  const db = client.db('agency_cms');
  const collection = db.collection('pages');

  await collection.updateOne(
    { slug: 'homepage' },
    {
      $set: {
        slug: 'homepage',
        hero: {
          title: 'Ship faster with intelligent automation',
          subtitle: 'AI-powered project management that adapts to your workflow.',
        },
        features: [
          { title: 'Autonomous Workflows', description: 'Let AI handle the repetitive work while you focus on what matters.' },
          { title: 'Real-time Sync', description: 'Every change propagates instantly across your entire pipeline.' },
          { title: 'Zero Maintenance', description: 'Self-healing infrastructure that never needs manual intervention.' },
        ],
        cta: { text: 'Start Building', href: '/signup' },
        updatedAt: new Date(),
      },
    },
    { upsert: true }
  );

  console.log('Homepage content seeded successfully.');
  await client.close();
}

seedContent().catch(console.error);

What does this script do?

The script connects to your MongoDB Atlas cluster and uses updateOne with upsert: true. This means it inserts the document if it does not exist, or updates it if it does. Running the script multiple times is safe.

The content mirrors what a real marketing page needs: a hero section with headline and subtitle, a features array with three items, and a call-to-action button.

  • Save seed-content.mjs.
  • Run the seed script to insert your homepage content into MongoDB:
node seed-content.mjs
  • You should see Homepage content seeded successfully. in your terminal output.

Seeing a connection error?

Verify that your MONGODB_URI in the .env file has the correct username, password, and cluster address. Also confirm your IP address is whitelisted in MongoDB Atlas under Network Access.

Help me debug my MongoDB connection error when running the seed script.

  • Start the Next.js development server by running:
npm run dev
  • Open http://localhost:3000 in your browser.
  • You should see a styled homepage with a dark background (#000000), purple accent elements (#5E6AD2), and content pulled from MongoDB: the hero title "Ship faster with intelligent automation", three feature cards, and a "Start Building" CTA button.

Page shows 'Content not found' or is blank?

If you see a 404 or empty page, the seed script may not have written to the correct database. Open your MongoDB Atlas console and check that the agency_cms database has a pages collection with a document where slug is homepage.

Also check your terminal for errors from the dev server. The MONGODB_URI must also be set in a .env.local file for Next.js to read it at runtime. If Claude Code did not create one, copy your .env to .env.local.

Help me figure out why my Next.js site cannot fetch content from MongoDB.

✔️ Awesome, I've got everything!

Great. Double check you have saved seed-content.mjs and that your dev server is running with content visible in the browser.

ⓧ I'd like to double check the full code

Here is the complete seed-content.mjs file:

import { MongoClient } from 'mongodb';
import 'dotenv/config';

const client = new MongoClient(process.env.MONGODB_URI);

async function seedContent() {
  await client.connect();
  const db = client.db('agency_cms');
  const collection = db.collection('pages');

  await collection.updateOne(
    { slug: 'homepage' },
    {
      $set: {
        slug: 'homepage',
        hero: {
          title: 'Ship faster with intelligent automation',
          subtitle: 'AI-powered project management that adapts to your workflow.',
        },
        features: [
          { title: 'Autonomous Workflows', description: 'Let AI handle the repetitive work while you focus on what matters.' },
          { title: 'Real-time Sync', description: 'Every change propagates instantly across your entire pipeline.' },
          { title: 'Zero Maintenance', description: 'Self-healing infrastructure that never needs manual intervention.' },
        ],
        cta: { text: 'Start Building', href: '/signup' },
        updatedAt: new Date(),
      },
    },
    { upsert: true }
  );

  console.log('Homepage content seeded successfully.');
  await client.close();
}

seedContent().catch(console.error);

And here is the expected src/app/api/content/route.ts (generated by Claude Code):

import { NextResponse } from 'next/server';
import clientPromise from '@/lib/mongodb';

export async function GET() {
  const client = await clientPromise;
  const db = client.db('agency_cms');
  const page = await db.collection('pages').findOne({ slug: 'homepage' });

  if (!page) {
    return NextResponse.json({ error: 'Content not found' }, { status: 404 });
  }

  return NextResponse.json(page);
}

Your autonomous pipeline just generated a complete production-ready web application from design tokens alone. Next up, you will push this to GitHub and configure Vercel for automatic deployments on every commit.

Deploy with CI/CD to Vercel

Your Next.js application runs perfectly on your local machine, pulling content from MongoDB and rendering it with the extracted design tokens.

The final piece of the pipeline is deploying it to the internet. In this step, you will push your code to GitHub and connect it to Vercel for automatic deployments on every push. You will also create a Deploy Hook so external systems can trigger rebuilds when content changes.

In this step, get ready to:
  • Initialize a Git repository and push your project to GitHub.
  • Connect Vercel to your GitHub repo and deploy to production.
  • Create a Deploy Hook for content-triggered rebuilds.
Initialize Git and push to GitHub

Version control tracks every change you make. It also connects your local code to Vercel's automatic deployment pipeline.

  • Initialize a new Git repository by running this command in your project terminal:
git init
  • Create a .gitignore file to exclude files that should not be committed. Run this command:
echo "node_modules\n.env\n.next" > .gitignore

What does .gitignore do?

This file tells Git to skip certain files and folders. node_modules contains thousands of dependency files that can be reinstalled. .env holds your secrets that must never be committed. .next is the build output folder that Vercel regenerates on every deploy.

  • Confirm the file was created correctly by running:
cat .gitignore

You should see three lines: node_modules, .env, and .next.

  • Stage all project files and create your initial commit by running these commands:
git add .
git commit -m "Initial commit: autonomous web agency pipeline"

What do these commands do?

  • git add . stages every file in the project (except those in .gitignore) for committing.
  • git commit -m saves a snapshot of your code with a descriptive message.

You should see a list of files being committed, including extract-design.mjs, seed-content.mjs, CLAUDE.md, and your src/ directory.

  • Open github.com in your browser.
  • Click the + icon in the top right corner, then select New repository.
  • Name your repository (for example, autonomous-web-agency).
  • Leave all initialization options unchecked (no README, no .gitignore, no license). You already have code ready to push.
  • Click Create repository.
  • Copy the remote URL shown on the next page (it starts with https://github.com/).
  • Connect your local repo to GitHub and push your code by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/your-repo-name.git"]]
git branch -M main
git push -u origin main

What do these commands do?

  • git remote add origin links your local repository to the GitHub remote.
  • git branch -M main renames the default branch to main.
  • git push -u origin main uploads all committed files to GitHub.

You should see output confirming the push, ending with a line like Branch 'main' set up to track remote branch 'main' from 'origin'.

Push failed or permission denied?

  • Make sure you are signed into the correct GitHub account in your terminal. Run gh auth status to check.
  • If you see a 403 error, your GitHub credentials may have expired. Re-authenticate with gh auth login or update your personal access token.

Why can't I push to GitHub?

Connect Vercel and configure environment variables

Vercel auto-detects Next.js projects and handles the entire build process. Once connected to your GitHub repo, every push to main triggers a new production deployment automatically.

  • Open vercel.com and sign in to your account.
  • Click the Add New... dropdown button, then select Project.
  • In the Import Git Repository section, find your repository and click Import.
  • Vercel auto-detects the Next.js framework. Leave the default settings (Framework Preset, Root Directory, Build Command) as they are.
  • Before clicking Deploy, expand the Environment Variables section on the configuration page.
  • Add your MongoDB connection string. Enter MONGODB_URI as the key, and paste your full mongodb+srv:// connection string as the value.
  • Click Deploy.

Why set MONGODB_URI here?

Your .env file is excluded from Git (it is in .gitignore), so Vercel does not have access to your secrets. You must add MONGODB_URI directly in Vercel's environment variable settings so the production build can connect to your database.

Vercel streams the build output live. Wait for the build to complete. This typically takes 30-60 seconds.

  • Once the deployment finishes, Vercel shows a Ready status and a production URL (ending in .vercel.app).
  • Click the production URL to open your live site.

You should see your styled homepage with the hero section, features, and CTA. All content is being served from your MongoDB Atlas database.

Build failed or page shows an error?

  • Check the Vercel build logs for an error mentioning MONGODB_URI. If you see it, return to your project Settings > Environment Variables and confirm the value is correct.
  • If the build log shows a TypeScript or import error, make sure all files from the previous steps were committed. Run git status locally to check for uncommitted files.
  • If your MongoDB Atlas cluster has IP access restrictions, add 0.0.0.0/0 to the Network Access list so Vercel's serverless functions can connect.

Why is my Vercel deployment failing?

Create a Deploy Hook for content-triggered rebuilds

A Deploy Hook is a unique URL that triggers a new Vercel deployment when called with an HTTP POST request. This lets external systems (like a CMS or an API endpoint) rebuild your site without needing a code push.

  • In the Vercel dashboard, open your project.
  • Click Settings in the sidebar.
  • Select Git from the settings menu.
  • Scroll down to the Deploy Hooks section.
  • Enter cms-rebuild as the hook name.
  • Select main as the branch.
  • Click Create Hook.
  • Copy the generated URL by clicking the copy button next to it.

What is a Deploy Hook?

Deploy Hooks accept HTTP POST requests and trigger a new deployment without requiring a code commit. No authorization header is needed. The URL format is https://api.vercel.com/v1/integrations/deploy/{hook-id}/{hook-token}. You can have up to 5 deploy hooks per project on Hobby/Pro plans.

  • Open your .env file and paste the Deploy Hook URL as the value for VERCEL_DEPLOY_HOOK_URL. Replace the placeholder with the URL you just copied.
  • Test that the Deploy Hook works by running this command in your terminal:
curl -X POST [[DEPLOY_HOOK_URL="your Deploy Hook URL from Vercel"]]

You should see a JSON response containing a job object with a state of PENDING. This confirms Vercel received the request and queued a new deployment.

  • Switch back to the Vercel dashboard. Click Deployments in the sidebar.
  • Confirm you see a new deployment triggered by the Deploy Hook (it will show the hook name cms-rebuild as the source).

Curl returned an error or no deployment appeared?

  • Double-check the URL you pasted. The full URL must include both the hook ID and token segments.
  • Make sure you used -X POST in the curl command. GET requests also work for Deploy Hooks, but POST is the standard method.
  • If the deployment was queued but never completed, check your Vercel build logs for errors (usually a missing environment variable).

Why isn't my Deploy Hook triggering a build?

Secret mission

Content Update Webhook

Your pipeline extracts designs, generates code, and deploys automatically. But what about content changes? Right now, updating a headline means editing seed-content.mjs and redeploying manually. In this secret mission, you'll build a secure API endpoint that accepts content updates via POST, writes directly to MongoDB, and triggers a Vercel redeployment. The result is a true zero-maintenance loop.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your MongoDB Atlas Flex cluster is the primary ongoing cost at $8-30/month depending on usage.

Cost warning

Your MongoDB Atlas Flex cluster bills between $8 and $30 per month based on storage and operations. If you leave it running without actively using the site, you will still incur the base charge. Consider pausing (downgrading to Free tier) or deleting the cluster if you are done experimenting.

Resources you used:

  • MongoDB Atlas Flex cluster (agency_cms database with homepage content).
  • Vercel project (deployed Next.js site with environment variables and Deploy Hook).
  • GitHub repository (source code pushed to main branch).
  • Local project files on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building on the pipeline or plan to use it as a base for future projects.

  • Your Vercel deployment remains live at no additional cost under your Pro plan.
  • Your MongoDB Atlas Flex cluster continues billing at $8-30/month.
  • Your GitHub repository stays accessible for future pushes and automatic deploys.

Pause - I'll come back to this later

Shut down the paid cluster to stop billing while keeping your code and deployment intact. You can restore everything later.

  • In MongoDB Atlas, downgrade your Flex cluster to the Free tier (M0) to stop usage-based billing. Your data remains intact up to the 0.5 GB Free tier limit.
  • Your Vercel deployment stays live at no additional cost. No action needed there.
  • Your GitHub repository and local files remain untouched.

Delete - I don't want to use this again

Remove all project resources and start fresh. This is permanent for cloud resources.

  • Terminate your MongoDB Atlas cluster: open the Atlas console, click Clusters in the left sidebar under Database, click the ... menu next to your cluster, select Terminate, type the cluster name to confirm, then click Terminate.
  • Delete your Vercel project: open the Vercel dashboard, select your project, click Settings in the sidebar, scroll to the bottom of the General page, click Delete in the Delete Project section, enter the project name, then click Continue.
  • Archive your GitHub repository: navigate to your repository on GitHub, click Settings, scroll down to Danger Zone, click Archive this repository, type the repository name, then click I understand the consequences, archive this repository.
  • Delete your local project files by running this command (macOS/Linux):
cd ~/Desktop
rm -rf web-agency-pipeline

On Windows, run:

cd ~\Desktop
Remove-Item -Recurse -Force web-agency-pipeline

What about your accounts?

Your MongoDB Atlas account, Vercel account, GitHub account, and Firecrawl account are pre-existing tools you set up before this project. They remain active and usable for future projects. No action needed unless you want to close them entirely through each provider's account settings.

That's a Wrap!

That's a Wrap!

Nice work. You just built a complete autonomous web agency pipeline that extracts design tokens from a live website, generates a full-stack application, and deploys it with zero manual intervention.

You've learned how to:

  • Built a Firecrawl extraction script that pulls a structured design system (colors, fonts, typography, spacing) from any live website and outputs reusable design tokens as JSON.
  • Used Claude Code with a CLAUDE.md blueprint to autonomously generate a complete Next.js 15 application with Tailwind CSS styling derived from the extracted design system.
  • Deployed via a GitHub-to-Vercel CI/CD pipeline with automatic production builds on every push, connected to MongoDB Atlas for dynamic content.
  • Secret Mission: Created a content update webhook that writes to MongoDB and triggers Vercel redeployment, completing the zero-maintenance loop.

Ready to quiz yourself?