@keystone-sites/services
v1.0.1
Published
Keystone Sites Services - analytics/ads providers (Meta Pixel, PostHog, GTM) and the KeystoneServices injection shell for customer websites
Maintainers
Keywords
Readme
@keystone-sites/services
Analytics/ads providers and the injection shell for Keystone customer websites. Supplies Meta Pixel (browser + CAPI coordination), PostHog (events, pageviews, client log shipping via OTel), Google Tag Manager, and the KeystoneServices server component that wires them all up from server-fetched config.
KeystoneServices
KeystoneServices is a slim async server component that replaces the tracking role of the legacy KeystoneRootLayout. It fetches getAdsConfig(), getAnalyticsConfig(), getCompanyInformation(), and hasOptedOut() itself (via @keystone-sites/core) and renders Meta Pixel, PostHog + Keystone analytics tracking, and the GTM script around {children}.
// app/layout.tsx
import { KeystoneServices } from '@keystone-sites/services';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<KeystoneServices>{children}</KeystoneServices>
</body>
</html>
);
}Every service is a silent no-op when its config is not provisioned. Any future site-wide service (a new pixel, consent management, etc.) is added here once and every site picks it up on version bump.
KeystoneServices also skips mounting Meta Pixel / PostHog + Keystone analytics tracking / GTM entirely for a visitor who previously opted out via @keystone-sites/widgets' CookieConsentModal (checked with hasOptedOut() from @keystone-sites/core — same presence-gate shape as the config checks above). First-visit / no-stored-choice sites are unaffected. This is the CCPA/CPRA default (opt-out).
For EU/EEA/UK/CH visitors (getConsentRegime() from @keystone-sites/core returns 'gdpr'), the gate flips: nothing mounts until the visitor has explicitly granted that category (getGranularChoice() — { analytics, advertising }, undecided treated as denied). Meta Pixel gates on advertising; PostHog + GTM gate on analytics. This is a server-side, per-request check (Cloudflare's CF-IPCountry header), so a GDPR visitor never has the trackers mounted even for the first paint — true prior opt-in, not opt-in-after-the-fact.
Cookie consent
setPixelConsent / setAnalyticsConsent (in ./tracking) are the live opt-out/opt-in counterparts to the mount-time gate above — plain functions, same shape as firePixelEvent / captureEvent, for turning already-mounted trackers off (or back on) without a page reload:
import { setPixelConsent, setAnalyticsConsent } from '@keystone-sites/services/tracking';
setPixelConsent(false); // fbq('consent', 'revoke') — no-op if no Meta Pixel configured
setAnalyticsConsent(false); // posthog.opt_out_capturing() + GTM consent update — no-op per tracker not mountedCalled by @keystone-sites/widgets' CookieConsentModal / CookiePreferencesLink; see @keystone-sites/core's consent module for the persisted-choice state these pair with.
GDPR note: these same two functions grant/revoke live — they don't change whether a tracker is mounted in the first place. Since a GDPR visitor's tracker is not mounted until the mount-time gate above sees an explicit grant, CookieConsentModal triggers a Next.js router refresh after writing a granular "accept" choice so KeystoneServices re-evaluates server-side and actually mounts the newly-granted tracker — a live setAnalyticsConsent(true) call alone cannot un-un-mount a component tree that was never rendered.
Package exports
| Import path | Contents |
|---|---|
| @keystone-sites/services | KeystoneServices + everything from ./tracking |
| @keystone-sites/services/tracking | MetaPixel, MetaPixelTracker, firePixelEvent, setPixelUserData, PostHogProvider, KeystoneAnalyticsTracker, GoogleTagManager, captureEvent, captureCustomEvent, setPixelConsent, setAnalyticsConsent, log/warn/error client logging |
| @keystone-sites/services/services/KeystoneServices | The injection shell on its own |
Source directory structure
src/
├── services/
│ └── KeystoneServices.tsx # Injection shell (async server component)
└── tracking/ # Meta Pixel, PostHog, GTM components and helpers
├── MetaPixel.tsx / MetaPixelTracker.tsx / firePixelEvent.ts
├── PostHogProvider.tsx / KeystoneAnalyticsTracker.tsx / captureEvent.ts
├── GoogleTagManager.tsx
├── consent.ts # setPixelConsent / setAnalyticsConsent (opt-out controls)
└── logging.ts # Structured client logging (OTel → PostHog Logs)Publishing workflow
Publish to the public npm registry in dependency order (wait for each package to propagate before publishing dependents):
# 1. @keystone-sites/core (no internal Keystone deps)
npm run test && npm publish --access public
# 2. @keystone-sites/services (depends on @keystone-sites/core)
npm run test && npm publish --access public
# 3. @keystone-sites/widgets (depends on @keystone-sites/core + @keystone-sites/services)
npm run test && npm publish --access publicprepublishOnly runs npm run build automatically before each publish.
For local development against an unpublished build, use yalc:
# In @keystone-sites/services — build and publish to local yalc store
npm run build && yalc publish
# In the customer site — link to the local build
yalc add @keystone-sites/services
# or update an existing link
yalc update @keystone-sites/servicesTo restore the published npm version:
yalc remove @keystone-sites/services
npm installDocs
docs/meta-tracking.md— Meta Pixel + CAPI tracking contractdocs/posthog-tracking.md— PostHog events and taxonomy
