@sonordev/site-kit 7.0.0 → 7.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3539 -0
- package/README.md +12 -13
- package/agent-manifest.json +1 -1
- package/dist/{AnalyticsProvider-GJ6JFQO5.js → AnalyticsProvider-XXWTFKJH.js} +4 -4
- package/dist/{ArticleViewTracker-FEA5SBKW.js → ArticleViewTracker-V4KZB6QN.js} +3 -3
- package/dist/{BlocksPopup-MNK4SDZM.js → BlocksPopup-JHGHB6XW.js} +4 -4
- package/dist/{ChatWidget-M2YNLRYW.js → ChatWidget-CG32POI3.js} +5 -5
- package/dist/{EngageWidget-RR63JQCA.js → EngageWidget-LQMR4LEX.js} +4 -4
- package/dist/{FileField-JFBXXD43.js → FileField-KUG3CKXG.js} +3 -3
- package/dist/{FormSpotlight-QE3J3VB5.js → FormSpotlight-FVNPOCU3.js} +1 -1
- package/dist/{FormStage-OTPKEWID.js → FormStage-C7VKRURJ.js} +1 -1
- package/dist/{ManagedForm-3VRALWUD.js → ManagedForm-VLNJKV65.js} +6 -6
- package/dist/{ManagedNewsletterForm-A2FE6L5C.js → ManagedNewsletterForm-KJEU23BV.js} +4 -4
- package/dist/{SignalCore-S6DO3VQV.js → SignalCore-K2O46QG7.js} +3 -3
- package/dist/{SiteDesignReporter-2WZVWJMA.js → SiteDesignReporter-D7MD66GI.js} +5 -5
- package/dist/SitemapSync-NMXGMPCQ.js +8 -0
- package/dist/_client/booking-widget.js +5 -5
- package/dist/affiliates/index.js +3 -3
- package/dist/analytics/index.js +4 -4
- package/dist/articles/index.js +1 -1
- package/dist/articles/server-ui.js +1 -1
- package/dist/chat/index.js +5 -5
- package/dist/{chunk-XZMVNORA.js → chunk-42OXY4JV.js} +1 -1
- package/dist/{chunk-5YSI5KFD.js → chunk-56JNI463.js} +1 -1
- package/dist/{chunk-M4ZY2FVB.js → chunk-5FBY2ZIH.js} +1 -1
- package/dist/{chunk-3XHK5DFX.js → chunk-7JIKGKWD.js} +7 -7
- package/dist/{chunk-7ZHJIRM3.js → chunk-B6RZ2NRH.js} +1 -1
- package/dist/{chunk-UINSEWQ3.js → chunk-BEL7YFMC.js} +1 -1
- package/dist/{chunk-R3TOKUDJ.js → chunk-BS7FWUOY.js} +1 -1
- package/dist/{chunk-MZFF5F7R.js → chunk-DKTSGYLM.js} +2 -2
- package/dist/{chunk-LT5ITURG.js → chunk-GGD4P7UW.js} +1 -1
- package/dist/{chunk-OO2GZ272.js → chunk-GYY6ETGB.js} +1 -1
- package/dist/{chunk-I6WUCBKI.js → chunk-K5WZX776.js} +2 -2
- package/dist/{chunk-B52EXDWI.js → chunk-LJZ3SUET.js} +2 -2
- package/dist/{chunk-OSQHWIM5.js → chunk-O52CH273.js} +1 -1
- package/dist/{chunk-HPUXKMNR.js → chunk-OIETJKIL.js} +1 -1
- package/dist/{chunk-5UZN5V52.js → chunk-P2GIIQH5.js} +1 -1
- package/dist/{chunk-5C4WVOVO.js → chunk-P72ZJRSX.js} +3 -3
- package/dist/{chunk-4WI3FU5L.js → chunk-RU2RMTGT.js} +2 -2
- package/dist/{chunk-GYBMMWGF.js → chunk-SAUTJMK6.js} +1 -1
- package/dist/{chunk-JMRESWQT.js → chunk-SWP36NCB.js} +1 -1
- package/dist/{chunk-WFHI6HXP.js → chunk-T4SY3FMN.js} +3 -3
- package/dist/{chunk-6ZB3XNAT.js → chunk-ZRE4ZYEG.js} +1 -1
- package/dist/{chunk-6CUFRMMF.js → chunk-ZSLRAMCK.js} +1 -1
- package/dist/client/index.js +3 -3
- package/dist/commerce/index.js +4 -4
- package/dist/engage/index.js +6 -6
- package/dist/fleet/index.js +4 -4
- package/dist/forms/index.js +7 -7
- package/dist/forms/server.js +2 -2
- package/dist/forms/types.d.ts +3 -1
- package/dist/images/index.js +4 -4
- package/dist/index.js +1 -1
- package/dist/layout/client.js +7 -7
- package/dist/layout/index.js +8 -8
- package/dist/maps/index.js +3 -3
- package/dist/mcp/sonor.js +9 -7
- package/dist/seo/client.js +4 -4
- package/dist/seo/index.js +4 -4
- package/dist/server/index.js +2 -2
- package/dist/shared/version.d.ts +1 -1
- package/dist/signal/index.js +2 -2
- package/dist/sync/index.js +5 -5
- package/dist/website/images.js +4 -4
- package/dist/website/index.js +5 -5
- package/dist/website/popups.js +4 -4
- package/docs/MIGRATING-TO-7.md +146 -0
- package/docs.json +67 -0
- package/package.json +9 -4
- package/src/admin-auth/README.md +88 -0
- package/src/analytics/README.md +264 -0
- package/src/articles/README.md +325 -0
- package/src/commerce/README.md +109 -0
- package/src/cta-bar/README.md +154 -0
- package/src/engage/README.md +241 -0
- package/src/forms/README.md +219 -0
- package/src/images/README.md +74 -0
- package/src/layout/README.md +66 -0
- package/src/llms/README.md +723 -0
- package/src/mcp/README.md +376 -0
- package/src/motion/README.md +372 -0
- package/src/og/README.md +304 -0
- package/src/proxy/README.md +152 -0
- package/src/redirects/README.md +74 -0
- package/src/reputation/README.md +64 -0
- package/src/seo/README.md +359 -0
- package/src/signal/README.md +115 -0
- package/src/sitemap/README.md +127 -0
- package/src/sync/README.md +115 -0
- package/dist/SitemapSync-IKNKPT2G.js +0 -8
package/docs.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "What sonor.dev renders for this package, in nav order. sonor.dev reads this file from the published tarball (latest on npm), so a release IS the docs deploy: add a page here and ship it in package.json `files`. scripts/verify-docs.cjs fails the publish if a listed file is missing from the pack.",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"title": "site-kit",
|
|
5
|
+
"summary": "One package, one env var, every Sonor module on a Next.js site: SEO, analytics, forms, articles, commerce, chat, AI visibility, agent tools and motion.",
|
|
6
|
+
"groups": [
|
|
7
|
+
{
|
|
8
|
+
"title": "Start here",
|
|
9
|
+
"pages": [
|
|
10
|
+
{ "slug": "", "title": "Overview", "file": "README.md" },
|
|
11
|
+
{ "slug": "migrating-to-7", "title": "Migrating to 7", "file": "docs/MIGRATING-TO-7.md" },
|
|
12
|
+
{ "slug": "agents", "title": "For coding agents", "file": "AGENTS.md" }
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "Core",
|
|
17
|
+
"pages": [
|
|
18
|
+
{ "slug": "layout", "title": "Layout", "file": "src/layout/README.md" },
|
|
19
|
+
{ "slug": "seo", "title": "SEO", "file": "src/seo/README.md" },
|
|
20
|
+
{ "slug": "analytics", "title": "Analytics", "file": "src/analytics/README.md" },
|
|
21
|
+
{ "slug": "sitemap", "title": "Sitemap", "file": "src/sitemap/README.md" },
|
|
22
|
+
{ "slug": "proxy", "title": "Proxy", "file": "src/proxy/README.md" },
|
|
23
|
+
{ "slug": "redirects", "title": "Redirects", "file": "src/redirects/README.md" },
|
|
24
|
+
{ "slug": "og", "title": "OG cards", "file": "src/og/README.md" }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"title": "Content",
|
|
29
|
+
"pages": [
|
|
30
|
+
{ "slug": "articles", "title": "Articles", "file": "src/articles/README.md" },
|
|
31
|
+
{ "slug": "images", "title": "Images", "file": "src/images/README.md" },
|
|
32
|
+
{ "slug": "reputation", "title": "Reputation", "file": "src/reputation/README.md" }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"title": "Engagement",
|
|
37
|
+
"pages": [
|
|
38
|
+
{ "slug": "forms", "title": "Forms", "file": "src/forms/README.md" },
|
|
39
|
+
{ "slug": "chat", "title": "Website chat", "file": "src/engage/README.md" },
|
|
40
|
+
{ "slug": "cta-bar", "title": "CTA bar", "file": "src/cta-bar/README.md" },
|
|
41
|
+
{ "slug": "signal", "title": "Signal (A/B)", "file": "src/signal/README.md" }
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"title": "Commerce",
|
|
46
|
+
"pages": [
|
|
47
|
+
{ "slug": "commerce", "title": "Commerce", "file": "src/commerce/README.md" },
|
|
48
|
+
{ "slug": "booking", "title": "Booking", "file": "src/sync/README.md" }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"title": "AI visibility",
|
|
53
|
+
"pages": [
|
|
54
|
+
{ "slug": "llms", "title": "llms.txt and AEO", "file": "src/llms/README.md" },
|
|
55
|
+
{ "slug": "mcp", "title": "MCP and agent tools", "file": "src/mcp/README.md" }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"title": "More",
|
|
60
|
+
"pages": [
|
|
61
|
+
{ "slug": "motion", "title": "Motion", "file": "src/motion/README.md" },
|
|
62
|
+
{ "slug": "admin-auth", "title": "Sign in with Sonor", "file": "src/admin-auth/README.md" }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"changelog": { "file": "CHANGELOG.md", "since": "7.0.0" }
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sonordev/site-kit",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@11.5.3",
|
|
6
6
|
"description": "Complete client-side integration kit for Sonor - SEO, Analytics, Engage, Forms, Blog",
|
|
@@ -367,7 +367,11 @@
|
|
|
367
367
|
"README.md",
|
|
368
368
|
"AGENTS.md",
|
|
369
369
|
"agent-manifest.json",
|
|
370
|
-
"skills"
|
|
370
|
+
"skills",
|
|
371
|
+
"docs.json",
|
|
372
|
+
"CHANGELOG.md",
|
|
373
|
+
"docs/MIGRATING-TO-7.md",
|
|
374
|
+
"src/*/README.md"
|
|
371
375
|
],
|
|
372
376
|
"scripts": {
|
|
373
377
|
"build": "NODE_OPTIONS=--max-old-space-size=8192 tsup && pnpm build:types && node scripts/gen-agent-manifest.cjs",
|
|
@@ -387,8 +391,9 @@
|
|
|
387
391
|
"typecheck": "tsc --noEmit",
|
|
388
392
|
"typecheck:setup": "tsc --noEmit -p packages/sonor-setup/tsconfig.json",
|
|
389
393
|
"version": "node scripts/sync-version.cjs && node scripts/gen-agent-manifest.cjs && git add src/shared/version.ts agent-manifest.json",
|
|
390
|
-
"prepublishOnly": "rm -rf dist && NODE_OPTIONS=--max-old-space-size=8192 tsup && pnpm build:types && node scripts/gen-agent-manifest.cjs && node scripts/verify-dts.cjs && node scripts/prepublish-integration.cjs && node scripts/audit-as-consumer.cjs",
|
|
391
|
-
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --noEmit false && node scripts/alias-dts.cjs"
|
|
394
|
+
"prepublishOnly": "rm -rf dist && NODE_OPTIONS=--max-old-space-size=8192 tsup && pnpm build:types && node scripts/gen-agent-manifest.cjs && node scripts/verify-dts.cjs && node scripts/prepublish-integration.cjs && node scripts/audit-as-consumer.cjs && node scripts/verify-docs.cjs",
|
|
395
|
+
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --noEmit false && node scripts/alias-dts.cjs",
|
|
396
|
+
"verify:docs": "node scripts/verify-docs.cjs"
|
|
392
397
|
},
|
|
393
398
|
"peerDependencies": {
|
|
394
399
|
"@portabletext/react": "^8.0.0",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# `@sonordev/site-kit/admin-auth` — Sign in with Sonor
|
|
2
|
+
|
|
3
|
+
Put a site's own admin area behind a Sonor login. Whoever can open the
|
|
4
|
+
project in Sonor (its owners and members, the managing agency, platform
|
|
5
|
+
admins) can sign in; nobody else can. There are no passwords on the site.
|
|
6
|
+
|
|
7
|
+
## Wiring
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// lib/sonor-sso.ts
|
|
11
|
+
import { createSonorSso } from '@sonordev/site-kit/admin-auth'
|
|
12
|
+
|
|
13
|
+
export const sso = createSonorSso({
|
|
14
|
+
paths: '/admin', // or ['/bid', '/route'] for several areas
|
|
15
|
+
// loginPath: '/admin/login' (default: `${paths[0]}/login`)
|
|
16
|
+
// adminEmails: process.env.ADMIN_EMAILS,
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// proxy.ts
|
|
22
|
+
import { createProxy } from '@sonordev/site-kit/proxy'
|
|
23
|
+
import { sso } from '@/lib/sonor-sso'
|
|
24
|
+
|
|
25
|
+
export default createProxy({ before: sso.gate, redirects: true, securityHeaders: true })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// app/api/auth/callback/route.ts
|
|
30
|
+
import { sso } from '@/lib/sonor-sso'
|
|
31
|
+
export const GET = sso.handleCallback
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// app/api/auth/logout/route.ts
|
|
36
|
+
import { sso } from '@/lib/sonor-sso'
|
|
37
|
+
export const GET = sso.handleLogout
|
|
38
|
+
export const POST = sso.handleLogout
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// app/admin/login/page.tsx (server component)
|
|
43
|
+
import { SSO_ERROR_MESSAGES } from '@sonordev/site-kit/admin-auth'
|
|
44
|
+
import { sso } from '@/lib/sonor-sso'
|
|
45
|
+
|
|
46
|
+
export default async function Login({ searchParams }) {
|
|
47
|
+
const { error, return_to } = await searchParams
|
|
48
|
+
let href: string | null = null
|
|
49
|
+
try { href = await sso.getLoginUrl({ returnTo: return_to }) } catch {}
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
{error && <p>{SSO_ERROR_MESSAGES[error] ?? error}</p>}
|
|
53
|
+
{href ? <a href={href}>Sign in with Sonor</a> : <p>Sign-in is unavailable.</p>}
|
|
54
|
+
</>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// Every API route and server action that touches admin data
|
|
61
|
+
const session = await sso.getSession()
|
|
62
|
+
if (!session) return new Response('Unauthorized', { status: 401 })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Designer APIs or other JSON endpoints can be guarded in the middleware too:
|
|
66
|
+
pass `apiPaths: ['/api/studio']` and include them in the matcher. They get a
|
|
67
|
+
401 JSON response instead of a redirect.
|
|
68
|
+
|
|
69
|
+
## Env
|
|
70
|
+
|
|
71
|
+
| Var | |
|
|
72
|
+
|---|---|
|
|
73
|
+
| `SONOR_API_KEY` | Already on every site. Identifies the project and verifies tokens. |
|
|
74
|
+
| `SONOR_SESSION_SECRET` | 32+ random chars (`openssl rand -hex 32`). Rotate to sign everyone out. |
|
|
75
|
+
| `NEXT_PUBLIC_SITE_URL` | The site's public origin, for the callback URL. |
|
|
76
|
+
|
|
77
|
+
The callback route has to be reachable at `callbackPath` (default
|
|
78
|
+
`/api/auth/callback`) on `NEXT_PUBLIC_SITE_URL`.
|
|
79
|
+
|
|
80
|
+
## Rules
|
|
81
|
+
|
|
82
|
+
- **The gate doesn't cover `/api/*`.** The standard middleware matcher
|
|
83
|
+
excludes it. Check `getSession()` in every API route.
|
|
84
|
+
- **Don't trust identity from headers, query strings or bodies.**
|
|
85
|
+
`getSession()` reads the httpOnly cookie and verifies its signature on
|
|
86
|
+
every call; that's the only source.
|
|
87
|
+
- **Moving a site onto this module:** pass its existing `cookieName` and
|
|
88
|
+
`secretEnv` so current sessions stay valid.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Analytics — `@sonordev/site-kit/analytics`
|
|
2
|
+
|
|
3
|
+
Automatic page view tracking, custom events, conversions, scroll depth, heatmap clicks, and Core Web Vitals. All data flows through the Sonor API.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
`SiteKitLayout` mounts analytics for you. It renders `{children}` first and
|
|
8
|
+
mounts `AnalyticsProvider` after them as a childless sibling, deferred until
|
|
9
|
+
window load + idle (or the first interaction). Analytics never sits in the
|
|
10
|
+
page's hydration path and can't push a route to client rendering. Configure it
|
|
11
|
+
on the layout:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
// app/layout.tsx (server component)
|
|
15
|
+
import { SiteKitLayout } from '@sonordev/site-kit/layout'
|
|
16
|
+
import { ContactTracking } from '@sonordev/site-kit/analytics'
|
|
17
|
+
|
|
18
|
+
export default function RootLayout({ children }) {
|
|
19
|
+
return (
|
|
20
|
+
<html lang="en">
|
|
21
|
+
<body>
|
|
22
|
+
<SiteKitLayout analytics={{ excludePaths: ['/admin'] }}>{children}</SiteKitLayout>
|
|
23
|
+
{/* Optional: tel:/mailto: clicks as conversions. Childless, renders null. */}
|
|
24
|
+
<ContactTracking />
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Don't wrap `{children}` in `AnalyticsProvider` yourself. It puts analytics in
|
|
32
|
+
the hydration path (the page-view code ships in the page's initial scripts),
|
|
33
|
+
and if it's lazy-loaded with `next/dynamic({ ssr: false })` the whole route
|
|
34
|
+
de-opts to client rendering.
|
|
35
|
+
|
|
36
|
+
### Without `SiteKitLayout`
|
|
37
|
+
|
|
38
|
+
A layout that doesn't use core `SiteKitLayout` mounts analytics itself, and
|
|
39
|
+
still as a deferred, childless island. Publish the credential server-side
|
|
40
|
+
first with `SiteKitCredential` (see `@sonordev/site-kit/client`). Never pass
|
|
41
|
+
`SONOR_API_KEY` to a client component.
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
// app/analytics-shell.tsx
|
|
45
|
+
'use client'
|
|
46
|
+
import { AnalyticsProvider, ContactTracking } from '@sonordev/site-kit/analytics'
|
|
47
|
+
|
|
48
|
+
export default function AnalyticsShell() {
|
|
49
|
+
return (
|
|
50
|
+
<AnalyticsProvider trackPageViews trackWebVitals>
|
|
51
|
+
<ContactTracking />
|
|
52
|
+
</AnalyticsProvider>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// app/deferred-analytics.tsx
|
|
59
|
+
'use client'
|
|
60
|
+
import { lazy, Suspense } from 'react'
|
|
61
|
+
import { useDeferredActivation } from '@sonordev/site-kit/client'
|
|
62
|
+
|
|
63
|
+
const AnalyticsShell = lazy(() => import('./analytics-shell'))
|
|
64
|
+
|
|
65
|
+
export function DeferredAnalytics() {
|
|
66
|
+
const ready = useDeferredActivation(true)
|
|
67
|
+
if (!ready) return null
|
|
68
|
+
return (
|
|
69
|
+
<Suspense fallback={null}>
|
|
70
|
+
<AnalyticsShell />
|
|
71
|
+
</Suspense>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// app/layout.tsx (server component): {children} is a SIBLING of the island, never inside it
|
|
78
|
+
import { resolveClientCredential } from '@sonordev/site-kit/server'
|
|
79
|
+
import { SiteKitCredential } from '@sonordev/site-kit/client'
|
|
80
|
+
import { DeferredAnalytics } from './deferred-analytics'
|
|
81
|
+
|
|
82
|
+
export default async function RootLayout({ children }) {
|
|
83
|
+
const credential = await resolveClientCredential(process.env.SONOR_API_KEY ?? '', 'https://api.sonor.io')
|
|
84
|
+
return (
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<body>
|
|
87
|
+
<SiteKitCredential apiKey={credential} />
|
|
88
|
+
{children}
|
|
89
|
+
<DeferredAnalytics />
|
|
90
|
+
</body>
|
|
91
|
+
</html>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`AnalyticsProvider` already mounts `WebVitals` when `trackWebVitals` is on.
|
|
97
|
+
Don't add a second `<WebVitals />`, or every metric reports twice.
|
|
98
|
+
|
|
99
|
+
## Tracking custom events
|
|
100
|
+
|
|
101
|
+
Under `SiteKitLayout` the provider is childless, so no page component sits
|
|
102
|
+
inside it. Use the standalone functions. They need no provider and queue until
|
|
103
|
+
the deferred provider mounts:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
'use client'
|
|
107
|
+
import { trackEvent, trackConversion } from '@sonordev/site-kit/analytics'
|
|
108
|
+
|
|
109
|
+
trackEvent({ name: 'button_click', category: 'engagement', properties: { buttonId: 'cta' } })
|
|
110
|
+
trackConversion({ type: 'purchase', value: 99.99, currency: 'USD' })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Hooks
|
|
114
|
+
|
|
115
|
+
`useAnalytics()` and `useTrackEvent()` read the provider's context, so they
|
|
116
|
+
only work in components rendered inside an `AnalyticsProvider` (children of
|
|
117
|
+
your own analytics island, for example). Anywhere else they throw. Reach for
|
|
118
|
+
the standalone functions above instead of wrapping `{children}` to make a hook
|
|
119
|
+
work. `useAnalyticsOptional()` returns `null` instead of throwing.
|
|
120
|
+
|
|
121
|
+
### useAnalytics()
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
const { trackEvent, trackConversion, sessionId, visitorId } = useAnalytics()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### useTrackEvent()
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
const { trackEvent, trackConversion } = useTrackEvent()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### useContactTracking()
|
|
134
|
+
|
|
135
|
+
Auto-tracks `tel:` and `mailto:` link clicks as conversions. Works anywhere: it
|
|
136
|
+
uses the standalone dispatch, not the provider's context.
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
const { trackPhoneClick, trackEmailClick } = useContactTracking()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Components
|
|
143
|
+
|
|
144
|
+
| Component | Purpose |
|
|
145
|
+
|-----------|---------|
|
|
146
|
+
| `AnalyticsProvider` | The tracker. `SiteKitLayout` mounts it childless; never wrap `{children}` in it |
|
|
147
|
+
| `WebVitals` | Auto-reports LCP, CLS, TTFB, INP, FCP. `AnalyticsProvider` mounts it when `trackWebVitals` is on |
|
|
148
|
+
| `ContactTracking` | Auto-tracks phone/email link clicks. Childless, renders null |
|
|
149
|
+
|
|
150
|
+
## AnalyticsProvider Props
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
interface AnalyticsConfig {
|
|
154
|
+
projectId?: string // Auto-resolved from API key
|
|
155
|
+
trackPageViews?: boolean // Default: true
|
|
156
|
+
trackWebVitals?: boolean // Default: true
|
|
157
|
+
trackScrollDepth?: boolean // Default: true
|
|
158
|
+
sessionTimeout?: number // Minutes (default: 30)
|
|
159
|
+
excludePaths?: string[] // Don't track these paths
|
|
160
|
+
allowInFrame?: boolean // Default: false — see below
|
|
161
|
+
allowLocalhost?: boolean // Default: false — local builds report nothing
|
|
162
|
+
debug?: boolean // Log events to console
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## What Gets Tracked Automatically
|
|
167
|
+
|
|
168
|
+
- **Page views** — on every route change (path, URL, title, referrer, UTM params, device/browser/OS)
|
|
169
|
+
- **Web Vitals** — LCP, CLS, TTFB, INP, FCP with good/needs-improvement/poor ratings
|
|
170
|
+
- **Contact clicks** — `tel:` and `mailto:` links tracked as conversions, once `<ContactTracking />` is mounted (it isn't by default)
|
|
171
|
+
- **DOM metadata** — full snapshot per page view (meta tags, H1, word count, links, content, FAQs)
|
|
172
|
+
|
|
173
|
+
## Embedded pages report nothing (`analytics.allowInFrame`)
|
|
174
|
+
|
|
175
|
+
When this site is loaded inside a **cross-origin iframe**, nothing is sent —
|
|
176
|
+
no page views, journey/session rows, scroll depth, heatmap clicks, web vitals,
|
|
177
|
+
events or conversions. The visitor is on whoever framed the page, not on this
|
|
178
|
+
site, so every metric the frame produces is phantom traffic in the analytics
|
|
179
|
+
its owner reads.
|
|
180
|
+
|
|
181
|
+
This is not hypothetical. Upforge case studies embed each client's whole
|
|
182
|
+
production site in three eager iframes, and `securityHeaders`'
|
|
183
|
+
`DEFAULT_FRAME_ANCESTORS` permits that. Before the guard landed, 23 client
|
|
184
|
+
sites carried the signature in `analytics_page_views`: three views of `/`
|
|
185
|
+
sharing one session and visitor id, 21-80ms apart, referrer
|
|
186
|
+
`https://upforge.io/` — the desktop, tablet and mobile frames of one case
|
|
187
|
+
study. The screenshot overlay in those frames hides the pixels, not the
|
|
188
|
+
JavaScript.
|
|
189
|
+
|
|
190
|
+
**Same-origin frames still report.** A site embedding itself (a preview pane,
|
|
191
|
+
a print view, an on-domain booking frame) has a real visitor really on that
|
|
192
|
+
site and no other tenant to pollute.
|
|
193
|
+
|
|
194
|
+
Opt back in only when the frame IS the product — a widget or partner-hosted
|
|
195
|
+
page deliberately distributed as an embed:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<SiteKitLayout analytics={{ allowInFrame: true }}>…</SiteKitLayout>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
There is deliberately no env var or window global for this. A silent switch
|
|
202
|
+
that turns cross-tenant tracking back on is the failure mode, not the feature.
|
|
203
|
+
|
|
204
|
+
The decision lives in one place — `shared/reporting-gate.ts`, over the frame
|
|
205
|
+
primitive in `shared/frame.ts`. Every send in the module routes through it, and
|
|
206
|
+
`send-gate.test.ts` fails the build if a new one does not. `isCrossOriginFrame()`
|
|
207
|
+
is exported from `@sonordev/site-kit/analytics` if a site needs the same answer
|
|
208
|
+
for its own third-party pixels.
|
|
209
|
+
|
|
210
|
+
## Local builds report nothing (`analytics.allowLocalhost`)
|
|
211
|
+
|
|
212
|
+
The actual browser hostname controls this gate, even in a production build
|
|
213
|
+
with `analytics.site` or `NEXT_PUBLIC_SITE_URL` set to a production domain.
|
|
214
|
+
`localhost`, its subdomains, `127.0.0.1`, `[::1]`, and `0.0.0.0` report nothing
|
|
215
|
+
by default: page views, events, conversions, sessions, vitals, scroll and
|
|
216
|
+
heatmap data, fleet heartbeats, and client-side sitemap registrations all use
|
|
217
|
+
`resolveAnalyticsTarget`. Engage doesn't mount its widgets or chat transport.
|
|
218
|
+
Public hosts and same-origin frames on public hosts still report normally.
|
|
219
|
+
|
|
220
|
+
For intentional local reporting:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
<SiteKitLayout analytics={{ allowLocalhost: true }}>…</SiteKitLayout>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The layout passes this option to AnalyticsProvider, WebVitals, FleetHeartbeat,
|
|
227
|
+
SitemapSync and EngageWidget. Standalone components and `sendFleetHeartbeat`
|
|
228
|
+
accept it too; standalone `trackEvent`/`trackConversion` use their mounted
|
|
229
|
+
AnalyticsProvider's options. A local cross-origin frame needs **both** opt-ins.
|
|
230
|
+
Neither option bypasses authentication. No environment variable or global
|
|
231
|
+
silently enables local reporting.
|
|
232
|
+
|
|
233
|
+
This is a browser gate: build-time `createSitemap` and Node fleet reporting
|
|
234
|
+
still run. It doesn't disable forms, commerce, or Signal. When verifying those
|
|
235
|
+
modules locally, point them at a test API as well.
|
|
236
|
+
|
|
237
|
+
## Environment
|
|
238
|
+
|
|
239
|
+
Uses `SONOR_API_KEY` (injected by `SiteKitLayout`) or `window.__SITE_KIT_API_KEY__`.
|
|
240
|
+
|
|
241
|
+
## Multi-site projects (`analytics.site`)
|
|
242
|
+
|
|
243
|
+
A single Sonor project can host many sub-sites (e.g. one project hosting
|
|
244
|
+
example.com + 16 state microsites). Every analytics event is
|
|
245
|
+
tagged with the sub-site host so the dashboard can roll up + filter per
|
|
246
|
+
site without forcing each microsite into its own Sonor project.
|
|
247
|
+
|
|
248
|
+
`SiteKitLayout` resolves the host in this precedence order:
|
|
249
|
+
|
|
250
|
+
1. Explicit `analytics.site` config
|
|
251
|
+
2. `NEXT_PUBLIC_SITE_URL` host
|
|
252
|
+
3. `window.location.host`
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
<SiteKitLayout
|
|
256
|
+
analytics={{ site: 'ohio.example.com' }}
|
|
257
|
+
>
|
|
258
|
+
…
|
|
259
|
+
</SiteKitLayout>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Most projects leave it implicit — `NEXT_PUBLIC_SITE_URL` is already set per
|
|
263
|
+
microsite, so the dimension fills in automatically. The dashboard shows a
|
|
264
|
+
"Site" picker + a "Sites" tab as soon as ≥2 distinct hosts are detected.
|