@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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Signal — `@sonordev/site-kit/signal`
|
|
2
|
+
|
|
3
|
+
Real-time A/B experiments, behavior tracking, and dynamic configuration from Signal AI. Requires `full_signal` plan.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Auto-included by `SiteKitLayout` when `signal` prop is enabled:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
<SiteKitLayout signal>{children}</SiteKitLayout>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For standalone use:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
'use client'
|
|
17
|
+
import { SignalBridge } from '@sonordev/site-kit/signal'
|
|
18
|
+
|
|
19
|
+
export default function Providers({ children }) {
|
|
20
|
+
return <SignalBridge>{children}</SignalBridge>
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## A/B Experiments
|
|
25
|
+
|
|
26
|
+
### Declarative
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { SignalExperiment } from '@sonordev/site-kit/signal'
|
|
30
|
+
|
|
31
|
+
<SignalExperiment
|
|
32
|
+
experimentId="hero-cta"
|
|
33
|
+
variants={{
|
|
34
|
+
control: <Button>Get Started</Button>,
|
|
35
|
+
variant_a: <Button>Start Free Trial</Button>,
|
|
36
|
+
}}
|
|
37
|
+
trackImpression
|
|
38
|
+
fallback={<Button>Default</Button>}
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Hook-Based
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useSignalExperiment } from '@sonordev/site-kit/signal'
|
|
46
|
+
|
|
47
|
+
function HeroCTA() {
|
|
48
|
+
const { variant, isControl } = useSignalExperiment('hero-cta')
|
|
49
|
+
return isControl ? <Button>Get Started</Button> : <Button>Start Free Trial</Button>
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Conversion Tracking
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { ExperimentConversion } from '@sonordev/site-kit/signal'
|
|
57
|
+
|
|
58
|
+
<ExperimentConversion experimentId="hero-cta" conversionType="click">
|
|
59
|
+
<Button>Sign Up</Button>
|
|
60
|
+
</ExperimentConversion>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Hooks
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
useSignal() // Full context: config, loading, trackEvent, trackOutcome
|
|
67
|
+
useSignalConfig() // Just the config object
|
|
68
|
+
useSignalEvent() // Returns trackEvent function
|
|
69
|
+
useSignalOutcome() // Returns trackOutcome function
|
|
70
|
+
useSignalExperiment(id) // Returns { assignment, variant, isControl }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## SignalBridge Props
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
interface SignalBridgeProps {
|
|
77
|
+
enabled?: boolean // Default: true
|
|
78
|
+
realtime?: boolean // SSE real-time updates (default: true)
|
|
79
|
+
experiments?: boolean // Participate in A/B tests (default: true)
|
|
80
|
+
behaviorTracking?: boolean // Scroll, clicks, time-on-page (default: true)
|
|
81
|
+
children: React.ReactNode
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## What It Does
|
|
86
|
+
|
|
87
|
+
1. Fetches config from `GET /api/public/signal/config`
|
|
88
|
+
2. Opens SSE stream for real-time `config_update` and `experiment_update` events
|
|
89
|
+
3. Assigns experiment variants per visitor (cached)
|
|
90
|
+
4. Batches behavioral events (scroll depth, click count, time-on-page) and flushes on debounce
|
|
91
|
+
5. Tracks outcomes/conversions via POST
|
|
92
|
+
|
|
93
|
+
## Key Types
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
interface ExperimentConfig {
|
|
97
|
+
id: string; name: string;
|
|
98
|
+
status: 'draft' | 'running' | 'paused' | 'completed';
|
|
99
|
+
variants: ExperimentVariant[];
|
|
100
|
+
traffic_allocation: number; // 0-1
|
|
101
|
+
goal: string;
|
|
102
|
+
winner?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface ExperimentVariant {
|
|
106
|
+
key: string; name: string; weight: number; description?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface SignalEvent {
|
|
110
|
+
event_type: string; event_name: string; event_data?: object;
|
|
111
|
+
page_url: string; page_title: string;
|
|
112
|
+
engagement: { time_on_page: number; scroll_depth: number; click_count: number };
|
|
113
|
+
experiments: Array<{ id: string; variant: string }>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Sitemap — `@sonordev/site-kit/sitemap`
|
|
2
|
+
|
|
3
|
+
Auto-generates `sitemap.xml` from your Next.js app directory structure. Discovers pages, resolves dynamic routes, syncs to Sonor, and optionally writes build-time `llms.txt`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// app/sitemap.ts
|
|
9
|
+
import { createSitemap } from '@sonordev/site-kit/sitemap'
|
|
10
|
+
|
|
11
|
+
export default createSitemap({
|
|
12
|
+
baseUrl: 'https://example.com',
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Config
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface SitemapConfig {
|
|
20
|
+
baseUrl?: string // Resolved from Sonor API if not set
|
|
21
|
+
trailingSlash?: boolean // Default: next.config's trailingSlash (see below)
|
|
22
|
+
exclude?: string[] // Glob patterns: ['/admin/*', '/api/*']
|
|
23
|
+
defaultPriority?: number // Default: 0.5
|
|
24
|
+
defaultChangeFrequency?: 'weekly' | 'monthly' | 'yearly' | 'never'
|
|
25
|
+
|
|
26
|
+
// Dynamic route resolution
|
|
27
|
+
dynamicRoutes?: Record<string, string[]> // Manual: { '[slug]': ['seo', 'analytics'] }
|
|
28
|
+
resolveGenerateStaticParams?: boolean // Auto-import generateStaticParams (default: true)
|
|
29
|
+
|
|
30
|
+
// Priority overrides by path pattern
|
|
31
|
+
priorities?: Record<string, number> // { '/services/*': 0.8, '/article/*': 0.6 }
|
|
32
|
+
|
|
33
|
+
// Additional paths not in app directory
|
|
34
|
+
additionalPaths?: () => Promise<{ path: string; priority?: number }[]>
|
|
35
|
+
|
|
36
|
+
// Sonor sync
|
|
37
|
+
apiUrl?: string // Sonor API URL
|
|
38
|
+
apiKey?: string // Sonor API key
|
|
39
|
+
disableSync?: boolean // Skip Sonor sync (default: false)
|
|
40
|
+
awaitMetaOptimization?: boolean // Wait for Signal meta optimization
|
|
41
|
+
|
|
42
|
+
// GEO / llms.txt integration
|
|
43
|
+
optimizedLLMsTxt?: boolean // Write AI-optimized llms.txt at build (default: true)
|
|
44
|
+
optimizedLLMsFullTxt?: boolean // Also write llms-full.txt
|
|
45
|
+
includeLlmsTxtInSitemap?: boolean // Add /llms.txt to sitemap (default: false; leave it off)
|
|
46
|
+
includeLlmsFullTxtInSitemap?: boolean // Add /llms-full.txt (default: false; leave it off)
|
|
47
|
+
|
|
48
|
+
// Intelligent priority (requires Signal)
|
|
49
|
+
intelligentPriority?: boolean // Use visibility scores + depth heuristics
|
|
50
|
+
|
|
51
|
+
// Local data for llms.txt fallback
|
|
52
|
+
getLocalData?: () => Promise<LLMsDataResponse | null>
|
|
53
|
+
llmsPublicSummaryOnly?: boolean
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## How Page Discovery Works
|
|
58
|
+
|
|
59
|
+
1. Scans `app/` directory recursively for `page.tsx`/`page.jsx` files
|
|
60
|
+
2. Detects dynamic segments (`[slug]`, `[...catchAll]`) and resolves them via:
|
|
61
|
+
- `dynamicRoutes` config (highest priority)
|
|
62
|
+
- Auto-import of `generateStaticParams()` from the page file (5s timeout)
|
|
63
|
+
3. Fetches portfolio paths via `getPortfolioPaths()` if portfolio module is used
|
|
64
|
+
4. Deduplicates and filters exclusion patterns
|
|
65
|
+
5. Infers `pageType` for each page (homepage, service, article, faq, etc.)
|
|
66
|
+
|
|
67
|
+
## Trailing Slashes
|
|
68
|
+
|
|
69
|
+
Every `<loc>` is the URL the site actually serves. With `trailingSlash: true` in
|
|
70
|
+
next.config, Next 308-redirects `/about` to `/about/`, so createSitemap emits
|
|
71
|
+
`/about/`. The exceptions follow Next's own redirects: `/` itself, file-like
|
|
72
|
+
paths (a `.` in the last segment, like `/llms.txt` or `/feed.xml`) and
|
|
73
|
+
`/.well-known/*`.
|
|
74
|
+
|
|
75
|
+
You don't need to set anything. The option defaults to the site's next.config
|
|
76
|
+
value, which Next inlines into the bundle. Set `trailingSlash` only if the kit
|
|
77
|
+
is loaded outside Next's bundler (`serverExternalPackages`).
|
|
78
|
+
|
|
79
|
+
Only the emitted URL changes. Dedupe, `exclude`, `priorities` and the Sonor
|
|
80
|
+
sync all use the unslashed path, which is how `seo_pages` stores it. The
|
|
81
|
+
build-time llms.txt links follow the same setting. `npx sonor-setup doctor`
|
|
82
|
+
warns (`sitemap.trailing-slash`) when a built sitemap's URLs don't match
|
|
83
|
+
next.config.
|
|
84
|
+
|
|
85
|
+
## Portfolio Paths
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { getPortfolioPaths } from '@sonordev/site-kit/sitemap'
|
|
89
|
+
|
|
90
|
+
createSitemap({
|
|
91
|
+
additionalPaths: () => getPortfolioPaths({ basePath: '/work', priority: 0.7 }),
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Sonor Sync
|
|
96
|
+
|
|
97
|
+
During `next build` only (not ISR), the sitemap entries are POSTed to Sonor via `POST /api/public/seo/register-sitemap` with `full-replace` mode. This keeps `seo_pages` in sync with the actual site structure.
|
|
98
|
+
|
|
99
|
+
## Build-Time llms.txt
|
|
100
|
+
|
|
101
|
+
When `optimizedLLMsTxt` is true (default), the sitemap build also writes `public/llms.txt` (and optionally `public/llms-full.txt`) using `writeLLMsTxtToPublic()`. This is served as a static file by the llms route handler.
|
|
102
|
+
|
|
103
|
+
If the Sonor API is unreachable at build time and local data can't produce real content, the write is **skipped** (with a warning) rather than replaced with an "Information not available." stub — any existing `public/llms.txt` / `public/llms-full.txt` from a previous successful build keeps serving.
|
|
104
|
+
|
|
105
|
+
### It often won't fit in the route — write it from your postbuild
|
|
106
|
+
|
|
107
|
+
Sonor generates llms.txt with an LLM call that can take a minute, and Next
|
|
108
|
+
gives a prerendered route 60s before it retries and fails the build. So the
|
|
109
|
+
in-route write gets only what's left of a 50s share of that budget: it refreshes
|
|
110
|
+
the file when Sonor is quick, and otherwise leaves the existing file serving
|
|
111
|
+
(the warning says so). It never fails the build.
|
|
112
|
+
|
|
113
|
+
For a refresh on every build, move the write to your postbuild, where nothing
|
|
114
|
+
imposes a ceiling and the sync it reads has already run:
|
|
115
|
+
|
|
116
|
+
```jsonc
|
|
117
|
+
// package.json
|
|
118
|
+
"scripts": { "postbuild": "sonor-register-sitemap --write-llms" }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
// app/sitemap.ts — one writer owns the file
|
|
123
|
+
export default createSitemap({ baseUrl, optimizedLLMsTxt: false })
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Add `--write-llms-full` for `public/llms-full.txt`. The flag works even when the
|
|
127
|
+
CLI skips its own sync because a sitemap route owns it.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Sync (Booking) — `@sonordev/site-kit/sync`
|
|
2
|
+
|
|
3
|
+
Embeddable booking/scheduling widget — like Calendly, built into Sonor. Appointments, consultations, classes.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
'use client'
|
|
9
|
+
import { BookingWidget } from '@sonordev/site-kit/sync'
|
|
10
|
+
|
|
11
|
+
export default function BookPage() {
|
|
12
|
+
return (
|
|
13
|
+
<BookingWidget
|
|
14
|
+
onBookingComplete={(result) => {
|
|
15
|
+
console.log('Booked:', result.booking.confirmationCode)
|
|
16
|
+
}}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Props
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
interface BookingWidgetProps {
|
|
26
|
+
orgSlug?: string // Organization slug (for public mode)
|
|
27
|
+
apiKey?: string // API key (auto-resolved from env)
|
|
28
|
+
apiUrl?: string // Default: https://api.sonor.io
|
|
29
|
+
bookingTypeSlug?: string // Show specific type only
|
|
30
|
+
timezone?: string // Guest timezone (auto-detected)
|
|
31
|
+
className?: string
|
|
32
|
+
daysToShow?: number // Days of availability to display
|
|
33
|
+
onBookingComplete?: (result: BookingResult) => void
|
|
34
|
+
onError?: (error: Error) => void
|
|
35
|
+
hideTypeSelector?: boolean // Hide type selector for single-type embed
|
|
36
|
+
|
|
37
|
+
styles?: { // Custom theme
|
|
38
|
+
primaryColor?: string
|
|
39
|
+
borderRadius?: string
|
|
40
|
+
fontFamily?: string
|
|
41
|
+
backgroundColor?: string
|
|
42
|
+
textPrimary?: string
|
|
43
|
+
borderColor?: string
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## UI Flow
|
|
49
|
+
|
|
50
|
+
1. **Type selector:** choose a booking type, unless a specific `bookingTypeSlug` is provided.
|
|
51
|
+
2. **Meeting format:** when the booking type enables meeting options, choose virtual, office, or a visit to the guest's location. Travel meetings validate the address before showing availability.
|
|
52
|
+
3. **Calendar and times:** choose an available time and reserve it while completing contact details.
|
|
53
|
+
4. **Guest info:** name, email, phone, and notes. Travel meetings require a phone number.
|
|
54
|
+
5. **Result:** a confirmed booking shows calendar links when Sonor sends the guest's invitation. When the host's Google Calendar sends it, the widget says who the invite is coming from instead (see [Calendar invitations](#calendar-invitations)). Meetings that require review show a pending request without calendar links.
|
|
55
|
+
|
|
56
|
+
Changing a meeting format restores the selected format, address, and access notes. Guests can return to the service selector when it's available. Contact details survive changes to the format or reserved time.
|
|
57
|
+
|
|
58
|
+
Expired meeting details and time reservations show a recovery action. Guests can recheck their meeting details or choose another time without re-entering their contact information. An active hold remains usable if its prepared meeting token expires, until the hold's own deadline.
|
|
59
|
+
|
|
60
|
+
Navigation is disabled while a reservation or booking request is running. Stale responses cannot reopen a previous step, and holds created after the widget is removed are released.
|
|
61
|
+
|
|
62
|
+
## API Functions
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import {
|
|
66
|
+
fetchBookingTypes, fetchBookingTypeDetails,
|
|
67
|
+
fetchAvailability, fetchAvailableDates,
|
|
68
|
+
createSlotHold, releaseSlotHold, createBooking,
|
|
69
|
+
prepareMeeting,
|
|
70
|
+
detectTimezone, formatTime, formatDate, formatDuration,
|
|
71
|
+
} from '@sonordev/site-kit/sync'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Types
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
interface BookingType {
|
|
78
|
+
slug: string; name: string; description?: string;
|
|
79
|
+
duration_minutes: number; color?: string;
|
|
80
|
+
location_type: 'virtual' | 'phone' | 'in_person' | 'custom';
|
|
81
|
+
price_cents?: number; currency?: string; is_active: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface BookingResult {
|
|
85
|
+
success: boolean
|
|
86
|
+
booking: {
|
|
87
|
+
id: string; confirmationCode: string; scheduledAt: string;
|
|
88
|
+
durationMinutes: number; hostName?: string; timezone: string;
|
|
89
|
+
}
|
|
90
|
+
cancelUrl: string; rescheduleUrl: string;
|
|
91
|
+
invitation?: 'google' | 'sonor' // who sends the guest's calendar invite
|
|
92
|
+
calendarLinks: { google: string; outlook: string; ics: string }
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Calendar invitations
|
|
97
|
+
|
|
98
|
+
Each booking gives the guest one calendar invitation, and `invitation` says who sends it:
|
|
99
|
+
|
|
100
|
+
- **`'google'`**: the host has Google Calendar connected, so Google invites the guest to the host's event. Sonor's emails don't attach a second invite, and the success screen says who the invite is coming from instead of showing calendar links. The Google and Outlook links build a separate event with no tie to that invitation, so a guest who clicked one would have two entries for the same meeting.
|
|
101
|
+
- **`'sonor'`**: there's no Google event for the booking, usually because the host hasn't connected a calendar. Sonor's confirmation email carries the invite, and the success screen shows the calendar links.
|
|
102
|
+
|
|
103
|
+
APIs older than the field don't send it; treat a missing `invitation` like `'sonor'`. `calendarLinks` is still sent for every booking so older widgets keep working. If you build your own success screen with `createBooking`, check `invitation` before you render `calendarLinks`.
|
|
104
|
+
|
|
105
|
+
## Features
|
|
106
|
+
|
|
107
|
+
- Automatic timezone detection
|
|
108
|
+
- Slot hold with expiry (prevents double-booking)
|
|
109
|
+
- Business hours support
|
|
110
|
+
- Multiple hosts per booking type
|
|
111
|
+
- Custom meeting URL / location
|
|
112
|
+
|
|
113
|
+
## Booking regression checks
|
|
114
|
+
|
|
115
|
+
Run `pnpm exec vitest run src/sync/booking-flow.test.ts` for expiry and server-error recovery checks. Start `pnpm exec vite --host 127.0.0.1 --port 5189`, then run `node test/meeting-flow/run.mjs` and `node test/meeting-flow/regression.mjs` for the real widget flows. The browser harness intercepts requests to a reserved test domain, so it creates no live bookings.
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { SitemapSync } from './chunk-R3TOKUDJ.js';
|
|
2
|
-
import './chunk-YVKRYQRG.js';
|
|
3
|
-
import './chunk-43OCZ3JA.js';
|
|
4
|
-
import './chunk-EKBEOXTH.js';
|
|
5
|
-
import './chunk-4WI3FU5L.js';
|
|
6
|
-
import './chunk-HPUXKMNR.js';
|
|
7
|
-
import './chunk-7ZHJIRM3.js';
|
|
8
|
-
import './chunk-PKBMQBKP.js';
|