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

@c1m-public/dash-site-kit

v0.17.0

Published

Shared transport, domain and UI layer for MML dash-scoped public sites

Readme

@c1m-public/dash-site-kit

The shared transport, domain and presentation layer for MML dash-scoped public sites — the marketing site a dash publishes at its own domain, backed by a dash-scoped SaaSDash Channel token.

It exists because that layer was being copy-pasted. Across the maintained dash-site base and its commissioned sites, roughly 49 of 69 shared-path source files were byte-identical, and the copies had begun to drift: one site missed a fix and served the first visitor's currency to everyone for 60 seconds. This package is that identical set, published once.

Not for workspace sites. A workspace site's surface (cart, catalog, giving, session, subscriptions) shares almost nothing with this one — measured at 3 of 16 files. Do not widen this package to cover it.

Entry points

The three subpaths are the layer boundary, made structural:

design (app/, components/)  →  logic  →  access
      /ui                        .        /next

| Import | Layer | Contents | | --- | --- | --- | | @c1m-public/dash-site-kit | logic | domain models, ports, validation, URL/image policy, formatMoney, createSiteConfig | | @c1m-public/dash-site-kit/next | access | channel client, providers, createDashDataLayer | | @c1m-public/dash-site-kit/ui | design | structural primitives |

The root is framework-neutral — no server-only, no Next, no React — so it is safe to import from anywhere, including a client component. /next is server-only.

Forbid /next in your design layer. A page that can call the door directly makes the backend un-swappable and starts leaking the channel token into things that render:

// eslint.config.mjs
{
  files: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
  rules: {
    "no-restricted-imports": ["error", {
      paths: [{
        name: "@c1m-public/dash-site-kit/next",
        message: "The design layer must not talk to the API. Use the data layer you composed in lib/.",
      }],
    }],
  },
}

Composing a site

Two calls, in one lib/ module the rest of the site imports:

// lib/site.ts
import "server-only";
import { createSiteConfig } from "@c1m-public/dash-site-kit";
import { createDashDataLayer } from "@c1m-public/dash-site-kit/next";
import siteDefaults from "@/site.defaults.json";
import { isLocalPreviewMode, localPreviewBootstrap, localPreviewProfile } from "@/lib/preview";

export const data = createDashDataLayer({
  leadSource: siteDefaults.leadSource ?? siteDefaults.name,
  preview: {
    enabled: isLocalPreviewMode,
    bootstrap: localPreviewBootstrap,
    profile: localPreviewProfile,
  },
});

export const site = createSiteConfig(siteDefaults, {
  loadBootstrap: data.loadDashBootstrap,
});

data exposes loadDashBootstrap, loadDashProfile, listDirectoryRegions, getDirectoryRegion, listDirectory, getDirectoryProfile, loadDashPlans, submitLead, requestPortalLink and viewPortal. Every read is wrapped in React cache, so the shell and the page share one request-scoped fetch.

Why these two arguments and no others

They are the only things that legitimately differ between two dash sites:

  • leadSource is recorded on every capture so a dash's CRM can tell its sites apart. It is required, not defaulted — a wrong source silently mis-attributes real leads, which is worse than a startup error.
  • preview is the credential-free local preview. This is the seam that used to force every site to hand-write its own lib/{bootstrap,directory,profile}/index.ts, and those files were the single largest source of drift between sites that were otherwise identical.

Everything else — the transport, the normalisation, the validation, the cache policy — is the same everywhere by definition, which is why it is in here.

Configuration

The access layer reads the channel token from the environment. SAASDASH_* is preferred; C1M_* is accepted for older sites:

| Variable | Required | Meaning | | --- | --- | --- | | SAASDASH_CHANNEL_TOKEN / C1M_CHANNEL_TOKEN | yes | dash-scoped channel key, sk_chan_… | | SAASDASH_API_URL / C1M_API_URL | no | override only for local or self-hosted |

The token is server-only. A NEXT_PUBLIC_* form of either variable is rejected at startup rather than ignored.

Error reporting

The kit owns the shared channel-site Sentry options and validation, but the site owns the SDK import. Keep the static import in the special Next.js file so Turbopack can include @sentry/nextjs in both server and edge outputs:

// instrumentation.ts, or src/instrumentation.ts when the app uses src/app
import * as Sentry from "@sentry/nextjs";
import {
  createChannelSiteRequestErrorHandler,
  initializeChannelSiteSentry,
} from "@c1m-public/dash-site-kit/next";

export function register() {
  initializeChannelSiteSentry(Sentry.init);
}

export const onRequestError = createChannelSiteRequestErrorHandler(
  Sentry.captureRequestError,
);

The consuming site must install @sentry/nextjs. The kit deliberately does not list or dynamically discover it: an indirect runtime import can be omitted from a Vercel function bundle and turn a packaging error into silent telemetry loss.

| Variable | Required | Meaning | | --- | --- | --- | | CHANNEL_SITE_SENTRY_DSN | no | Shared channel-sites Sentry DSN; absence disables reporting | | CHANNEL_SITE_NAME | no | Backwards-compatible site-tag override; otherwise the Vercel production hostname is used | | CHANNEL_SITE_CHANNEL_ID | no | Immutable channel identifier added as the channel_id tag | | SENTRY_ENVIRONMENT | no | Explicit Sentry environment; otherwise VERCEL_ENV is used |

Browser capture uses the framework-neutral builder from the package root. The consumer passes literal NEXT_PUBLIC_* values so Next.js can inline them:

// instrumentation-client.ts
import * as Sentry from "@sentry/nextjs";
import { resolveChannelSiteSentryOptions } from "@c1m-public/dash-site-kit";

const options = resolveChannelSiteSentryOptions({
  dsn: process.env.CHANNEL_SITE_BROWSER_SENTRY_DSN,
  environment: process.env.NEXT_PUBLIC_VERCEL_ENV,
  release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
  site: globalThis.location.hostname,
});
if (options) Sentry.init(options);

export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;

The browser DSN is public by design. Source-map upload credentials remain build-only and must never use NEXT_PUBLIC_*.

A configured but malformed DSN or missing site identity fails fast. On the server the kit uses CHANNEL_SITE_NAME when present, then derives site from VERCEL_PROJECT_PRODUCTION_URL. Browser composition roots use location.hostname. Every event also receives site_kind=dash-site, so one shared Sentry project and one Grafana dashboard can separate future site families without another per-deployment variable. This prevents a deployment from claiming error reporting while sending no attributable events. The observability.grafana_dashboard event context links directly back to the same site selection in the shared Grafana dashboard; it is derived from site and does not need configuration. Use CHANNEL_SITE_CHANNEL_ID only when one deployment always represents one channel. If a process can serve multiple channels, attach channel_id inside a request-local Sentry scope after resolving the request instead of setting a global initialization tag.

Cache policy

Two clients, deliberately:

  • A shared, revalidating client for reads that are identical for every visitor — profile, directory, regions.
  • A no-store client for anything whose answer depends on who is asking — geo detection (which picks the pricing region and therefore the displayed currency) and lead capture.

The npm channel client forces no-store only for mutations and customer-session reads, so a signed-visitor GET on the shared client would be answered from the first visitor's cache entry for everyone that follows. That is a real bug this package exists to stop repeating, and it is covered by tests that fail if the two clients are merged.

Styling contract

/ui ships structural primitives, not a design system. They render Tailwind utility classes that resolve against CSS custom properties the site owns:

--ink, --muted, --subtle, --border, --accent, --accent-foreground, --accent-text

A site rebrands by redefining those tokens in its own globals.css, not by forking the components. If a primitive needs a variant that cannot be expressed through the tokens, add the variant here rather than copying the file back into a site — that copy is how the drift started.

Tailwind v4 does not scan node_modules, so every site must add this line after @import "tailwindcss"; in its globals.css. Without it, kit components render only the classes the site happens to use elsewhere:

@source "../node_modules/@c1m-public/dash-site-kit/dist";

Landing pages

DashLandingPage renders a typed LandingContent around any call to action: the hero, then optional how it works, benefits, audiences, FAQ and closing sections. DashWaitlistLanding puts the waitlist form in it. Sites own the copy; the kit owns the structure. Use the generic DashWaitlistPage when a Dash has no launch copy yet.

Site modes

The Dash's mode arrives in dash.init's signals as the server-decided gate, and createDashSiteProxy enforces it before anything renders, through the same siteDecision workspace sites use:

  • Waitlist (page placement): every non-exempt path gets a 307 to /waitlist. Signups are stored as CRM leads.
  • Maintenance: every non-exempt path is served /maintenance in place with 503, Retry-After (when the owner announced an end) and noindex. Add an app/maintenance/page.tsx that renders DashMaintenancePage.

Legal pages, framework routes and crawler files are always exempt (GATE_EXEMPT_PATHS); pass exempt for site-specific paths. A mode this kit version does not know is ignored, so the server can add modes without breaking older sites.

Legal pages

Legal pages stay reachable in every site mode, so every site needs them before it collects signups. Build them from the site's facts and render them with DashLegalPage:

import { DashLegalPage } from "@c1m-public/dash-site-kit/ui";
import { privacyNotice } from "@c1m-public/dash-site-kit";

const facts = { siteName: "SimpleVan", contactHref: "/contact", sharesWithProviders: true, waitlist: true, updated: "2026-09-22" };
export default function PrivacyPage() {
  return <DashLegalPage document={privacyNotice(facts)} siteName={facts.siteName} />;
}

cookieNotice and termsOfUse take the same facts. The text states what the platform collects (requests, waitlist sign-ups, the c1m_ft attribution cookie); pass operator with the registered company name and review the text before relying on it.

Peer dependencies

@c1m-public/channel-client (>=0.36.0), next 16, react 19.

The moving quote form, validation, server data layer and customer lead portal are shared across Dash sites. A site supplies its own channel client and server actions, then renders DashMovingQuoteForm and DashLeadPortalPage from /ui. Quote submission uses one attempt ID as an idempotency key; address lookup and distance calls go through the site's server route so the channel credential stays server-side. Other industries can add a typed intake model without changing the broker's delivery and portal infrastructure.

Admin host links

A Dash's providers sign up, sign in and manage their workspace on the Dash's admin host (https://admin.<dash domain>, or https://<slug>.saasdash.app before it has a domain), never on saasdash.app. Set adminUrl in site.defaults.json and build links with createSiteConfig().adminUrl(path). DashAdminLink swaps its label while the admin host's c1m_signed_in hint is set; the session itself never leaves the admin host.