@asksable/site-connector
v0.6.21
Published
Thin first-party package for connecting separate website repos to Sable through `siteSlug`.
Readme
@asksable/site-connector
Thin first-party package for connecting separate website repos to Sable through siteSlug.
Purpose
Use this package in public website repos that should:
- read business/site profile data from Sable
- submit contact forms into Sable
- render the shared booking widget against Sable public booking APIs
- send cookieless first-party web analytics into Sable
Setup
Wrap the site in SableSiteProvider:
import { SableSiteProvider } from '@asksable/site-connector'
;<SableSiteProvider
config={{
apiUrl: import.meta.env.VITE_SABLE_PUBLIC_API_URL,
siteSlug: import.meta.env.VITE_SABLE_SITE_SLUG,
timezone: 'America/Chicago',
}}
>
<App />
</SableSiteProvider>timezone is optional, but recommended for mobile/local-service sites. The
workspace timezone returned by Sable wins when configured; the host value is the
fallback before the widget considers browser or staff defaults.
Website analytics
SableSiteProvider sends a first-party pageview beacon to Sable on every route
change. The customer-facing Website tab reads from Sable's Convex rollups, so it
does not require PostHog query credentials.
The public site profile provides the analytics ingestion URL centrally. In
production this routes pageviews through Sable's Cloudflare Worker first, then
forwards the request to Convex with edge country metadata attached. apiUrl
can continue to point at the Convex public API used for site profiles, contact
forms, and booking.
Website analytics is cookieless by default. The first-party pageview beacon does
not write an analytics visitor ID to cookies, localStorage, or
sessionStorage; Sable derives a daily visitor key server-side from request
metadata, the site slug, the date, and a salt. The request handler uses IP,
user-agent, and accept-language only transiently for hashing and country
derivation, then discards them. Stored analytics rows contain aggregate counts
and short-lived anonymous session state, not raw IP addresses, full user-agents,
or persistent visitor IDs.
If a standalone host needs to override the server-provided config, pass:
<SableSiteProvider
config={{
apiUrl,
siteSlug,
analytics: {
captureEnabled: true,
apiUrl: 'https://secure.asksable.com',
environment: import.meta.env.MODE,
},
}}
>
<App />
</SableSiteProvider>The connector sends each pageview with a generated event id. Sable stores the event in a short-lived first-party ledger, dedupes retries, and processes it into Convex rollups for visitors, pageviews, sources, pages, devices, countries, bounce rate, and average visit duration.
Then consume the profile or render the shared booking widget:
import {
BookingWidgetPanel,
useSableSiteProfile,
} from '@asksable/site-connector'Preselecting From A Host Estimator
When the host page already knows what the customer is booking, pass an
initialSelection. The widget resolves serviceSlug after booking setup loads,
shows the selected service card, keeps the Change affordance available, and
sends intakeResponses with the final booking.
<BookingWidgetPanel
initialSelection={{
serviceSlug: 'interior-detailing',
customerNotes: 'Estimate shown: $199',
quotedTotalCents: 19900,
quotedTotalLabel: '$199',
intakeResponses: {
vehicle_size: 'large',
pet_hair: 'minimal',
estimate_cents: 19900,
},
}}
/>Flexible Arrival Windows
For mobile services where the owner optimizes the route after customer intake, enable flexible scheduling. Exact slots remain available, but the customer can request a date plus an arrival window without taking a hard hold.
<BookingWidgetPanel
allowFlexibleScheduling
defaultSchedulingPreference="flexible"
/>Exports
createSablePublicClientSableSiteProvideruseSableSiteProfileuseSableSiteClientuseSableSiteConfiguseSableLocaleuseTranslationBookingWidgetPanelBookingWidgetPlaceholdergetResolvedSiteProfilecreateTranslator,pickLocaleField,localeToIntl,TRANSLATIONS,DEFAULT_LOCALE- types:
SableSiteConfig,BookingInitialSelection,Locale,TranslationKey,TranslationOverrides, plus public site / booking payloads
Layout-stable loading
The booking panel reserves its own height while it loads setup data. If a host site lazy-loads the widget bundle or route, render the lightweight placeholder as the Suspense fallback so the footer does not jump before the widget code arrives:
import { Suspense, lazy } from 'react'
import { BookingWidgetPlaceholder } from '@asksable/site-connector/booking-widget-placeholder'
const BookingWidgetPanel = lazy(() =>
import('@asksable/site-connector').then((module) => ({
default: module.BookingWidgetPanel,
})),
)
<Suspense fallback={<BookingWidgetPlaceholder />}>
<BookingWidgetPanel />
</Suspense>Multi-language support
The booking widget translates its own UI chrome (form labels, buttons, summaries, error messages, date/time formatting) to match the host site's language. Every Sable customer website MUST declare its current language to the widget so the customer never sees mismatched copy (e.g. an English "Confirm Booking" button on a Spanish-language site).
Supported locales: 'en' (default), 'es'. Adding a locale requires a package version bump.
Three usage modes
| Site type | Pattern |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Single-language site (English only) | Omit language entirely or pass 'en'. Widget defaults to English. |
| Single-language site (Spanish only) | Pass language: 'es' once at provider mount. |
| Multi-language site with a toggle | Pass a reactive value that updates when the toggle changes. The provider re-renders, the widget re-renders with the new locale. |
Multi-language example
import { SableSiteProvider } from '@asksable/site-connector'
import { useLanguage } from './your-i18n-context'
function App() {
const { lang } = useLanguage() // your toggle owns this state
return (
<SableSiteProvider
config={{
apiUrl: import.meta.env.VITE_SABLE_PUBLIC_API_URL,
siteSlug: import.meta.env.VITE_SABLE_SITE_SLUG,
language: lang,
}}
>
<Routes />
</SableSiteProvider>
)
}The widget responds instantly to language changes. Your toggle component flips both the host site's text and the widget by sharing the same language state.
Override individual strings (rare)
When a specific client needs different brand voice (e.g. "Reserva mi entrega" instead of the canonical "Confirmar reserva"), pass translationOverrides:
<SableSiteProvider
config={{
apiUrl,
siteSlug,
language: lang,
translationOverrides: {
es: { btnConfirmBooking: 'Reserva mi entrega' },
},
}}
>
<App />
</SableSiteProvider>Use overrides sparingly. If a change would benefit all customers, extend the canonical dictionary in translations.ts and bump the package version instead.
What the widget translates
Form labels, button text, mobile step labels, helper text, success/cancelled state copy, error messages, ARIA labels, date/time formatting (via Intl.DateTimeFormat(locale)), and currency formatting.
What the widget does NOT translate
- Service names, descriptions, category names: these come from the Sable workspace. The widget reads
nameEn/nameEs(or any${field}En/${field}Es) fields when available, falling back to the base field. If the workspace only entered one locale, that text renders regardless of UI language. (Future workstream: dashboard support for entering both locales.) - Customer-typed input: names, notes, etc.
Detecting locale in custom components
If you build something inside SableSiteProvider that needs locale awareness, use the exposed hooks:
import { useTranslation, useSableLocale } from '@asksable/site-connector'
function MyComponent() {
const { t, locale } = useTranslation()
// t('contactFullName') → "Nombre completo" when locale is 'es'
}For template builders
Every Sable website template should include the language prop wiring as part of the boilerplate. If the template supports a toggle, the toggle component must flip both the host site's text and the widget by sharing the same language state. Never let the widget and host site drift to different locales. Pass a single reactive language value into SableSiteConfig and the widget stays in sync automatically.
Public API Contract
The package expects the public connector endpoints documented in:
