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

@willow-hq/roots

v0.4.0

Published

Shared client-side package for Willow customer sites: attribution capture, the lead form island, CallRail dynamic number insertion, and JSON-LD helpers. Built for Astro sites with React 19 islands, deployed per customer on Cloudflare.

Readme

@willow-hq/roots

Shared client-side package for Willow customer sites: attribution capture, the lead form island, CallRail dynamic number insertion, and JSON-LD helpers. Built for Astro sites with React 19 islands, deployed per customer on Cloudflare.

Install

Public package on npmjs.com, no auth token or .npmrc needed.

bun add @willow-hq/roots

react (^19.0.0) is a peer dependency. Astro's React integration already provides it; if a site doesn't have it yet, run bunx astro add react.

Attribution capture

initAttribution() reads gclid/gbraid/wbraid and UTM params off the current URL and writes a wl_attr record to localStorage. It's last-paid- touch with a 90-day window: a new paid touch overwrites the stored record, an organic visit does not clobber an existing paid touch. Call it once, as early as possible, from the Astro base layout:

---
// src/layouts/Base.astro
---
<html lang="en">
  <head>
    <script>
      import { initAttribution } from "@willow-hq/roots";
      initAttribution();
    </script>
  </head>
  <body>
    <slot />
  </body>
</html>

readAttributionBrowser() reads the stored record back (or null if nothing's stored or it's expired past 90 days). LeadForm calls this internally on submit, so most sites never need to call it directly.

The stored record keeps all three click-id candidates (gclid / gbraid / wbraid) raw; the platform backend resolves the single winner at write time (backend/app/click_id.py, gclid first), so no click-id logic lives client-side.

Lead form

LeadForm is the only interactive piece a customer site needs. It's unstyled by design, mounts as a React island, and POSTs to the platform backend on submit.

---
import { LeadForm } from "@willow-hq/roots";
---
<LeadForm
  client:visible
  siteId="willow"
  classNames={{
    form: "flex flex-col gap-3",
    input: "border rounded px-3 py-2",
    textarea: "border rounded px-3 py-2",
    button: "bg-ink text-white rounded px-4 py-2",
    success: "text-green-700",
    error: "text-red-700",
  }}
/>

Props:

  • siteId (required): the tenant slug, sent as site_id on the payload.
  • apiBase (optional): backend origin; defaults to the exported API_BASE constant (the shared Railway backend every customer site posts to). The form POSTs to ${apiBase}/web-leads. Custom forms should import API_BASE instead of hardcoding the origin.
  • submitLabel, successText, errorText, namePlaceholder, emailPlaceholder, phonePlaceholder, messagePlaceholder: optional copy overrides.
  • classNames: optional { form, input, textarea, button, success, error } map. There's no default styling; pass whatever the tenant's design system needs.

Fields: name, phone, email are required (the form no-ops on submit if any are empty); message is optional. There's a hidden subject honeypot field, visually hidden, excluded from tab order, and opted out of browser autofill, that real users never fill in. On submit the form reads the stored attribution via readAttributionBrowser() and builds the payload with buildLeadPayload, which drops captured_at and omits message/subject entirely when empty.

buildLeadPayload and its LeadPayload type are exported directly if you need to build the same payload shape outside the form (a custom form, a non-React island, and so on):

import { buildLeadPayload, readAttributionBrowser } from "@willow-hq/roots";

const payload = buildLeadPayload(
  "willow",
  { name, email, phone, message },
  readAttributionBrowser(),
);

CallRail dynamic number insertion

CallRail.astro renders CallRail's swap script for dynamic number insertion. The swapSrc URL comes from the tenant's sites registry (typed columns arrive with the onboarding-pipeline migration), not from a hardcoded value.

Pass siteUrl (the canonical production origin, i.e. SITE.url) so the swap script only loads when served from that host. Dev servers and preview origins otherwise start real CallRail sessions and hold pool numbers, which pollutes the customer's attribution data. Omitting siteUrl loads the script unconditionally.

---
import CallRail from "@willow-hq/roots/callrail.astro";
---
<CallRail swapSrc="https://cdn.callrail.com/companies/XXXX/swap.js" siteUrl="https://www.example.com" />

JSON-LD helpers

localBusinessJsonLd and faqJsonLd build the SEO structured-data objects every customer site needs. Render the result in a <script type="application/ld+json"> tag with set:html (Astro escapes plain interpolation, which breaks JSON):

---
import { localBusinessJsonLd, faqJsonLd } from "@willow-hq/roots";

const business = localBusinessJsonLd({
  name: "Cool Power HVAC",
  url: "https://coolpower.example.com",
  phone: "+1-555-010-0100",
  areaServed: ["Austin, TX", "Round Rock, TX"],
});

const faqs = faqJsonLd([
  { q: "Do you offer emergency service?", a: "Yes, 24/7 for existing customers." },
]);
---
<script type="application/ld+json" set:html={JSON.stringify(business)} />
<script type="application/ld+json" set:html={JSON.stringify(faqs)} />

localBusinessJsonLd takes name, url, and optional phone and areaServed. faqJsonLd takes an array of { q, a } pairs.

Publishing

CI publishes on every push to master that touches packages/roots/** (.github/workflows/publish-roots.yml): it installs, runs bun test, runs bun run build, then publishes to npmjs.com if the version in package.json isn't already published. There's no changesets step, versioning is a manual bump in package.json before merging. Publishing needs the NPM_TOKEN repo secret (a granular automation token for the willow-hq npm org) set up before the first publish; until that's done the test and build steps still run, only the publish step fails.