@bernierllc/website-builder-suite
v0.1.1
Published
Ready-to-use website builder bundle: config-driven site profiles, tenant resolution, preview UI, and onboarding plugin.
Readme
@bernierllc/website-builder-suite
Ready-to-use bundle wiring the Website Builder cluster: config-driven site profiles, tenant resolution, live preview UI, and one-line onboarding agent integration.
Overview
website-builder-suite provides a single import for the complete Website Builder cluster:
createWebsiteBuilder(config)— factory wiringSiteBuilderwith one call- UI components from
@bernierllc/site-preview-ui(re-exported) WebsiteBuilderPluginfrom@bernierllc/onboarding-plugin-website-builder(re-exported)- Schema helpers and types from
@bernierllc/site-config-core(re-exported) InMemorySiteConfigStorefrom@bernierllc/site-builder-service(re-exported)
Installation
npm install @bernierllc/website-builder-suite
# React peer deps (required for UI components)
npm install react react-domUsage
Standalone Admin Setup
// server/site-builder.ts
import { createWebsiteBuilder, InMemorySiteConfigStore } from '@bernierllc/website-builder-suite';
export const builder = createWebsiteBuilder({
store: new InMemorySiteConfigStore(), // Replace with PrismaSiteConfigStore for production
neverhub: { enabled: process.env.NEVERHUB_ENABLED === 'true' },
});
// Call initialize() once at server startup
await builder.initialize();// app/api/sites/[tenantId]/route.ts (Next.js 15)
import { builder } from '@/server/site-builder';
export async function GET(req: Request, { params }: { params: { tenantId: string } }) {
const model = await builder.loadByTenant(params.tenantId);
if (!model) return Response.json({ error: 'Not found' }, { status: 404 });
return Response.json(model);
}
export async function PATCH(req: Request, { params }: { params: { tenantId: string } }) {
const patch = await req.json();
const saved = await builder.save({ tenantId: params.tenantId, ...patch });
return Response.json(saved);
}// app/admin/site/page.tsx (admin editing UI)
'use client';
import { SitePreviewCard, useSitePreview } from '@bernierllc/website-builder-suite';
export default function SiteAdminPage() {
const { renderModel, refresh } = useSitePreview({
fetchFn: () => fetch('/api/sites/my-tenant').then(r => r.json()),
});
return (
<div>
<SitePreviewCard renderModel={renderModel} onRefresh={refresh} />
{/* Admin editing forms here */}
</div>
);
}Onboarding Agent Integration
// server/onboarding-setup.ts
import { createWebsiteBuilder, WebsiteBuilderPlugin } from '@bernierllc/website-builder-suite';
import { OnboardingAgentSuite } from '@bernierllc/onboarding-agent-suite';
const builder = createWebsiteBuilder({ store: myPrismaStore });
await builder.initialize();
const agent = new OnboardingAgentSuite({
plugins: [
new WebsiteBuilderPlugin({
siteBuilder: builder,
tenantId: session.tenantId,
slug: 'acme-plumbing',
previewSlugPattern: '/booking-preview/{slug}',
offeredSections: ['services', 'serviceArea', 'hours', 'contact'],
}),
],
});Next.js Middleware Recipe (Tenant Resolution)
// middleware.ts — subdomain-based tenant resolution
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const host = request.headers.get('host') ?? '';
const slug = host.split('.')[0];
const response = NextResponse.rewrite(new URL('/booking-site', request.url));
response.headers.set('x-org-slug', slug);
return response;
}// app/booking-site/page.tsx
import { builder } from '@/server/site-builder';
import { headers } from 'next/headers';
export default async function BookingSitePage() {
const slug = (await headers()).get('x-org-slug') ?? '';
const model = await builder.loadBySlug(slug);
if (!model) return <div>Site not found</div>;
return (
<main style={model.cssVariables as React.CSSProperties}>
<h1>{model.config.branding.name}</h1>
</main>
);
}Schema Helpers
import {
validate,
defaultConfig,
completenessScore,
toCssVariables,
mergeConfigs,
} from '@bernierllc/website-builder-suite';
// Create a default config
const config = defaultConfig('tenant-1', 'acme-plumbing');
// Validate a config
const result = validate(config);
if (!result.success) console.error(result.error);
// Check completeness
const { score, missingFields } = completenessScore(config);
console.log(`${score}% complete, missing: ${missingFields.join(', ')}`);
// Generate CSS variables
const cssVars = toCssVariables(config);
// { '--color-primary': '#1a1a2e', '--font-heading': 'Inter', ... }
// Merge a partial patch
const updated = mergeConfigs(config, { branding: { tagline: 'Best in town' } });API Reference
createWebsiteBuilder(config)
Creates a wired SiteBuilder instance.
function createWebsiteBuilder(config: WebsiteBuilderSuiteConfig): SiteBuilderWebsiteBuilderSuiteConfig:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| store | SiteConfigStore | ✅ | Persistence adapter (InMemory or Prisma) |
| dataReaders | DataReaderMap | ❌ | Live section data readers |
| storage | StorageAdapter | ❌ | Logo upload adapter |
| tenantResolver | TenantResolverStrategy | ❌ | Custom slug → tenantId strategy |
| neverhub | { enabled?: boolean } | ❌ | NeverHub integration |
WebsiteBuilderSuiteError
Error class for suite-level failures.
class WebsiteBuilderSuiteError extends Error {
readonly code: string; // Error code (e.g., 'MISSING_STORE')
readonly context?: Record<string, unknown>;
// cause is ES2022 Error.cause
}Re-exports
All exports from sub-packages are available from a single import:
import {
// Factory
createWebsiteBuilder, WebsiteBuilderSuiteError,
// Store
InMemorySiteConfigStore,
// Config helpers
validate, defaultConfig, completenessScore, toCssVariables, mergeConfigs, diffConfigs,
// UI components
SitePreviewCard, SiteFramePreview, ThemeSwatches, CompletenessBar, useSitePreview,
// Plugin
WebsiteBuilderPlugin,
} from '@bernierllc/website-builder-suite';MECE Boundary: content-management-suite
This package manages tenant business profiles: theme, branding, section layout, and publish lifecycle.
@bernierllc/content-management-suite manages editorial content: CMS articles, blog posts, editorial workflows, and Contentful/Prisma CMS adapters.
These are distinct data domains. A production application may use both simultaneously — CMS manages blog posts, website builder manages the business profile that frames them.
NeverHub Integration
SiteBuilder registers site-management capabilities with NeverHub on initialize() when enabled. Events emitted: site.config.updated, site.published. All operations degrade gracefully when NeverHub is absent or disabled.
License
Copyright (c) 2025 Bernier LLC. Licensed for use within the delivered project only.
