@bernierllc/site-config-core
v0.3.0
Published
Typed SiteConfig schema, Zod validation, and pure helpers for config-driven website profiles.
Readme
@bernierllc/site-config-core
Typed SiteConfig schema, Zod validation, and pure helpers for config-driven website profiles.
Overview
The canonical data layer for website builder profiles. No I/O, no rendering — pure data: validate, transform, inspect.
| Export | Description |
|--------|-------------|
| SiteConfigSchema | Root Zod schema for a complete site configuration |
| SiteSectionSchema | Discriminated union of all section types |
| HexColorSchema | #rrggbb validated branded hex color |
| validate(raw) | Returns a Zod SafeParseReturnType — never throws |
| defaultConfig(tenantId, slug) | Factory returning a draft-status default config |
| completenessScore(config) | 0–100 score across 6 named checkpoints |
| completenessCheckpoints(config) | Raw checkpoint array for custom progress UI |
| toCssVariables(theme) | Emits 5 CSS custom property key-value pairs |
| mergeConfigs(base, patch) | Shallow merge of branding/theme, validated by schema |
| diffConfigs(a, b) | Structured diff of changed fields and section changes |
| SiteConfigError | Base error class with code and context |
| SiteConfigValidationError | Thrown on schema validation failure (code: VALIDATION_ERROR) |
Installation
npm install @bernierllc/site-config-coreUsage
Creating a default draft config
import { defaultConfig, validate } from '@bernierllc/site-config-core';
const config = defaultConfig('tenant-abc', 'acme-plumbing');
// config.status === 'draft'
// config.theme.heroStyle === 'minimal'
// config.branding.name === '' (draft — fill in before marking ready)
const result = validate(config);
if (result.success) {
console.log(result.data.tenantId); // 'tenant-abc'
} else {
console.error(result.error.issues);
}Validating an unknown payload
import { validate } from '@bernierllc/site-config-core';
const result = validate(rawPayloadFromRequest);
if (!result.success) {
// result.error.issues contains every Zod validation issue
throw new Error(result.error.message);
}
// result.data is fully typed as SiteConfigCompleteness scoring
import { completenessScore, mergeConfigs, defaultConfig } from '@bernierllc/site-config-core';
const config = mergeConfigs(defaultConfig('t', 'acme'), {
branding: { name: 'Acme Plumbing', tagline: 'We fix things fast' },
sections: [
{ type: 'services', items: [{ id: 's1', name: 'Drain Cleaning' }] },
{ type: 'contact', email: '[email protected]' },
],
});
const { score, checkpoints, missingFields } = completenessScore(config);
// score: 50 (3 of 6 checkpoints pass)
// missingFields: ['logo', 'theme' is always true, 'status']
checkpoints.forEach(cp => {
console.log(`${cp.label}: ${cp.passed ? 'done' : cp.detail ?? 'incomplete'}`);
});Emitting CSS variables from a theme
import { toCssVariables, defaultConfig } from '@bernierllc/site-config-core';
const { theme } = defaultConfig('t', 's');
const vars = toCssVariables(theme);
// {
// '--color-primary': '#1a1a1a',
// '--color-secondary': '#ffffff',
// '--color-accent': '#3b82f6',
// '--font-heading': 'Inter',
// '--font-body': 'Inter',
// }
// Apply in a browser context:
Object.entries(vars).forEach(([prop, val]) => {
document.documentElement.style.setProperty(prop, val);
});Merging a partial patch
import { mergeConfigs, defaultConfig } from '@bernierllc/site-config-core';
const base = defaultConfig('tenant-1', 'my-site');
// Only override what changed — unchanged fields survive
const updated = mergeConfigs(base, {
branding: { name: 'My Site' },
status: 'ready',
});
// updated.branding.name === 'My Site'
// updated.theme.fontHeading === 'Inter' ← preservedDiffing two configs
import { diffConfigs, mergeConfigs, defaultConfig } from '@bernierllc/site-config-core';
const a = defaultConfig('t', 's');
const b = mergeConfigs(a, {
status: 'published',
sections: [{ type: 'hero', headline: 'Welcome' }],
});
const diff = diffConfigs(a, b);
// diff.changedFields: [{ path: 'status', from: 'draft', to: 'published' }, ...]
// diff.addedSections: ['hero']
// diff.removedSections: []Error handling
import {
SiteConfigError,
SiteConfigValidationError,
} from '@bernierllc/site-config-core';
try {
doSomethingWithConfig(config);
} catch (error) {
if (error instanceof SiteConfigValidationError) {
// code === 'VALIDATION_ERROR'
console.error(error.code, error.context);
} else if (error instanceof SiteConfigError) {
console.error(error.code, error.message);
if (error.cause) {
console.error('caused by:', error.cause);
}
}
}SiteSection types
The sections array accepts any of these discriminated types:
| type | Required fields | Notable optional fields |
|--------|-----------------|------------------------|
| hero | — | headline, subheadline, ctaLabel, ctaHref |
| services | — | title, items[] (id, name, description, category, price) |
| team | — | title, members[] (id, name, role, bio, avatarUrl) |
| serviceArea | — | title, cities[], mapEmbedUrl |
| hours | — | title, schedule (Record<string,string>) |
| contact | — | email, phone, address, showForm |
| custom | id, title, content | — |
Multiple sections of the same type are allowed (e.g., two custom sections).
Completeness checkpoints
completenessScore evaluates exactly 6 named checkpoints in order:
| name | label | Passes when |
|------|-------|-------------|
| branding | Business identity | branding.name and branding.tagline are both non-empty |
| logo | Logo uploaded | branding.logoUrl is set |
| theme | Colors & fonts | primaryColor and secondaryColor are present |
| services | Services listed | A services section exists with at least one item |
| contact | Contact info | A contact section has email or phone |
| status | Site marked ready | status is ready or published |
MECE Boundary
Includes: schema, types, validation, pure helpers (factory, completeness, CSS vars, merge, diff).
Excludes: persistence, rendering, HTTP, React components. Those live in site-builder-service and site-preview-ui.
License
Copyright (c) 2025 Bernier LLC. See LICENSE for details.
