@sonordev/site-kit 7.0.1 → 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-EMM2TKRE.js → AnalyticsProvider-XXWTFKJH.js} +4 -4
- package/dist/{ArticleViewTracker-RA64BGL6.js → ArticleViewTracker-V4KZB6QN.js} +3 -3
- package/dist/{BlocksPopup-D25RFNOV.js → BlocksPopup-JHGHB6XW.js} +4 -4
- package/dist/{ChatWidget-RYI7BMJJ.js → ChatWidget-CG32POI3.js} +5 -5
- package/dist/{EngageWidget-UKFCN33M.js → EngageWidget-LQMR4LEX.js} +4 -4
- package/dist/{FileField-MUHA7LZR.js → FileField-KUG3CKXG.js} +3 -3
- package/dist/{FormSpotlight-TCLPWPLL.js → FormSpotlight-FVNPOCU3.js} +1 -1
- package/dist/{FormStage-CNYLP6I6.js → FormStage-C7VKRURJ.js} +1 -1
- package/dist/{ManagedForm-7ZL5SKTO.js → ManagedForm-VLNJKV65.js} +6 -6
- package/dist/{ManagedNewsletterForm-33B4JLX7.js → ManagedNewsletterForm-KJEU23BV.js} +4 -4
- package/dist/{SignalCore-L5FVDHFE.js → SignalCore-K2O46QG7.js} +3 -3
- package/dist/{SiteDesignReporter-4JOFL4FP.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-QANVUXKH.js → chunk-42OXY4JV.js} +1 -1
- package/dist/{chunk-GYESATRY.js → chunk-56JNI463.js} +1 -1
- package/dist/{chunk-MV2MBTC3.js → chunk-5FBY2ZIH.js} +1 -1
- package/dist/{chunk-BMO3VGMR.js → chunk-7JIKGKWD.js} +7 -7
- package/dist/{chunk-QGHSMJKW.js → chunk-B6RZ2NRH.js} +1 -1
- package/dist/{chunk-OFOAHPUV.js → chunk-BEL7YFMC.js} +1 -1
- package/dist/{chunk-WATH55UY.js → chunk-BS7FWUOY.js} +1 -1
- package/dist/{chunk-FL4EPUWA.js → chunk-DKTSGYLM.js} +2 -2
- package/dist/{chunk-HGCK465A.js → chunk-GGD4P7UW.js} +1 -1
- package/dist/{chunk-FYBZ5SNP.js → chunk-GYY6ETGB.js} +1 -1
- package/dist/{chunk-CVTVNC2U.js → chunk-K5WZX776.js} +2 -2
- package/dist/{chunk-4IQ52CXL.js → chunk-LJZ3SUET.js} +2 -2
- package/dist/{chunk-P5J7VMQ3.js → chunk-O52CH273.js} +1 -1
- package/dist/{chunk-3KUUH2YP.js → chunk-OIETJKIL.js} +1 -1
- package/dist/{chunk-V6LSQRTH.js → chunk-P2GIIQH5.js} +1 -1
- package/dist/{chunk-4RMVXRBO.js → chunk-P72ZJRSX.js} +3 -3
- package/dist/{chunk-EGOD74PP.js → chunk-RU2RMTGT.js} +2 -2
- package/dist/{chunk-QZZIKMAT.js → chunk-SAUTJMK6.js} +1 -1
- package/dist/{chunk-T3MC4HOD.js → chunk-SWP36NCB.js} +1 -1
- package/dist/{chunk-5SEM2V4A.js → chunk-T4SY3FMN.js} +3 -3
- package/dist/{chunk-P4GRY6QP.js → chunk-ZRE4ZYEG.js} +1 -1
- package/dist/{chunk-UZN4ZYR2.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 +6 -6
- 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-7WKY4HXI.js +0 -8
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# Articles — `@sonordev/site-kit/articles`
|
|
2
|
+
|
|
3
|
+
## Publication routes and artwork (5.4.0)
|
|
4
|
+
|
|
5
|
+
Use one routing object for the stock components and their SEO helpers. Existing
|
|
6
|
+
sites keep `/article/slug` unless they opt in. `basePath` remains supported as a
|
|
7
|
+
metadata alias; `basePath` takes precedence when both are provided.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Article, ArticleList } from '@sonordev/site-kit/articles/server-ui'
|
|
11
|
+
import {
|
|
12
|
+
createPublicationRoutes, generateArticleMetadata, generateArticleSchema,
|
|
13
|
+
generateArticleSitemap, generateArticleStaticParams, generateRssFeed,
|
|
14
|
+
getArticle,
|
|
15
|
+
} from '@sonordev/site-kit/articles/server'
|
|
16
|
+
import type { PublicationRoutingOptions } from '@sonordev/site-kit/articles/server'
|
|
17
|
+
|
|
18
|
+
const siteUrl = 'https://example.com'
|
|
19
|
+
const routing = {
|
|
20
|
+
basePath: '/theforge',
|
|
21
|
+
includeCategoryInPath: true,
|
|
22
|
+
categoryPath: (slug: string) => `/theforge?category=${encodeURIComponent(slug)}`,
|
|
23
|
+
} satisfies PublicationRoutingOptions
|
|
24
|
+
|
|
25
|
+
// Cards, related stories and cluster links now use /theforge/category/slug.
|
|
26
|
+
const article = <Article slug="build-first" routing={routing} />
|
|
27
|
+
const archive = <ArticleList routing={routing} />
|
|
28
|
+
|
|
29
|
+
// Use these helpers from their corresponding Next.js route exports.
|
|
30
|
+
const metadata = await generateArticleMetadata('build-first', { siteUrl, ...routing })
|
|
31
|
+
const post = await getArticle('build-first')
|
|
32
|
+
const schema = post ? generateArticleSchema(post, { siteUrl, ...routing }) : null
|
|
33
|
+
// In app/theforge/[category]/[slug]/page.tsx, wrap it to pass the routing:
|
|
34
|
+
// export function generateStaticParams() { return generateArticleStaticParams(routing) }
|
|
35
|
+
const params = await generateArticleStaticParams(routing) // { category, slug }[]
|
|
36
|
+
const sitemap = await generateArticleSitemap(siteUrl, {
|
|
37
|
+
...routing,
|
|
38
|
+
includeCategories: false, // category filters don't need separate sitemap entries
|
|
39
|
+
includeClusters: false, // enable when your site implements cluster routes
|
|
40
|
+
})
|
|
41
|
+
const rss = await generateRssFeed({ siteUrl, siteName: 'The Forge', ...routing })
|
|
42
|
+
const href = createPublicationRoutes(routing).post({ slug: 'build-first', category: 'guides' })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`routing` also works on `PublicationLayout`, `PublicationSidebar`, `RelatedPosts`, `AuthorPage`,
|
|
46
|
+
`ClusterLandingPage`, and `ClusterNavigation`. To support another article shape,
|
|
47
|
+
provide `postPath: post => '/articles/' + encodeURIComponent(post.slug)`.
|
|
48
|
+
Callbacks return local paths; generated metadata, feeds, and sitemaps separately
|
|
49
|
+
honor a valid `canonical_url`. The same routes apply to Atom and generated
|
|
50
|
+
breadcrumb/cluster schemas. Supplied `schema`/`schema_json` objects stay intact.
|
|
51
|
+
|
|
52
|
+
For custom layouts, `resolveArticleArtwork(post, 'article')` prefers
|
|
53
|
+
`editorial_image`, preserving an empty decorative `editorial_image_alt`.
|
|
54
|
+
`resolveArticleArtwork(post, 'card')` keeps the complete `featured_image`. Older API
|
|
55
|
+
responses fall back to the featured image for both. The stock UI keeps generated
|
|
56
|
+
Sonor cards fully visible, while manually selected photos keep their existing
|
|
57
|
+
layout. Social metadata and RSS enclosures use the featured share card.
|
|
58
|
+
|
|
59
|
+
The stock article renders accessible table scroll regions automatically. Custom
|
|
60
|
+
renderers can call `wrapArticleTables(html)` and include `articleTableCss` within their
|
|
61
|
+
`.sk-article-content` scope. Both exports are available from `article/server` and
|
|
62
|
+
`article/server-ui`. The wrapper preserves the original table and is safe to apply
|
|
63
|
+
twice. It's a layout transform, not a sanitizer; keep your existing content trust
|
|
64
|
+
policy. Scrollbars and keyboard focus use `--sk-*` tokens.
|
|
65
|
+
|
|
66
|
+
## Import server components from `article/server-ui`
|
|
67
|
+
|
|
68
|
+
`Article`, `ArticleList`, `PublicationLayout`, `PublicationSidebar` and `RelatedPosts` are async
|
|
69
|
+
server components. The `@sonordev/site-kit/articles` barrel is stamped `'use client'`
|
|
70
|
+
at build time, and an async component inside a client module is not something Next
|
|
71
|
+
can render — the route returns a 500.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { Article, ArticleList } from '@sonordev/site-kit/articles/server-ui' // ✅
|
|
75
|
+
import { Article } from '@sonordev/site-kit/articles' // ❌ 500s
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The barrel still re-exports them for backwards compatibility and they will move out
|
|
79
|
+
in the next major. Reach for `@sonordev/site-kit/articles` only for genuine client
|
|
80
|
+
components: `TableOfContents`, `ArticleFAQ`, `AuthorCard`, `ServiceCallout`,
|
|
81
|
+
`NewsletterWidget`.
|
|
82
|
+
|
|
83
|
+
`NewsletterWidget` needs either an `onSubmit` callback or a `formSlug`
|
|
84
|
+
pointing at a managed Sonor form (newsletter routing) — with neither it
|
|
85
|
+
renders nothing rather than a form that discards emails. `Article` mounts a
|
|
86
|
+
childless `ArticleViewTracker` client island that counts real readers (one POST
|
|
87
|
+
per post per session, deferred to idle); it needs `SiteKitLayout`'s globals
|
|
88
|
+
and silently no-ops without them.
|
|
89
|
+
|
|
90
|
+
Data helpers live in `@sonordev/site-kit/articles/server`, which is `server-only` — a
|
|
91
|
+
client import there is a build error rather than a runtime one.
|
|
92
|
+
|
|
93
|
+
Sonor-managed article with SSG, topic clusters, E-E-A-T author profiles, and full SEO integration. Create posts in the Sonor dashboard — they appear on your site automatically.
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
### Articles Index
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// app/article/page.tsx
|
|
101
|
+
import { ArticleList, PublicationLayout } from '@sonordev/site-kit/articles/server-ui'
|
|
102
|
+
import { generatePublicationMetadata } from '@sonordev/site-kit/articles/server'
|
|
103
|
+
|
|
104
|
+
export async function generateMetadata() {
|
|
105
|
+
return generatePublicationMetadata({ siteName: 'My Site', siteUrl: 'https://example.com' })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function PublicationPage() {
|
|
109
|
+
return (
|
|
110
|
+
<PublicationLayout hero={{ title: 'The Forge', subtitle: 'Latest articles' }}>
|
|
111
|
+
<ArticleList showCategoryFilter showPagination />
|
|
112
|
+
</PublicationLayout>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Single Post
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// app/article/[slug]/page.tsx
|
|
121
|
+
import { Article } from '@sonordev/site-kit/articles/server-ui'
|
|
122
|
+
import {
|
|
123
|
+
generateArticleStaticParams,
|
|
124
|
+
generateArticleMetadata,
|
|
125
|
+
requireArticle,
|
|
126
|
+
} from '@sonordev/site-kit/articles/server'
|
|
127
|
+
|
|
128
|
+
export const generateStaticParams = generateArticleStaticParams
|
|
129
|
+
|
|
130
|
+
type Props = { params: Promise<{ slug: string }> }
|
|
131
|
+
|
|
132
|
+
export async function generateMetadata({ params }: Props) {
|
|
133
|
+
const { slug } = await params
|
|
134
|
+
await requireArticle(slug) // 404 for an unknown slug, before anything streams
|
|
135
|
+
return generateArticleMetadata(slug, { siteName: 'My Site', siteUrl: 'https://example.com' })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default async function Post({ params }: Props) {
|
|
139
|
+
const { slug } = await params
|
|
140
|
+
return <Article slug={slug} showRelated showToc showAuthor />
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**A missing post is a 404.** `requireArticle`, `generateArticleMetadata` and
|
|
145
|
+
`Article` all call Next's `notFound()` when the post doesn't exist, so a
|
|
146
|
+
mistyped or deleted post URL answers 404 with `noindex`. They used to render a
|
|
147
|
+
"Post Not Found" page on a 200 with an indexable title, which is a soft 404.
|
|
148
|
+
Calling `requireArticle` from `generateMetadata` is what guarantees the status:
|
|
149
|
+
metadata resolves before the page streams. To render your own missing-post
|
|
150
|
+
state instead, pass `notFound: false` to `generateArticleMetadata` (its
|
|
151
|
+
placeholder is marked `noindex`) and `notFound={false}` to `Article`.
|
|
152
|
+
|
|
153
|
+
**A post with its own social card.** If the post route has an
|
|
154
|
+
`opengraph-image.tsx` beside the page (see `@sonordev/site-kit/og/route`), pass
|
|
155
|
+
`images: false`:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
return generateArticleMetadata(slug, { siteName: 'My Site', images: false })
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Otherwise the featured image is declared as `openGraph.images`, and Next lets
|
|
162
|
+
declared images beat the file convention: the card never ships. `images: false`
|
|
163
|
+
leaves the keys out entirely (Next checks `hasOwnProperty('images')`, so even
|
|
164
|
+
`images: undefined` would hide the card). `sonor-setup doctor` flags a post
|
|
165
|
+
route that has a card but still declares the image.
|
|
166
|
+
|
|
167
|
+
### Topic Cluster Landing
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// app/article/topics/[slug]/page.tsx
|
|
171
|
+
// From `article/server-ui`, not `article` — it's an async server component that fetches
|
|
172
|
+
// the cluster, so it must stay out of the client-side `article` barrel.
|
|
173
|
+
import { ClusterLandingPage } from '@sonordev/site-kit/articles/server-ui'
|
|
174
|
+
|
|
175
|
+
export default function ClusterPage({ params }: { params: { slug: string } }) {
|
|
176
|
+
return <ClusterLandingPage slug={params.slug} basePath="/article" />
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Components
|
|
181
|
+
|
|
182
|
+
| Component | Import | Purpose |
|
|
183
|
+
|-----------|--------|---------|
|
|
184
|
+
| `Article` | `article/server-ui` | Single post with content, TOC, author, related |
|
|
185
|
+
| `ArticleList` | `article/server-ui` | Post grid with pagination and category filter |
|
|
186
|
+
| `PublicationLayout` | `article/server-ui` | Full layout with optional sidebar |
|
|
187
|
+
| `PublicationSidebar` | `article/server-ui` | Categories, recent posts, tags |
|
|
188
|
+
| `RelatedPosts` | `article/server-ui` | Related articles widget |
|
|
189
|
+
| `PublicationPage` | `article/server-ui` | Drop-in publication index page (layout + list) |
|
|
190
|
+
| `ArticlePage` | `article/server-ui` | Drop-in single-post page |
|
|
191
|
+
| `CategoryPage` | `article/server-ui` | Drop-in category archive page |
|
|
192
|
+
| `ClusterLandingPage` | `article/server-ui` | Topic cluster overview with pillar + support articles |
|
|
193
|
+
| `ClusterNavigation` | `article` | Breadcrumb-style cluster nav |
|
|
194
|
+
| `AuthorCard` | `article` | Author profile with E-E-A-T fields |
|
|
195
|
+
| `TableOfContents` | `article` | Auto-generated from H2-H4 headings |
|
|
196
|
+
| `ArticleFAQ` | `article` | FAQ section with schema |
|
|
197
|
+
| `ServiceCallout` | `article` | CTA callout for related services |
|
|
198
|
+
| `NewsletterWidget` | `article` | Email capture; needs `onSubmit` or `formSlug` |
|
|
199
|
+
|
|
200
|
+
## Server Functions (`article/server`)
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// Data fetching
|
|
204
|
+
getArticle(slug) // Single post with full data, or null
|
|
205
|
+
requireArticle(slug) // Same, but calls notFound() when there's no post
|
|
206
|
+
getAllArticleSlugs() // All published slugs (for generateStaticParams)
|
|
207
|
+
getArticleCategories() // Categories with post counts
|
|
208
|
+
getTopicCluster(slug) // Cluster with pillar + support articles
|
|
209
|
+
getTopicClusters() // All clusters
|
|
210
|
+
|
|
211
|
+
// Next.js integration
|
|
212
|
+
generateArticleStaticParams(routing?) // [{ slug }], or [{ category, slug }] with includeCategoryInPath
|
|
213
|
+
generateCategoryStaticParams() // Returns [{ category }]
|
|
214
|
+
generateAuthorStaticParams() // Returns [{ slug }]
|
|
215
|
+
// With no routing options, all three can be exported directly as
|
|
216
|
+
// `generateStaticParams`; the props Next passes are ignored.
|
|
217
|
+
generateArticleMetadata(slug, opts) // Next.js Metadata object; notFound() for a missing post.
|
|
218
|
+
// opts.images: false when the route has its own opengraph-image card
|
|
219
|
+
generatePublicationMetadata(opts) // Index page metadata
|
|
220
|
+
generateArticleCategoryMetadata(name, opts)
|
|
221
|
+
|
|
222
|
+
// Schema & SEO
|
|
223
|
+
generateArticleSchema(post, opts) // JSON-LD Article with FAQ
|
|
224
|
+
generateArticleListSchema() // JSON-LD for publication index
|
|
225
|
+
generateFaqSchema(items) // FAQ Page schema
|
|
226
|
+
generateArticleSitemap(siteUrl) // Sitemap entries for article
|
|
227
|
+
|
|
228
|
+
// Validation
|
|
229
|
+
validateArticleSeo(post) // Returns field-by-field SEO audit
|
|
230
|
+
validateSeoTitle(title, keyphrase?) // Title length + keyword checks
|
|
231
|
+
validateMetaDescription(desc) // Description length check
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Multi-site projects
|
|
235
|
+
|
|
236
|
+
One Sonor project can serve many domains (example.com plus its city
|
|
237
|
+
microsites), each with its own article. A post with no site is project-wide and
|
|
238
|
+
shows on every host; a post tagged `charlotte.example.com` shows only there.
|
|
239
|
+
|
|
240
|
+
Every article read sends the site host automatically, as `?site=` on GETs and a
|
|
241
|
+
`site` field on the related-posts and view-count POSTs. The host resolves from
|
|
242
|
+
`NEXT_PUBLIC_SITE_URL`, which every microsite already sets, so most sites
|
|
243
|
+
change nothing. To pin a host explicitly, pass `site`:
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
<ArticleList site="charlotte.example.com" /> // also Article, PublicationSidebar, PublicationLayout, RelatedPosts, ClusterLandingPage
|
|
247
|
+
await getArticle(slug, { site: 'charlotte.example.com' })
|
|
248
|
+
await getAllArticles({ site: 'charlotte.example.com' })
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`getAllArticleSlugs()` and `getAllAuthorSlugs()` take no arguments, so they can
|
|
252
|
+
still be exported as `generateStaticParams`. They always use
|
|
253
|
+
`NEXT_PUBLIC_SITE_URL`. Single-site projects and older API servers ignore
|
|
254
|
+
`site`.
|
|
255
|
+
|
|
256
|
+
## Article Props
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
interface ArticleProps {
|
|
260
|
+
slug: string
|
|
261
|
+
showRelated?: boolean // Related posts section
|
|
262
|
+
showToc?: boolean // Table of contents
|
|
263
|
+
showAuthor?: boolean // Author card
|
|
264
|
+
unstyled?: boolean // Skip default styles
|
|
265
|
+
notFound?: boolean // Default true: notFound() when the post is missing. false renders a message.
|
|
266
|
+
className?: string
|
|
267
|
+
children?: (props: { post, toc, related }) => ReactNode // Render prop
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## ArticleList Props
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
interface ArticleListProps {
|
|
275
|
+
category?: string // Filter by category slug
|
|
276
|
+
tag?: string // Filter by tag
|
|
277
|
+
author?: string // Filter by author
|
|
278
|
+
featured?: boolean // Featured posts only
|
|
279
|
+
search?: string // Search posts
|
|
280
|
+
page?: number // Default: 1
|
|
281
|
+
perPage?: number // Default: 12
|
|
282
|
+
orderBy?: 'published_at' | 'title' | 'view_count'
|
|
283
|
+
order?: 'asc' | 'desc'
|
|
284
|
+
showCategoryFilter?: boolean
|
|
285
|
+
showPagination?: boolean
|
|
286
|
+
unstyled?: boolean
|
|
287
|
+
className?: string
|
|
288
|
+
children?: (props: { posts, pagination, categories }) => ReactNode
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Key Types
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
interface Article {
|
|
296
|
+
slug: string; title: string; excerpt?: string; content: string;
|
|
297
|
+
featured_image?: string; author?: ArticleAuthor; category?: ArticleCategory;
|
|
298
|
+
tags?: string[]; meta_title?: string; meta_description?: string;
|
|
299
|
+
faq_items?: { question: string; answer: string }[];
|
|
300
|
+
article_type?: 'pillar' | 'support' | 'comparison' | 'faq' | 'glossary' | 'checklist';
|
|
301
|
+
cluster_slug?: string; reading_time?: number; // the API column ('X min read')
|
|
302
|
+
published_at?: string; status: 'draft' | 'published' | 'scheduled' | 'archived';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
interface ArticleAuthor {
|
|
306
|
+
name: string; slug: string; bio?: string; avatar_url?: string;
|
|
307
|
+
title?: string; credentials?: string[]; expertise_areas?: string[];
|
|
308
|
+
years_experience?: number; is_subject_matter_expert?: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
interface TopicCluster {
|
|
312
|
+
cluster_name: string; cluster_slug: string; core_topic: string;
|
|
313
|
+
geo_target?: string; target_service_page?: string; article_count: number;
|
|
314
|
+
pillar: Article | null; supports: Article[];
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Styling
|
|
319
|
+
|
|
320
|
+
Components use `.sk-article-*` and `.sk-article-list-*` classes. Import default styles:
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Or use `unstyled` prop + `children` render prop for complete control.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Commerce — `@sonordev/site-kit/commerce`
|
|
2
|
+
|
|
3
|
+
Products, services, classes, events, and checkout flows — all managed from the Sonor dashboard.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
| Component | Purpose |
|
|
8
|
+
|-----------|---------|
|
|
9
|
+
| `OfferingCard` | Card display for any offering type |
|
|
10
|
+
| `OfferingList` | Grid/list of offerings with filtering |
|
|
11
|
+
| `ProductPage` / `ProductDetail` | Full product page with gallery, sizes, variants |
|
|
12
|
+
| `ProductGrid` / `ProductEmbed` | Product showcase widgets |
|
|
13
|
+
| `SizeChart` | Clothing size chart display |
|
|
14
|
+
| `EventTile` / `UpcomingEvents` | Event display widgets |
|
|
15
|
+
| `EventCalendar` / `EventModal` | Calendar view + detail modal |
|
|
16
|
+
| `EventEmbed` / `EventsWidget` | Embeddable event components |
|
|
17
|
+
| `CheckoutForm` | Payment checkout flow |
|
|
18
|
+
| `RegistrationForm` | Event/class registration |
|
|
19
|
+
| `CalendarView` | Date-based calendar component |
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { OfferingList, ProductPage } from '@sonordev/site-kit/commerce'
|
|
25
|
+
|
|
26
|
+
// List all offerings
|
|
27
|
+
export default function ShopPage() {
|
|
28
|
+
return <OfferingList type="product" />
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Single product page
|
|
32
|
+
export default function Product({ params }) {
|
|
33
|
+
return <ProductPage slug={params.slug} />
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API Functions
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import {
|
|
41
|
+
fetchOfferings, fetchOffering,
|
|
42
|
+
fetchProducts, fetchProductBySlug,
|
|
43
|
+
fetchUpcomingEvents, fetchNextEvent,
|
|
44
|
+
fetchCategories, fetchServices,
|
|
45
|
+
createCheckoutSession, createPaymentIntent,
|
|
46
|
+
validateDiscountCode, registerForEvent,
|
|
47
|
+
fetchShippingRates, validateAddress,
|
|
48
|
+
} from '@sonordev/site-kit/commerce'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Offering Types
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
type OfferingType = 'product' | 'service' | 'class' | 'event' | 'subscription'
|
|
55
|
+
|
|
56
|
+
interface CommerceOffering {
|
|
57
|
+
name: string; slug: string; type: OfferingType;
|
|
58
|
+
description?: string; featured_image_url?: string;
|
|
59
|
+
price_type: 'fixed' | 'variable' | 'quote' | 'free';
|
|
60
|
+
price?: number; compare_at_price?: number; currency: string;
|
|
61
|
+
track_inventory?: boolean; inventory_count?: number;
|
|
62
|
+
is_clothing?: boolean; size_chart?: SizeChart;
|
|
63
|
+
duration_minutes?: number; capacity?: number;
|
|
64
|
+
location?: string; is_virtual?: boolean;
|
|
65
|
+
schedules?: CommerceSchedule[]; variants?: CommerceVariant[];
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Per-category styling (`data-category` / `data-offering-type`)
|
|
70
|
+
|
|
71
|
+
Event and offering surfaces expose the offering's category slug and type as
|
|
72
|
+
data attributes, so sites can theme categories with plain CSS — no custom
|
|
73
|
+
components needed. `data-category` is only present when the offering has a
|
|
74
|
+
category; `data-offering-type` is always present.
|
|
75
|
+
|
|
76
|
+
Elements carrying the attributes:
|
|
77
|
+
|
|
78
|
+
- `CalendarView` / `EventCalendar` — `.site-kit-calendar-event` chips (both
|
|
79
|
+
`title` and `image` display modes) and the image-mode wrapper
|
|
80
|
+
- `EventTile` (both variants — also covers `UpcomingEvents` and `EventEmbed`)
|
|
81
|
+
- `EventsWidget` — `.site-kit-event-card` in list and grid views
|
|
82
|
+
- `OfferingCard` (all variants — also covers `OfferingList` / `ProductGrid`)
|
|
83
|
+
|
|
84
|
+
```css
|
|
85
|
+
/* e.g. two color schemes on one calendar: PTO vs school district */
|
|
86
|
+
.site-kit-calendar-event[data-category="pto"] {
|
|
87
|
+
background: rgba(16, 185, 129, 0.15);
|
|
88
|
+
color: #059669;
|
|
89
|
+
}
|
|
90
|
+
.site-kit-calendar-event[data-category="school-district"] {
|
|
91
|
+
background: rgba(59, 130, 246, 0.15);
|
|
92
|
+
color: #2563eb;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Size Charts (clothing products)
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
interface SizeChart {
|
|
100
|
+
unit: 'inches' | 'cm'
|
|
101
|
+
fit_note?: string // e.g., "Runs small. Order one size up."
|
|
102
|
+
measurements: string[] // ['Chest', 'Length', 'Sleeve']
|
|
103
|
+
rows: Array<{
|
|
104
|
+
size: string // 'S', 'M', 'L', 'XL'
|
|
105
|
+
values: number[] // Primary unit values
|
|
106
|
+
values_alt?: number[] // Auto-converted alternate unit
|
|
107
|
+
}>
|
|
108
|
+
}
|
|
109
|
+
```
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @sonordev/site-kit/cta-bar
|
|
2
|
+
|
|
3
|
+
The Liquid Glass mobile CTA bar (6.1.0). A floating frosted capsule that
|
|
4
|
+
keeps a site's one or two highest-intent actions a thumb away on phones.
|
|
5
|
+
|
|
6
|
+
It replaces every hand-rolled sticky mobile bar in the fleet. Those eleven
|
|
7
|
+
copies had each solved part of the same problem set; this one does all of it:
|
|
8
|
+
|
|
9
|
+
| Behaviour | Before | Here |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Hide while the form it points at is on screen | two sites, hand-built | `hideOver` |
|
|
12
|
+
| Stay off the hero until the hero CTA scrolls away | one site | `showAfter` |
|
|
13
|
+
| Get out from under the Echo launcher | two sites (`body:has`, a 72px dead corner) | automatic |
|
|
14
|
+
| Ride out the iOS toolbar collapsing | one site (a GSAP tween) | `--sk-vv-layout-gap` |
|
|
15
|
+
| Get out of the way of the keyboard | nobody | `hideWhileTyping` |
|
|
16
|
+
| Tell Sonor which action converts | one site (hand-wired) | `cta_click` event |
|
|
17
|
+
|
|
18
|
+
## Use it
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// app/layout.tsx (a server component)
|
|
22
|
+
import Link from 'next/link'
|
|
23
|
+
import { Phone, ClipboardCheck } from 'lucide-react'
|
|
24
|
+
import { CtaBar, CtaBarAction } from '@sonordev/site-kit/cta-bar'
|
|
25
|
+
|
|
26
|
+
<SiteKitLayout>
|
|
27
|
+
<Header />
|
|
28
|
+
<main>{children}</main>
|
|
29
|
+
<Footer />
|
|
30
|
+
<CtaBar label="Call or request a quote" hideOver="#quote">
|
|
31
|
+
<CtaBarAction href="tel:+15135550100" variant="secondary" icon={<Phone />}>
|
|
32
|
+
Call now
|
|
33
|
+
</CtaBarAction>
|
|
34
|
+
<CtaBarAction as={Link} href="/quote" icon={<ClipboardCheck />}>
|
|
35
|
+
Free quote
|
|
36
|
+
</CtaBarAction>
|
|
37
|
+
</CtaBar>
|
|
38
|
+
</SiteKitLayout>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rules:
|
|
42
|
+
|
|
43
|
+
- **Render it once per page:** at the layout root for a site-wide bar, or
|
|
44
|
+
inside the page for a page-specific one. It's a labelled `<aside>`, valid
|
|
45
|
+
at either depth; the kit's axe gate covers both placements.
|
|
46
|
+
- **Never inside a blurred header.** `backdrop-filter` on an ancestor becomes
|
|
47
|
+
the containing block for this fixed bar and clips it.
|
|
48
|
+
- **Delete the site's own bar and its compensating `padding-bottom`.** The
|
|
49
|
+
kit renders a spacer that reserves the bar's height at the end of the page.
|
|
50
|
+
- **Better: pad the footer instead of adding a strip after it.** A spacer
|
|
51
|
+
after a dark footer is a blank band in the page colour. Pass
|
|
52
|
+
`spacer={false}` and let the footer's own background run under the bar:
|
|
53
|
+
|
|
54
|
+
```css
|
|
55
|
+
footer { padding-bottom: calc(2rem + var(--sk-cta-bar-space, 0px)); }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`--sk-cta-bar-space` is set on `<html>` only while a bar is present and
|
|
59
|
+
below its breakpoint, so desktop and bar-less pages get `0px`.
|
|
60
|
+
- **Delete any `--sk-echo-offset-bottom` rule written for the old bar.** The
|
|
61
|
+
kit sets it while the bar is on screen.
|
|
62
|
+
|
|
63
|
+
`CtaBar` and `CtaBarAction` are plain components with no hooks, so a server
|
|
64
|
+
layout can pass `as={Link}` and Link children without crossing a client
|
|
65
|
+
boundary. The only client code is a childless behaviour island the bar
|
|
66
|
+
mounts itself (about 4.4 KB gzipped for the whole module, glass and
|
|
67
|
+
analytics included).
|
|
68
|
+
|
|
69
|
+
## `<CtaBar>`
|
|
70
|
+
|
|
71
|
+
| Prop | Default | |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `label` | `"Quick actions"` | Accessible name of the landmark. |
|
|
74
|
+
| `breakpoint` | `"lg"` | Hidden at this width and up: `sm` 640, `md` 768, `lg` 1024, `xl` 1280, `none` = every width. |
|
|
75
|
+
| `layout` | `"fill"` | `fill` stretches actions across the capsule. `fit` hugs a single action in a centred capsule (the old floating "Request a quote" button). |
|
|
76
|
+
| `showAfter` | | Selector. Hidden until that element scrolls off the top (the hero CTA). Server-rendered hidden, so it never slides in over the hero during hydration. No match = shown. |
|
|
77
|
+
| `hideOver` | | Selector or selectors. Steps aside while any match is on screen: the form it points at, the footer. |
|
|
78
|
+
| `hideWhileTyping` | `true` | Hidden while a text field has focus, so it never sits on the keyboard. |
|
|
79
|
+
| `compactOnScroll` | `true` | Tightens while scrolling down; an icon-bearing secondary action folds to its icon. Scrolling up restores it. |
|
|
80
|
+
| `echoClearance` | `true` | Lifts the Echo launcher above the bar while the bar is on screen, below the breakpoint only. |
|
|
81
|
+
| `spacer` | `true` | Reserves the bar's height at the end of the page. |
|
|
82
|
+
| `track` | `true` | Sends `cta_click` (`category: engagement`, `label`, `properties.href`, `properties.variant`, `properties.location = "cta_bar"`) through the standalone analytics dispatch. |
|
|
83
|
+
|
|
84
|
+
Client navigation re-finds `showAfter` and `hideOver` targets on each route.
|
|
85
|
+
|
|
86
|
+
## `<CtaBarAction>`
|
|
87
|
+
|
|
88
|
+
| Prop | Default | |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `as` | `a` with an `href`, else `button` | Any element or component, e.g. Next's `Link`. Polymorphic since 6.1.3: the action then takes that component's own props (required ones included), so `<CtaBarAction as={ScheduleTourButton} values={unit}>` type-checks and a missing required prop doesn't. |
|
|
91
|
+
| `variant` | `"primary"` | `primary`: solid brand. `secondary`: tinted glass. `plain`: layout only, bring your own classes. |
|
|
92
|
+
| `icon` | | Leading icon, hidden from assistive tech. Lucide icons are sized to 18px. |
|
|
93
|
+
| `collapse` | secondary + icon | Fold to icon-only while compact. The label stays in the accessible name. |
|
|
94
|
+
|
|
95
|
+
Everything else (`href`, `onClick`, `target`, `aria-*`, `data-*`) passes
|
|
96
|
+
through. A `button` without an explicit `type` gets `type="button"`.
|
|
97
|
+
|
|
98
|
+
## Theme it
|
|
99
|
+
|
|
100
|
+
Colours and geometry are custom properties. Set them on `:root` so the bar,
|
|
101
|
+
its spacer and the Echo clearance all read the same values.
|
|
102
|
+
|
|
103
|
+
```css
|
|
104
|
+
:root {
|
|
105
|
+
--sk-cta-primary-bg: var(--brand-primary); /* default: --sk-primary, then #2563eb */
|
|
106
|
+
--sk-cta-primary-text: #fff;
|
|
107
|
+
--sk-cta-bar-text: var(--ink-900); /* secondary label colour */
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**A dark bar: scope the tint to the bar, not `:root`.** The Echo window
|
|
112
|
+
reads the same `--sk-glass-tint`, and its text follows `--sk-text-primary`
|
|
113
|
+
(dark by default). A dark tint on `:root` puts that dark text on dark glass.
|
|
114
|
+
Either scope the bar's colours to the bar:
|
|
115
|
+
|
|
116
|
+
```css
|
|
117
|
+
.sk-cta-bar {
|
|
118
|
+
--sk-glass-tint: #0b0a09;
|
|
119
|
+
--sk-cta-bar-text: #fff;
|
|
120
|
+
--sk-cta-primary-bg: #fff;
|
|
121
|
+
--sk-cta-primary-text: #0b0a09;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
or theme the whole kit dark with `--sk-bg` and `--sk-text-primary` on
|
|
126
|
+
`:root`, which both surfaces (and site-kit forms) read.
|
|
127
|
+
|
|
128
|
+
| Token | Default |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `--sk-cta-bar-inset` | `12px` from the screen edges |
|
|
131
|
+
| `--sk-cta-bar-padding` | `6px` between glass and buttons |
|
|
132
|
+
| `--sk-cta-bar-action-height` | `48px` (44px tap target minimum; don't go lower) |
|
|
133
|
+
| `--sk-cta-bar-max-width` | `520px` |
|
|
134
|
+
| `--sk-cta-bar-z` | `40` |
|
|
135
|
+
| `--sk-cta-bar-text` | `--sk-text-primary`, then `#111827` |
|
|
136
|
+
| `--sk-cta-primary-bg` / `--sk-cta-primary-text` | `--sk-primary` / `#fff` |
|
|
137
|
+
| `--sk-cta-secondary-bg` / `--sk-cta-secondary-text` | 8% of the text colour / the text colour |
|
|
138
|
+
| `--sk-glass-*` | the shared glass recipe (`--sk-glass-tint`, `--sk-glass-opacity`, `--sk-glass-blur`, `--sk-glass-saturate`) |
|
|
139
|
+
|
|
140
|
+
## The glass
|
|
141
|
+
|
|
142
|
+
The surface is the kit's one Liquid Glass recipe (`src/shared/glass.tsx`),
|
|
143
|
+
the same material as the Echo launcher and chat window, so the bar and the
|
|
144
|
+
chat always match. The tint is mostly opaque (72%): the tint carries the
|
|
145
|
+
contrast, and the blur only softens what shows through. Browsers without
|
|
146
|
+
`backdrop-filter`, and visitors who ask their OS for reduced transparency
|
|
147
|
+
or more contrast, get the same surface solid. Reduced motion turns the
|
|
148
|
+
transitions off. JS-off visitors get the bar, visible, through
|
|
149
|
+
`@media (scripting: none)`.
|
|
150
|
+
|
|
151
|
+
On phones where the layout viewport runs taller than the visible one
|
|
152
|
+
(in-app browsers, toolbar animations), mount `VisualViewportGap` from
|
|
153
|
+
`@sonordev/site-kit/client`; the bar already adds `--sk-vv-layout-gap` to
|
|
154
|
+
its `bottom`.
|