Builder.io Integration

Implementing visual content management with Builder.io in ShipKit

Builder.io Integration

ShipKit integrates with Builder.io for visual content management, custom component registration, and dynamic content delivery. This guide covers setup, component registration, and content management.

Setup

Installation

pnpm add @builder.io/react @builder.io/sdk @builder.io/sdk-react

Configuration

// src/lib/builder/config.ts
import { builder } from '@builder.io/react';
import { env } from '@/env';

// Initialize the Builder SDK
builder.init(env.BUILDER_PUBLIC_KEY);

export const BUILDER_CONFIG = {
  apiKey: env.BUILDER_PUBLIC_KEY,
  apiSecret: env.BUILDER_PRIVATE_KEY, // Only used server-side
  previewSecret: env.BUILDER_PREVIEW_SECRET,
  models: {
    page: {
      model: 'page',
      apiVersion: 'v3',
    },
    section: {
      model: 'section',
      apiVersion: 'v3',
    },
  },
} as const;

Component Registration

Custom Components

// src/components/builder/hero-section.tsx
'use client';

import { Builder } from '@builder.io/react';
import { motion } from 'framer-motion';

interface HeroProps {
  title: string;
  subtitle: string;
  image: string;
  ctaText: string;
  ctaLink: string;
}

export const HeroSection = ({
  title,
  subtitle,
  image,
  ctaText,
  ctaLink,
}: HeroProps) => {
  return (
    <section className="relative h-[600px]">
      <div className="absolute inset-0">
        <img
          src={image}
          alt={title}
          className="object-cover w-full h-full"
        />
      </div>
      <div className="relative z-10 container mx-auto px-4 py-20">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6 }}
        >
          <h1 className="text-5xl font-bold text-white">{title}</h1>
          <p className="text-xl text-white mt-4">{subtitle}</p>
          <a
            href={ctaLink}
            className="mt-8 inline-block px-8 py-3 bg-primary text-white rounded-lg"
          >
            {ctaText}
          </a>
        </motion.div>
      </div>
    </section>
  );
};

// Register the component with Builder.io
Builder.registerComponent(HeroSection, {
  name: 'Hero Section',
  inputs: [
    {
      name: 'title',
      type: 'string',
      required: true,
    },
    {
      name: 'subtitle',
      type: 'string',
    },
    {
      name: 'image',
      type: 'file',
      allowedFileTypes: ['jpeg', 'jpg', 'png', 'webp'],
      required: true,
    },
    {
      name: 'ctaText',
      type: 'string',
      defaultValue: 'Learn More',
    },
    {
      name: 'ctaLink',
      type: 'url',
    },
  ],
});

Component Library

// src/lib/builder/components.ts
import { Builder } from '@builder.io/react';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';

// Register UI components
Builder.registerComponent(Button, {
  name: 'Button',
  inputs: [
    {
      name: 'text',
      type: 'string',
      defaultValue: 'Click me',
    },
    {
      name: 'variant',
      type: 'enum',
      enum: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
      defaultValue: 'default',
    },
    {
      name: 'size',
      type: 'enum',
      enum: ['default', 'sm', 'lg', 'icon'],
      defaultValue: 'default',
    },
  ],
});

// Register more components...

Visual Editing

Page Builder Integration

// src/app/[...path]/page.tsx
import { builder } from '@builder.io/react';
import { RenderBuilderContent } from '@/components/builder/render-content';
import { notFound } from 'next/navigation';

interface PageProps {
  params: {
    path: string[];
  };
}

export default async function Page({ params }: PageProps) {
  const content = await builder
    .get('page', {
      url: '/' + (params?.path?.join('/') || ''),
      options: {
        includeRefs: true,
      },
    })
    .promise();

  if (!content) {
    return notFound();
  }

  return <RenderBuilderContent content={content} />;
}

export async function generateStaticParams() {
  const pages = await builder.getAll('page', {
    fields: 'data.url',
    options: { noTargeting: true },
  });

  return pages.map((page) => ({
    path: page.data?.url?.split('/').filter(Boolean) || [],
  }));
}

Content Renderer

// src/components/builder/render-content.tsx
'use client';

import { BuilderComponent, useIsPreviewing } from '@builder.io/react';
import { builder } from '@/lib/builder/config';

interface RenderBuilderContentProps {
  content: any;
  model?: string;
}

export function RenderBuilderContent({
  content,
  model = 'page',
}: RenderBuilderContentProps) {
  const isPreviewing = useIsPreviewing();

  // If we're previewing in Builder, render the preview content
  if (isPreviewing) {
    return (
      <BuilderComponent
        model={model}
        content={content}
      />
    );
  }

  // In production, render the published content
  return (
    <BuilderComponent
      model={model}
      content={content}
      options={{ includeRefs: true }}
    />
  );
}

Content Management

Content Models

// src/lib/builder/models.ts
import { builder } from '@builder.io/react';

// Define custom content models
builder.defineCustomModel('section', {
  name: 'Section',
  fields: [
    {
      name: 'title',
      type: 'string',
      required: true,
    },
    {
      name: 'layout',
      type: 'enum',
      enum: ['full', 'contained', 'split'],
      defaultValue: 'contained',
    },
    {
      name: 'background',
      type: 'object',
      subFields: [
        {
          name: 'color',
          type: 'color',
        },
        {
          name: 'image',
          type: 'file',
        },
      ],
    },
  ],
});

Content API

// src/lib/builder/api.ts
import { builder } from '@builder.io/react';
import { BUILDER_CONFIG } from './config';

export class BuilderAPI {
  static async getContent(model: string, options: any = {}) {
    return await builder.get(model, {
      ...options,
      apiKey: BUILDER_CONFIG.apiKey,
    }).promise();
  }

  static async getAllContent(model: string, options: any = {}) {
    return await builder.getAll(model, {
      ...options,
      apiKey: BUILDER_CONFIG.apiKey,
    });
  }

  static async createContent(model: string, data: any) {
    return await builder.create(model, {
      data,
      apiKey: BUILDER_CONFIG.apiSecret,
    });
  }

  static async updateContent(model: string, id: string, data: any) {
    return await builder.update({
      model,
      id,
      data,
      apiKey: BUILDER_CONFIG.apiSecret,
    });
  }
}

Preview Mode

Preview Configuration

// src/app/api/preview/route.ts
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { BUILDER_CONFIG } from '@/lib/builder/config';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get('secret');
  const slug = searchParams.get('slug');

  if (secret !== BUILDER_CONFIG.previewSecret) {
    return new Response('Invalid token', { status: 401 });
  }

  cookies().set('builder-preview', 'true', {
    path: '/',
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
  });

  redirect(slug ?? '/');
}

Content Targeting

Personalization

// src/lib/builder/targeting.ts
import { builder } from '@builder.io/react';

// Set up targeting attributes
builder.setUserAttributes({
  // User attributes for targeting
  device: typeof window !== 'undefined' ?
    window.innerWidth > 768 ? 'desktop' : 'mobile' :
    undefined,
  loggedIn: true, // Set based on auth state
  userGroup: 'premium', // Set based on user data
});

// Custom targeting function
export function withTargeting(Component: any) {
  return function TargetedComponent(props: any) {
    const userAttributes = {
      // Get user attributes from your auth/state management
      device: typeof window !== 'undefined' ?
        window.innerWidth > 768 ? 'desktop' : 'mobile' :
        undefined,
      loggedIn: true,
      userGroup: 'premium',
    };

    return (
      <BuilderComponent
        {...props}
        userAttributes={userAttributes}
      />
    );
  };
}

Best Practices

  1. Component Design

    • Keep components focused and reusable
    • Implement proper prop validation
    • Use TypeScript for type safety
  2. Performance

    • Implement proper caching
    • Use static generation where possible
    • Optimize media assets
  3. Security

    • Keep API keys secure
    • Validate preview tokens
    • Implement proper CORS
  4. Content Management

    • Define clear content models
    • Implement content validation
    • Use proper targeting rules

Next Steps