@soloworks/smking-next 0.21.1 → 0.21.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @soloworks/smking-next
2
2
 
3
+ ## 0.21.2 — 2026-07-02
4
+
5
+ **CMS taxonomy page types caught up with the SaaS catalog (types only — no runtime change).**
6
+
7
+ The CMS page model now includes the `taxonomy` content type used by protected
8
+ category and tag index pages. This keeps the published SDK type surface aligned
9
+ with the SaaS block contract after the taxonomy-page normalization work.
10
+
3
11
  ## 0.21.1 — 2026-06-12
4
12
 
5
13
  **Webhook replay protection (security review M6).**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soloworks/smking-next",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "description": "AI-native SEO (AEO) for Next.js — auto-inject JSON-LD, FAQ, AI summary, and SEO metadata so AI crawlers (ChatGPT, Perplexity, Google AI) can cite your pages.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/sillyleo/smking/tree/main/packages/smking-next",
package/src/cms-blocks.ts CHANGED
@@ -29,15 +29,27 @@
29
29
 
30
30
  export interface HeroProps {
31
31
  title: string;
32
+ /** Generic visible meta lines. Legacy drafts may still also carry subtitle. */
33
+ line1?: string;
34
+ line2?: string;
35
+ /** When true, render the backend page publish date. */
36
+ showPublishedAt?: boolean;
37
+ /** ISO or display date injected from page metadata at publish time. */
38
+ publishedAt?: string;
39
+ /** Legacy single-line fallback for SDK Mode B consumers. */
32
40
  subtitle?: string;
33
41
  image?: { url: string; alt: string };
34
42
  cta?: { label: string; href: string };
35
43
  /** Header layout — omitted = the original centred hero; "start" = the
36
- * Apple-newsroom-style left-aligned header; "cover" = title-only banner
37
- * over a full-bleed background image (Apple services index). Visuals ship
38
- * via bodyHtml; this mirrors the node attr for SDK Mode B (per-block)
39
- * consumers. */
40
- align?: "center" | "start" | "cover";
44
+ * Apple-newsroom-style left-aligned header; "cover" = title-only banner over
45
+ * a full-bleed background image with a dark scrim (Apple services index);
46
+ * "cover-plain" = the same banner WITHOUT the scrim (image shown untouched).
47
+ * Visuals ship via bodyHtml; this mirrors the node attr for SDK Mode B
48
+ * (per-block) consumers. */
49
+ align?: "center" | "start" | "cover" | "cover-plain";
50
+ /** Crop ratio for cover heroes and the optional hero image.
51
+ * Omitted = auto, preserving the original image ratio. */
52
+ aspectRatio?: "auto" | "16:9" | "4:3";
41
53
  }
42
54
 
43
55
  export interface ArticleProps {
@@ -72,7 +84,26 @@ export interface NavSnapshotEntry {
72
84
  featuredImageUrl: string | null;
73
85
  /** ISO string. */
74
86
  publishedAt: string | null;
75
- contentType: "article" | "landing" | "listing" | null;
87
+ /** ISO string. Present when the article is manually pinned in CMS feeds. */
88
+ pinnedAt?: string | null;
89
+ contentType: "article" | "landing" | "listing" | "taxonomy" | null;
90
+ }
91
+
92
+ export type SearchQuickLinkSource = "category-by-path" | "tag" | "url";
93
+
94
+ /**
95
+ * Optional links shown beside the search trigger on desktop and inside the
96
+ * search panel on small screens. Category/tag links keep their semantic source
97
+ * + path so publish-time materialization can bake the customer's CMS mount
98
+ * prefix into `href`; URL links carry the author's literal `href`.
99
+ */
100
+ export interface SearchQuickLink {
101
+ label: string;
102
+ source: SearchQuickLinkSource;
103
+ /** Category slug path or flat tag slug. Not used for arbitrary URLs. */
104
+ path?: string;
105
+ /** Materialized href for category/tag links, or the author-entered URL. */
106
+ href?: string;
76
107
  }
77
108
 
78
109
  /**
@@ -88,6 +119,11 @@ export interface SearchProps {
88
119
  placeholder?: string;
89
120
  /** Optional heading above the search input. */
90
121
  heading?: string;
122
+ /** Content column vs wide breakout (default wide). Mirrors the media
123
+ * blocks (image/embed). */
124
+ widthMode?: "content" | "wide";
125
+ /** Optional quick links adjacent to search. */
126
+ links?: SearchQuickLink[];
91
127
  /** Publish-time materialized snapshot — every published page on the site. */
92
128
  snapshot?: NavSnapshotEntry[];
93
129
  }
@@ -95,23 +131,74 @@ export interface SearchProps {
95
131
  export interface RecentPostsProps extends ModuleHeader {
96
132
  /** Author-set number of latest posts to show (1..50). */
97
133
  limit: number;
134
+ /** Content column vs wide breakout (default content). Mirrors the media
135
+ * blocks. */
136
+ widthMode?: "content" | "wide";
98
137
  /** Publish-time materialized snapshot — whole-site newest-first. */
99
138
  snapshot?: NavSnapshotEntry[];
100
139
  }
101
140
 
141
+ /** One author-picked source for nav-related-posts. Same shape as
142
+ * `CategoryIndexItem` (a category path OR a tag), reused so the source picker
143
+ * UI stays identical across blocks. */
144
+ export interface RelatedPostsSource {
145
+ /** Slug prefix (category-by-path) or taxonomy slug (tag), per `source`. */
146
+ path: string;
147
+ /** Display name (only used by the editor chip; resolver ignores it). */
148
+ label: string;
149
+ /** Omitted = `category-by-path` (back-compat, mirrors CategoryIndexItem). */
150
+ source?: "category-by-path" | "tag";
151
+ }
152
+
102
153
  export interface RelatedPostsProps extends ModuleHeader {
154
+ /** Authoring mode before materialization. Published blocks carry resolved
155
+ * `heading`; SDK consumers can ignore this field. */
156
+ headingMode?: "global" | "custom";
103
157
  /** Author-set number of related posts to show (1..50). */
104
158
  limit: number;
105
- /** Publish-time materialized snapshot — same-tag, ranked by overlap. */
159
+ /** Content column vs wide breakout (default content). Mirrors the media
160
+ * blocks. */
161
+ widthMode?: "content" | "wide";
162
+ /** Author-picked sources. Empty/omitted → auto-fill from the CURRENT page's
163
+ * category, derived from its slug at publish time. When set, related posts
164
+ * are the latest across ALL picked sources (OR), newest-first, excluding the
165
+ * current page. */
166
+ sources?: RelatedPostsSource[];
167
+ /** Publish-time materialized snapshot. */
106
168
  snapshot?: NavSnapshotEntry[];
107
169
  }
108
170
 
109
- /** One author-selected category in a nav-category-index block. */
171
+ /** One tag attached to the current article, materialized at publish time. */
172
+ export interface ArticleTagSnapshot {
173
+ slug: string;
174
+ name: string;
175
+ /** Virtual tag archive href, e.g. `/blog/tag/tofu-life`. */
176
+ href: string;
177
+ }
178
+
179
+ /**
180
+ * `article-tags` block — renders the current article's CMS tags as chips.
181
+ * The tags are page metadata, not authored inside the block, so `snapshot`
182
+ * is filled at publish time from `content_taxonomies`.
183
+ */
184
+ export interface ArticleTagsProps {
185
+ /** Optional section heading. Omitted / empty means no heading. */
186
+ heading?: string;
187
+ /** Publish-time materialized current-article tags. */
188
+ snapshot?: ArticleTagSnapshot[];
189
+ }
190
+
191
+ /** One author-selected category OR tag in a nav-category-index block. */
110
192
  export interface CategoryIndexItem {
111
- /** Slug prefix identifying the category (e.g. `blog/seo`). */
193
+ /** Slug prefix (category-by-path) or taxonomy slug (tag), per `source`.
194
+ * e.g. category `blog/seo`, tag `announcements`. */
112
195
  path: string;
113
- /** Display name on the card (slug leaf by default; author-editable). */
196
+ /** Display name on the card (slug leaf / tag name by default; author-editable). */
114
197
  label: string;
198
+ /** Source discriminator (mirrors nav-taxonomy-list). Omitted =
199
+ * `category-by-path` — back-compat: pre-tag nodes carry no source. A `tag`
200
+ * card shows that tag's latest post + links to its archive. */
201
+ source?: "category-by-path" | "tag";
115
202
  }
116
203
 
117
204
  /**
@@ -126,15 +213,30 @@ export interface CategoryCardSnapshot {
126
213
  }
127
214
 
128
215
  export interface CategoryIndexProps extends ModuleHeader {
216
+ /** `auto` = listing categories directly under `autoBasePath` at publish
217
+ * time; `manual` = exactly the picked items below. Omitted = back-compat:
218
+ * items present means manual, otherwise auto. */
219
+ mode?: "auto" | "manual";
220
+ /** Auto mode source. Omitted = category hierarchy, for back-compat. */
221
+ autoSource?: "category-by-path" | "tag";
222
+ /** Auto mode source hierarchy. Empty string means root-level listing
223
+ * categories; any other value is a listing slug whose direct child listing
224
+ * pages should be shown. Omitted falls back to root for new materialization. */
225
+ autoBasePath?: string;
129
226
  items: CategoryIndexItem[];
130
227
  /** Card layout — omitted/"cards" = category cards (archive links);
131
228
  * "featured" = Apple-newsroom services-index tiles where each card IS the
132
229
  * category's latest article. Visuals ship via bodyHtml; this mirrors the
133
230
  * node attr for SDK Mode B (per-block) consumers. */
134
231
  layout?: "cards" | "featured";
232
+ /** Content column vs wide breakout. Omitted = layout-derived default
233
+ * (cards → content, featured → wide); set explicitly to override. */
234
+ widthMode?: "content" | "wide";
135
235
  snapshot?: CategoryCardSnapshot[];
136
236
  }
137
237
 
238
+ export type MediaAspectRatio = "auto" | "16:9" | "4:3" | "square";
239
+
138
240
  /**
139
241
  * `image` block — a single figure (image + optional caption). `widthMode`
140
242
  * controls the breakout (content = text column, wide = up to 1024px centred
@@ -146,6 +248,10 @@ export interface ImageProps {
146
248
  alt?: string;
147
249
  caption?: string;
148
250
  widthMode?: "content" | "wide";
251
+ /** Plain image vs soft neutral tonal surface. */
252
+ styleVariant?: "plain" | "tonal";
253
+ /** Omitted / auto preserves the original image ratio. Fixed values crop. */
254
+ aspectRatio?: MediaAspectRatio;
149
255
  }
150
256
 
151
257
  /** One slide in a `slideshow` block. */
@@ -168,17 +274,51 @@ export interface SlideshowProps {
168
274
  aspectRatio?: "16:9" | "4:3";
169
275
  }
170
276
 
277
+ /** One image in a `collage` block. Width/height are optional natural
278
+ * dimensions saved at authoring time so the auto layout can keep row
279
+ * proportions stable without loading images first. */
280
+ export interface CollageImageProps {
281
+ url: string;
282
+ alt?: string;
283
+ width?: number;
284
+ height?: number;
285
+ }
286
+
287
+ /**
288
+ * `collage` block — a 1–6 image composition. `auto` uses natural image
289
+ * ratios and the wide breakout; `row-grid` keeps each row at one height while
290
+ * allowing different rows to have different proportions. Content width forces
291
+ * `row-grid` at render time.
292
+ */
293
+ export interface CollageProps {
294
+ images?: CollageImageProps[];
295
+ widthMode?: "content" | "wide";
296
+ mode?: "auto" | "row-grid";
297
+ }
298
+
299
+ export type SocialSharePlatform =
300
+ | "threads"
301
+ | "instagram"
302
+ | "facebook"
303
+ | "line"
304
+ | "copy";
305
+
171
306
  /**
172
- * `social-share` block — X / Facebook / LinkedIn share links + copy-link. The
173
- * platforms are fixed; `shareUrl` is the optional no-JS fallback target (the
174
- * `<smking-share>` web component overrides every link from window.location at
175
- * runtime, so the live share always points at the real current page).
307
+ * `social-share` block — Threads / Instagram / Facebook / LINE share links +
308
+ * copy-link.
309
+ * Platform visibility is site-wide; `shareUrl` is the optional no-JS fallback
310
+ * target (the `<smking-share>` web component overrides every link from
311
+ * window.location at runtime, so the live share always points at the real
312
+ * current page).
176
313
  */
177
314
  export interface SocialShareProps {
178
315
  shareUrl?: string;
179
- /** Heading above the row. Omitted = "Share article" default at render
180
- * time; empty string = bare icon row (Apple-newsroom header style). */
316
+ /** Global mode reads the site-wide share title. Custom mode reads `label`.
317
+ * Omitted = global; empty resolved title = bare icon row. */
318
+ headingMode?: "global" | "custom";
181
319
  label?: string;
320
+ /** Site-wide platform visibility snapshot baked into published output. */
321
+ platforms?: SocialSharePlatform[];
182
322
  }
183
323
 
184
324
  export interface NavTaxonomyListProps extends ModuleHeader {
@@ -196,10 +336,115 @@ export interface NavTaxonomyListProps extends ModuleHeader {
196
336
  * safety cap. Omitted (not false) when off — back-compat shape. */
197
337
  showAll?: boolean;
198
338
  layout: NavLayout;
339
+ /** Content column vs wide breakout (default content). Mirrors the media
340
+ * blocks. */
341
+ widthMode?: "content" | "wide";
199
342
  /** Publish-time materialized snapshot. */
200
343
  snapshot?: NavSnapshotEntry[];
201
344
  }
202
345
 
346
+ /** One button in a `button-group` block. */
347
+ export interface ButtonItemProps {
348
+ label: string;
349
+ href: string;
350
+ /** Apple-style looks: `filled` = primary capsule, `tinted` = soft primary
351
+ * wash, `outline` = bordered. Omitted = filled. */
352
+ variant?: "filled" | "tinted" | "outline";
353
+ }
354
+
355
+ /**
356
+ * `button-group` block — a row of Apple-style capsule CTAs. Buttons wrap on
357
+ * narrow screens; `align` positions the row. Empty `buttons` is valid (an
358
+ * unconfigured block renders nothing on the customer side).
359
+ */
360
+ export interface ButtonGroupProps {
361
+ buttons?: ButtonItemProps[];
362
+ align?: "start" | "center";
363
+ }
364
+
365
+ /**
366
+ * `embed` block — a privacy-friendly YouTube embed (youtube-nocookie
367
+ * iframe). `url` accepts any YouTube form (watch / youtu.be / shorts /
368
+ * embed); the video id is resolved at render time. No url / unrecognised
369
+ * url = renders nothing on the customer side. `widthMode` matches the other
370
+ * media blocks (content column vs wide breakout).
371
+ */
372
+ export interface EmbedProps {
373
+ url?: string;
374
+ caption?: string;
375
+ widthMode?: "content" | "wide";
376
+ }
377
+
378
+ /**
379
+ * `media-text` block — Apple-style feature row: image on one side, RICH
380
+ * EDITABLE text on the other. Unlike the attr-panel blocks, the text is
381
+ * real Plate content (the node's children — headings/paragraphs edited
382
+ * inline); `html` carries its serialized form for Mode B consumers, while
383
+ * Mode A ships the full markup inside bodyHtml as usual.
384
+ */
385
+ export interface MediaTextProps {
386
+ imageUrl?: string;
387
+ imageAlt?: string;
388
+ /** Which side the IMAGE sits on (md+ screens; stacks on mobile). */
389
+ side?: "left" | "right";
390
+ /** Content column vs wide breakout (default wide). Mirrors the media
391
+ * blocks. */
392
+ widthMode?: "content" | "wide";
393
+ /** Plain row vs soft neutral tonal surface. */
394
+ styleVariant?: "plain" | "tonal";
395
+ /** Omitted / auto preserves the original image ratio. Fixed values crop. */
396
+ aspectRatio?: MediaAspectRatio;
397
+ /** Serialized text-column HTML (class-stripped, same treatment as
398
+ * `article`). */
399
+ html: string;
400
+ }
401
+
402
+ /**
403
+ * One slot in a `latest-news` block — sourced independently from EITHER a
404
+ * specific article (by slug) or a category (shows that category's latest
405
+ * post). The discriminator mirrors `nav-taxonomy-list`'s source pattern.
406
+ */
407
+ export type LatestNewsSlot =
408
+ | { kind: "article"; slug: string }
409
+ | {
410
+ kind: "category";
411
+ path: string;
412
+ label: string;
413
+ /** Source discriminator (mirrors nav-taxonomy-list). Omitted =
414
+ * `category-by-path` — back-compat. A `tag` slot shows the latest post
415
+ * carrying that tag. */
416
+ source?: "category-by-path" | "tag";
417
+ };
418
+
419
+ /**
420
+ * One materialized `latest-news` card: the resolved article entry (the
421
+ * article itself for an `article` slot, or the category's latest post for a
422
+ * `category` slot) plus an optional eyebrow (the category label for category
423
+ * slots; null for article slots). A null `entry` = the slot's source
424
+ * resolved to nothing and is skipped at render.
425
+ */
426
+ export interface LatestNewsCardSnapshot {
427
+ eyebrow: string | null;
428
+ entry: NavSnapshotEntry | null;
429
+ }
430
+
431
+ /**
432
+ * `latest-news` block — the Apple-newsroom "Latest News" magazine grid.
433
+ * 3–6 author-curated slots; the first is the large feature card, the rest
434
+ * scale down to medium / small tiles (6 = 1 feature + 2 medium + 3 small,
435
+ * down to 3 = 1 feature + 2 medium). Materialized at publish like the nav-*
436
+ * feeds; reuses `NavSnapshotEntry` for each resolved card.
437
+ */
438
+ export interface LatestNewsProps extends ModuleHeader {
439
+ /** Author-curated slots, 3–6, in display order (first = feature card). */
440
+ slots: LatestNewsSlot[];
441
+ /** Content column vs wide breakout (default wide). Mirrors the media
442
+ * blocks. */
443
+ widthMode?: "content" | "wide";
444
+ /** Publish-time materialized snapshot — one card per slot, same order. */
445
+ snapshot?: LatestNewsCardSnapshot[];
446
+ }
447
+
203
448
  export type Block =
204
449
  | { component: "hero"; id: string; props: HeroProps }
205
450
  | { component: "article"; id: string; props: ArticleProps }
@@ -211,9 +456,15 @@ export type Block =
211
456
  | { component: "search"; id: string; props: SearchProps }
212
457
  | { component: "nav-recent-posts"; id: string; props: RecentPostsProps }
213
458
  | { component: "nav-related-posts"; id: string; props: RelatedPostsProps }
459
+ | { component: "article-tags"; id: string; props: ArticleTagsProps }
214
460
  | { component: "nav-category-index"; id: string; props: CategoryIndexProps }
215
461
  | { component: "image"; id: string; props: ImageProps }
216
462
  | { component: "slideshow"; id: string; props: SlideshowProps }
217
- | { component: "social-share"; id: string; props: SocialShareProps };
463
+ | { component: "collage"; id: string; props: CollageProps }
464
+ | { component: "social-share"; id: string; props: SocialShareProps }
465
+ | { component: "button-group"; id: string; props: ButtonGroupProps }
466
+ | { component: "embed"; id: string; props: EmbedProps }
467
+ | { component: "media-text"; id: string; props: MediaTextProps }
468
+ | { component: "latest-news"; id: string; props: LatestNewsProps };
218
469
 
219
470
  export type BlockComponent = Block["component"];
package/src/index.ts CHANGED
@@ -27,9 +27,12 @@ export type {
27
27
  NavTaxonomyListProps,
28
28
  RecentPostsProps,
29
29
  RelatedPostsProps,
30
+ SearchQuickLink,
31
+ SearchQuickLinkSource,
30
32
  SearchProps,
31
33
  SeoMeta,
32
34
  SlideshowProps,
33
35
  SlideshowSlideProps,
36
+ SocialSharePlatform,
34
37
  SocialShareProps,
35
38
  } from "./types";
package/src/types.ts CHANGED
@@ -56,7 +56,7 @@ export interface AeoResponse {
56
56
 
57
57
  export type CmsStatus = "ready" | "preview" | "pending" | "not_found";
58
58
 
59
- export type CmsContentType = "article" | "landing" | "listing";
59
+ export type CmsContentType = "article" | "landing" | "listing" | "taxonomy";
60
60
 
61
61
  // `CmsThemeMode` removed in v0.15.0 — Mode B (`tailwind-prose`) dropped
62
62
  // in favour of the canonical Plate-serialized HTML render (Mode A only).
@@ -87,9 +87,12 @@ export type {
87
87
  NavTaxonomyListProps,
88
88
  RecentPostsProps,
89
89
  RelatedPostsProps,
90
+ SearchQuickLink,
91
+ SearchQuickLinkSource,
90
92
  SearchProps,
91
93
  SlideshowProps,
92
94
  SlideshowSlideProps,
95
+ SocialSharePlatform,
93
96
  SocialShareProps,
94
97
  } from "./cms-blocks";
95
98