@burdenoff/website-sdk
v2026.1004.1
Published
Shared SDK for Burdenoff product websites - reusable React components, utilities, and configurations
Maintainers
Readme
@burdenoff/website-sdk
Reusable React components and utilities for Burdenoff product websites.
Published automatically using npm trusted publishers (OIDC) - no long-lived tokens needed!
Features
- 🎨 Pre-built Components: Contact forms, newsletter subscriptions, pricing tables
- 🎯 SEO Optimized: Built-in meta tag management with React Helmet
- 🔧 Fully Configurable: All components accept props for complete customization
- 📱 Responsive Design: Mobile-first, works on all screen sizes
- 🎭 Theme Support: Built with Tailwind CSS and semantic tokens
- ✅ Type Safe: Written in TypeScript with full type definitions
- 🚀 Tree Shakeable: ESM and CJS builds for optimal bundle size
Installation
npm install @burdenoff/website-sdkPeer Dependencies
This package requires the following peer dependencies:
npm install react react-dom react-helmet-async react-hook-form @hookform/resolvers zod sonnerFor UI components (if using separately):
npm install @radix-ui/react-label @radix-ui/react-slot class-variance-authority clsx tailwind-mergeQuick Start
Contact Page
import { ContactPage } from '@burdenoff/website-sdk/components/contact'
function Contact() {
return (
<ContactPage
productName="MyProduct"
apiBaseUrl="https://api.myproduct.com"
recaptchaSiteKey="your-recaptcha-site-key"
contactEmail="[email protected]"
contactPhone="+1 234 567 8900"
officeAddress={{
street: '123 Main St',
city: 'San Francisco',
state: 'CA',
zip: '94102',
country: 'United States',
}}
// Optional SEO props
seoTitle="Contact Us"
seoDescription="Get in touch with our team"
/>
)
}Newsletter Page
import { NewsletterPage } from '@burdenoff/website-sdk/components/newsletter'
function Newsletter() {
return (
<NewsletterPage
productName="MyProduct"
apiBaseUrl="https://api.myproduct.com"
defaultEmail="[email protected]"
layout="centered" // or "split"
heroHeading="Stay Updated"
heroSubtitle="Subscribe to our newsletter"
/>
)
}Pricing Page
import { PricingPage } from '@burdenoff/website-sdk/components/pricing'
function Pricing() {
return (
<PricingPage
productName="MyProduct"
productId="myproduct-123"
apiBaseUrl="https://api.myproduct.com"
appLoginUrl="https://app.myproduct.com/login"
contactUrl="/contact"
fallbackFeatures={{
basic: ['Up to 5 dashboards', '10 data sources', 'Email support'],
professional: ['Unlimited dashboards', 'API access', 'Priority support'],
enterprise: ['Everything in Professional', 'Dedicated support', 'SLA guarantee'],
}}
seoTitle="Pricing Plans"
seoDescription="Choose the perfect plan for your needs"
/>
)
}Explore (/explore AI chat)
ExplorePage is the public AI chat surface: a visitor asks a question in plain
English and gets an answer drawn from the product's own published documentation,
with the source pages and next-step CTAs attached.
Import from the package ROOT.
ExplorePage,ExploreCtaanduseExploreChatmust be imported from@burdenoff/website-sdk, never from@burdenoff/website-sdk/components/explore. This package builds with tsupsplitting: false, so every sub-path entry bundles its own copy of the React context — a sub-pathExplorePagecannot see yourWebSDKProviderand throwsuseWebSDK must be used within a WebSDKProvidereven when it is rendered inside one. The sub-path entries exist only for build parity with the other components.
import { ExplorePage, ExploreCta } from '@burdenoff/website-sdk'
import { useNavigate, useLocation } from 'react-router'
function Explore() {
const navigate = useNavigate()
return (
<ExplorePage
productSlug="vibecontrols"
heightMode="viewport"
viewportOffset="4rem" // subtract your site header
onNavigate={(path) => navigate(path)}
seo={{ url: 'https://vibecontrols.com/explore' }}
/>
)
}
// Nav entry points
function Header() {
const navigate = useNavigate()
const { pathname } = useLocation()
return (
<>
<ExploreCta variant="header" onNavigate={(h) => navigate(h)} />
<ExploreCta variant="floating" currentPath={pathname} />
</>
)
}ExplorePage props
| Prop | Default | What it does |
|---|---|---|
| seo | – | Renders PageHead with these overrides. Omit to manage <head> yourself. |
| productSlug | – | The site this page is on. A soft hint to the assistant, not a hard filter. |
| examplePrompts | – | Prompt chips used only when the backend catalog supplies none. |
| className | – | Extra classes on the page root. |
| heightMode | 'viewport' | viewport pins the chat to the screen with an internally scrolling transcript, and is the only mode in which the component manages the host page's scroll position; auto lets the page grow and never touches its host's scrolling (for embedding). |
| viewportOffset | 0 | Chrome to subtract in viewport mode, before the first measurement only. A number is pixels; a string is any CSS length ('4rem'). Once mounted the pane measures its own top and this value stops being used — so an approximate header height is fine, and the on-screen keyboard is handled for you. |
| onNavigate | – | SPA navigation for internal CTA paths. Without it, CTAs render as plain links. |
| onSend | – | Fired with the message text each time a send is accepted (analytics). |
| welcomeTitle / welcomeBody | catalog values | Override the welcome copy. welcomeTitle is the page <h1>. |
ExploreCta props: variant ('header' \| 'mobile' \| 'floating', required),
href (default /explore), label (default Ask AI), className,
onNavigate, onClick, currentPath. The floating variant hides itself on
/explore. Pass currentPath from your router — the fallback
(window.location.pathname + popstate) cannot observe SPA pushState
navigations.
Prerender safety. The <h1> renders on the first render from
welcomeTitle or a built-in fallback catalog — never from the exploreCatalog
fetch. The website prerender waits for an h1, so a heading behind a network
round-trip would bake blank static pages. For the same reason a failed catalog
fetch degrades to the built-in catalog rather than an empty page.
reCAPTCHA v3 is backend-driven. Nothing to wire on the website: the SDK loads
the v3 script and mints a token only when exploreCatalog.captchaRequired &&
recaptchaSiteKey come back set. Turning captcha on is a backend secret change
with no website redeploy, and the "protected by reCAPTCHA" notice appears
with it. This is separate from the v2 checkbox used by ContactPage.
Headless use. useExploreChat(options) exposes the whole state machine —
{ phase, catalog, messages, activity, remainingToday, error, focusProduct,
setFocusProduct, send, stop, retry, reset, canSend } — if you want to build a
different shell around it.
Inline concept illustrations. An answer may embed images from the product
sites' concept pages (, followed by the backend's own caption
line). They render only once the answer is COMPLETED — never from a streaming
preview — and only when the URL is https on a catalog product's website
(apex or www.); anything else renders nothing. Each image is lazy-loaded,
links to the full file in a new tab (unless the markdown already wraps it in a
link, which then stays the only link), carries a "Concept illustration" badge,
and disappears entirely if it fails to load. Nothing to wire on the website.
Testing hooks. Stable data-boff-explore attributes are attached for the
end-to-end suite: page, messages, composer, chip, thinking, user,
assistant (plus data-status), reference, cta (plus data-kind),
cta-header, cta-mobile, cta-floating, send, stop, new-chat, error,
inline-image.
Concepts (/concepts + /concept/:slug)
ConceptsPage and ConceptDetailPage render a product's concepts: each one a
feature or flow, shown through the mockups drawn for it and explained in
reviewed copy. Every image is labelled an illustration of the concept, not a
screenshot of the actual product (CONCEPT_ILLUSTRATION_NOTICE).
The data is a generated src/data/concepts.ts in each site — never hand-edit
it; regenerate with
python3 ~/products/dev/scripts/concepts/build_site_concepts.py site --product <p>.
Both pages take the data as props and make no requests, so they prerender
completely. Import them from the package root, like every other page.
import { Link, Navigate, Route, useParams } from 'react-router'
import {
ConceptDetailPage,
ConceptsPage,
CONCEPTS_PATH,
getConceptBySlug,
type ConceptLinkProps,
} from '@burdenoff/website-sdk'
import { CONCEPT_CATEGORIES, CONCEPTS } from '@/data/concepts'
const SITE_URL = 'https://vibecontrols.com'
// Router adapter, so concept → concept navigation stays client-side.
// Without `linkComponent` the pages render plain <a href> links.
function ConceptLink({ href, className, children, 'aria-label': ariaLabel }: ConceptLinkProps) {
return (
<Link to={href} className={className} aria-label={ariaLabel}>
{children}
</Link>
)
}
// The site resolves the slug; an unknown one redirects to the landing page.
function ConceptRoute() {
const { slug } = useParams()
const concept = getConceptBySlug(CONCEPTS, slug)
if (!concept) return <Navigate to={CONCEPTS_PATH} replace />
return (
<ConceptDetailPage
productName="VibeControls"
siteUrl={SITE_URL}
concept={concept}
concepts={CONCEPTS}
categories={CONCEPT_CATEGORIES}
linkComponent={ConceptLink}
/>
)
}
// Routes
<Route
path={CONCEPTS_PATH}
element={
<ConceptsPage
productName="VibeControls"
siteUrl={SITE_URL}
concepts={CONCEPTS}
categories={CONCEPT_CATEGORIES}
linkComponent={ConceptLink}
/>
}
/>
<Route path="/concept/:slug" element={<ConceptRoute />} />ConceptsPage props: productName, siteUrl (public origin, for
canonical/OG/JSON-LD URLs), concepts (required); categories (section order
and descriptions — a concept in an unlisted category still renders, last),
cta (rendered in the hero), linkComponent, title (the <h1>, default
"<Product> concepts"), description (meta description and hero lead; the
default is built from the counts and stays within 160 characters), className.
With no concepts at all the page says none are published yet, without the
filter bar or zero counts.
ConceptDetailPage props: productName, siteUrl, concept, concepts
(all of them — for related concepts and previous / next by order);
categories, cta (rendered as a band at the end), linkComponent,
className.
Helpers: CONCEPTS_PATH (/concepts), CONCEPT_PATH_PREFIX (/concept),
conceptPath(slug), getConceptBySlug(concepts, slug); types Concept,
ConceptImage, ConceptCategory, ConceptLinkComponent, ConceptLinkProps.
SEO and llms. One <h1> per page, a self-canonical URL, absolute OG images
and JSON-LD (CollectionPage + ItemList + BreadcrumbList on the landing
page; ImageGallery + BreadcrumbList on a concept). Add /concepts and every
/concept/<slug> to the site's prerender list and sitemap. Only the gallery
images carry data-llms-image (one per image — the hero does not), and each is
followed directly by its <figcaption>, which scripts/llms.mjs reads as the
caption. The lightbox is a native <dialog> mounted only while open, so it is
never in the prerendered HTML.
Testing hooks. data-concepts="landing" | "card" (plus data-slug) | "filter"
| "notice" | "empty" (filters match nothing) | "none" (no concepts at all) |
"detail" | "category" | "hero" | "illustration" | "related" | "pager" | "cta" |
"lightbox", and data-concepts-notice on the notice.
SEO Component
import { PageHead } from '@burdenoff/website-sdk/seo'
function AboutPage() {
return (
<>
<PageHead
title="About Us"
description="Learn about our mission and team"
productName="MyProduct"
keywords="about, team, mission"
image="/og-about.png"
url="https://myproduct.com/about"
/>
<div>{/* Page content */}</div>
</>
)
}UI Components
import { Button, Input, Label, Textarea } from '@burdenoff/website-sdk/ui'
function Form() {
return (
<form>
<div>
<Label htmlFor="name">Name</Label>
<Input id="name" type="text" placeholder="Your name" />
</div>
<div>
<Label htmlFor="message">Message</Label>
<Textarea id="message" placeholder="Your message" />
</div>
<Button type="submit">Send</Button>
</form>
)
}Utilities
import { cn } from '@burdenoff/website-sdk/utils'
// Merge Tailwind CSS classes
const className = cn('px-4 py-2', isActive && 'bg-blue-500', 'rounded-lg')API Integration
All components that require API integration expect the following endpoints:
Contact Form: POST /api/contact
Request:
{
"name": "John Doe",
"email": "[email protected]",
"phone": "+1 234 567 8900",
"subject": "Inquiry",
"message": "Hello, I have a question...",
"recaptchaToken": "token-here"
}Response:
{
"success": true,
"message": "Message sent successfully"
}Newsletter: POST /api/newsletter/subscribe
Request:
{
"email": "[email protected]"
}Response:
{
"success": true,
"message": "Successfully subscribed!"
}Pricing: GET /api/plans
Headers:
x-product-id: your-product-idResponse:
{
"success": true,
"data": [
{
"id": "plan-1",
"name": "Basic",
"description": "Perfect for individuals",
"price": 9.99,
"currency": "USD",
"duration": "monthly",
"features": [...],
"isActive": true
}
]
}TypeScript
All components are fully typed. Import types as needed:
import type { ContactPageProps } from '@burdenoff/website-sdk/components/contact'
import type { NewsletterPageProps } from '@burdenoff/website-sdk/components/newsletter'
import type { PricingProps, PlanCard } from '@burdenoff/website-sdk/components/pricing'
import type { PageHeadProps } from '@burdenoff/website-sdk/seo'
import type { ButtonProps } from '@burdenoff/website-sdk/ui'Styling
This SDK uses Tailwind CSS for styling. Ensure your project has Tailwind configured with the following in your tailwind.config.js:
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@burdenoff/website-sdk/**/*.{js,mjs}',
],
// ... rest of your config
}Required CSS Variables
Add these CSS variables to your global stylesheet for proper theming:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}Component Documentation
ContactPage
Full-featured contact form with reCAPTCHA v3 integration.
Props:
productName(required): Product name for brandingapiBaseUrl(required): Base URL for API callsrecaptchaSiteKey(required): Google reCAPTCHA v3 site keycontactEmail(required): Contact email addresscontactPhone(required): Contact phone numberofficeAddress(required): Office address objectseoTitle,seoDescription, etc.: SEO metadata (optional)
NewsletterPage
Newsletter subscription component with rate limiting.
Props:
productName(required): Product nameapiBaseUrl(required): API base URLdefaultEmail(required): Default contact emaillayout: 'centered' or 'split' (default: 'centered')rateLimitAttempts: Number of attempts before rate limiting (default: 3)rateLimitWindowMinutes: Rate limit window in minutes (default: 15)
PricingPage / PricingSection
Dynamic pricing tables with API integration and billing cycle toggle.
Props:
productName(required): Product nameproductId(required): Product ID for API requestsapiBaseUrl(required): API base URLappLoginUrl(required): App login URL for signupcontactUrl: Contact page URL (default: '/contact')fallbackFeatures: Fallback features when API doesn't provide them
License
Proprietary - Copyright Burdenoff Consultancy Services Pvt. Ltd. 2025
Support
For support, email [email protected] or visit https://burdenoff.com
