npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

  • createSablePublicClient
  • SableSiteProvider
  • useSableSiteProfile
  • useSableSiteClient
  • useSableSiteConfig
  • useSableLocale
  • useTranslation
  • BookingWidgetPanel
  • BookingWidgetPlaceholder
  • getResolvedSiteProfile
  • createTranslator, 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: