@soloworks/smking-next 0.21.2 → 0.21.4

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,24 @@
1
1
  # @soloworks/smking-next
2
2
 
3
+ ## 0.21.4 — 2026-07-20
4
+
5
+ **Doctor checks now validate the actual App Router root and the public API contract.**
6
+
7
+ - Recursively finds nested root layouts such as `app/[locale]/layout.tsx`; a missing root is now a failure.
8
+ - Probes the public AEO endpoint with its documented `?key=` parameter.
9
+ - Treats a valid `not_found` response as proof that the API key was accepted and fails on unexpected `4xx` responses.
10
+ - Uses Page Zero as the default SaaS origin and in customer-facing CLI output.
11
+
12
+ ## 0.21.3 — 2026-07-03
13
+
14
+ **AEO original-vs-enhanced audits can now fetch the true host HTML.**
15
+
16
+ `<SmkingAEO>` now honors `x-smking-origin-mode: raw` by returning `null`
17
+ before calling the SaaS. This lets Page Zero fetch a customer page's real
18
+ pre-injection HTML for source-change detection, then compare it against the
19
+ served enhanced version. No customer code change; customers already on `^0.21`
20
+ receive it on the next install/update.
21
+
3
22
  ## 0.21.2 — 2026-07-02
4
23
 
5
24
  **CMS taxonomy page types caught up with the SaaS catalog (types only — no runtime change).**
@@ -521,36 +540,33 @@ Next.js's `MetadataRoute.Robots` type doesn't model Cloudflare's `Content-Signal
521
540
 
522
541
  ```ts
523
542
  // app/robots.ts — Next.js MetadataRoute (NO Content-Signal)
524
- import type { MetadataRoute } from 'next';
525
- import { smkingRobotsRules } from '@soloworks/smking-next/robots';
543
+ import type { MetadataRoute } from "next";
544
+ import { smkingRobotsRules } from "@soloworks/smking-next/robots";
526
545
 
527
546
  export default function robots(): MetadataRoute.Robots {
528
547
  return {
529
- rules: [
530
- { userAgent: '*', disallow: ['/admin/'] },
531
- ...smkingRobotsRules(),
532
- ],
533
- sitemap: 'https://example.com/sitemap.xml',
548
+ rules: [{ userAgent: "*", disallow: ["/admin/"] }, ...smkingRobotsRules()],
549
+ sitemap: "https://example.com/sitemap.xml",
534
550
  };
535
551
  }
536
552
  ```
537
553
 
538
554
  ```ts
539
555
  // app/robots.txt/route.ts — full robots.txt body (Content-Signal included)
540
- import { smkingRobotsTxt } from '@soloworks/smking-next/robots';
556
+ import { smkingRobotsTxt } from "@soloworks/smking-next/robots";
541
557
 
542
558
  export function GET() {
543
559
  return new Response(
544
560
  smkingRobotsTxt({
545
- rules: [{ userAgent: '*', disallow: ['/admin/'] }],
546
- sitemap: 'https://example.com/sitemap.xml',
561
+ rules: [{ userAgent: "*", disallow: ["/admin/"] }],
562
+ sitemap: "https://example.com/sitemap.xml",
547
563
  }),
548
- { headers: { 'Content-Type': 'text/plain' } },
564
+ { headers: { "Content-Type": "text/plain" } },
549
565
  );
550
566
  }
551
567
  ```
552
568
 
553
- Customer rules render *before* the smking AI bot block in both surfaces. `bots` config replaces (not merges) the default list — `{ CCBot: 'disallow' }` produces only one bot block, not eight. `contentSignal: null` (or `""`) drops the directive entirely; passing nothing uses the default `search=yes, ai-input=no, ai-train=no`.
569
+ Customer rules render _before_ the smking AI bot block in both surfaces. `bots` config replaces (not merges) the default list — `{ CCBot: 'disallow' }` produces only one bot block, not eight. `contentSignal: null` (or `""`) drops the directive entirely; passing nothing uses the default `search=yes, ai-input=no, ai-train=no`.
554
570
 
555
571
  ### Default policy
556
572
 
@@ -580,8 +596,8 @@ Minimal-surface rewrite. The package is now three focused files instead of a 21-
580
596
  ### Public surface
581
597
 
582
598
  ```ts
583
- import { SmkingAEO, getAeoContent } from '@soloworks/smking-next';
584
- import { POST, GET } from '@soloworks/smking-next/route';
599
+ import { SmkingAEO, getAeoContent } from "@soloworks/smking-next";
600
+ import { POST, GET } from "@soloworks/smking-next/route";
585
601
  ```
586
602
 
587
603
  Plus types: `AeoResponse`, `AeoStatus`, `SeoMeta`, `FaqItem`, `ChatLinks`, `DiscoverParams`.
package/README.md CHANGED
@@ -3,20 +3,20 @@
3
3
  AI-native SEO (AEO) for Next.js. One server component injects JSON-LD, OG tags, AI summary, and FAQ on every page so AI crawlers (ChatGPT, Perplexity, Google AI) can cite your content.
4
4
 
5
5
  - **One server component** in your root layout — every URL gets its own AEO content automatically (`/products/nike-air`, `/products/adidas/red`, anything dynamic, no codemod needed).
6
- - **Fail-fast, fail-open.** 2-second timeout + Next.js ISR — if smking is down, your page renders without injection. Never blocks.
6
+ - **Fail-fast, fail-open.** 2-second timeout + Next.js ISR — if Page Zero is down, your page renders without injection. Never blocks.
7
7
  - **Push updates** — webhook handler invalidates only the changed paths via `revalidateTag`.
8
8
 
9
9
  ## Install
10
10
 
11
- **Don't follow this README to install.** Your smking dashboard generates a per-site install prompt with the real `SMKING_API_KEY`, `SMKING_BASE_URL`, and (if you use CMS) `SMKING_WEBHOOK_SECRET` baked in, plus copy-pasteable layout / route shims. The prompt is the source of truth and stays in sync with the SDK version.
11
+ **Don't follow this README to install.** Your Page Zero dashboard generates a per-site install prompt with the real `SMKING_API_KEY`, `SMKING_BASE_URL`, and (if you use CMS) `SMKING_WEBHOOK_SECRET` baked in, plus copy-pasteable layout / route shims. The prompt is the source of truth and stays in sync with the SDK version.
12
12
 
13
13
  Two ways to get it:
14
14
 
15
15
  ```bash
16
16
  # Option 1 — one-shot wizard (installs deps + writes env + runs doctor)
17
- npx @soloworks/smking-wizard
17
+ npx @soloworks/smking-wizard@latest
18
18
 
19
- # Option 2 — copy the prompt manually from your smking dashboard's
19
+ # Option 2 — copy the prompt manually from your Page Zero dashboard's
20
20
  # install panel into your editor / coding agent.
21
21
  ```
22
22
 
@@ -26,10 +26,10 @@ The wizard owns: `pnpm add @soloworks/smking-next`, `<SmkingAEO />` mount in `ap
26
26
 
27
27
  Both `<SmkingAEO />` and your own `generateMetadata` emit head tags. Next.js + React 19 head dedup applies last-write-wins:
28
28
 
29
- - **No `generateMetadata`** → smking's `<title>` / `og:*` are used.
30
- - **You write `generateMetadata` in a layout / page** → your tags override smking's for that route segment.
29
+ - **No `generateMetadata`** → Page Zero's `<title>` / `og:*` are used.
30
+ - **You write `generateMetadata` in a layout / page** → your tags override Page Zero's for that route segment.
31
31
 
32
- This is the pattern: smking provides AEO/SEO baseline, you override per-page when you want. No HOF, no codemod.
32
+ This is the pattern: Page Zero provides the AEO/SEO baseline, and you override per-page when needed. No HOF, no codemod.
33
33
 
34
34
  For client pages (`'use client'`) that need dynamic metadata, write a sibling `layout.tsx` with `generateMetadata` — standard Next.js workflow, unrelated to smking.
35
35
 
@@ -38,8 +38,8 @@ For client pages (`'use client'`) that need dynamic metadata, write a sibling `l
38
38
  `getAeoContent` wraps `fetch` with `AbortSignal.timeout(2000)` and Next.js ISR (`next: { revalidate: 3600, tags: ['smking:path:<path>'] }`):
39
39
 
40
40
  - **Cache hit** (the common path): zero network. Tags allow webhook-driven invalidation.
41
- - **Cache miss + smking healthy**: one network roundtrip, response cached for 1h.
42
- - **Cache miss + smking down / hung**: returns null after at most 2s, page renders without injection. Next.js ISR retries on the next request after `revalidate`.
41
+ - **Cache miss + Page Zero healthy**: one network roundtrip, response cached for 1h.
42
+ - **Cache miss + Page Zero down / hung**: returns null after at most 2s, page renders without injection. Next.js ISR retries on the next request after `revalidate`.
43
43
  - **5xx / 4xx / parse error**: same fail-open path.
44
44
 
45
45
  No circuit breaker, no retry, no status command — Next.js infrastructure already covers what those would do.
@@ -50,10 +50,10 @@ No circuit breaker, no retry, no status command — Next.js infrastructure alrea
50
50
 
51
51
  ```ts
52
52
  interface SmkingAEOProps {
53
- apiKey: string; // required
54
- baseUrl?: string; // override SMKING_BASE_URL env
55
- path?: string; // explicit path; auto-resolved from headers() otherwise
56
- revalidate?: number; // ISR seconds; default 3600 (1h)
53
+ apiKey: string; // required
54
+ baseUrl?: string; // override SMKING_BASE_URL env
55
+ path?: string; // explicit path; auto-resolved from headers() otherwise
56
+ revalidate?: number; // ISR seconds; default 3600 (1h)
57
57
  }
58
58
  ```
59
59
 
@@ -92,11 +92,15 @@ Add `<SmkingRuntime />` once in your root layout — it emits a `<link>` to the
92
92
  // app/layout.tsx
93
93
  import { SmkingRuntime } from "@soloworks/smking-next";
94
94
 
95
- export default function RootLayout({ children }: { children: React.ReactNode }) {
95
+ export default function RootLayout({
96
+ children,
97
+ }: {
98
+ children: React.ReactNode;
99
+ }) {
96
100
  return (
97
101
  <html>
98
102
  <body>
99
- <SmkingRuntime />
103
+ <SmkingRuntime apiKey={process.env.SMKING_API_KEY!} />
100
104
  {children}
101
105
  </body>
102
106
  </html>
@@ -104,42 +108,44 @@ export default function RootLayout({ children }: { children: React.ReactNode })
104
108
  }
105
109
  ```
106
110
 
107
- Optional `baseUrl` prop overrides `process.env.SMKING_BASE_URL`. Default: `https://smking.app`.
111
+ Optional `baseUrl` prop overrides `process.env.SMKING_BASE_URL`. Default: `https://getpagezero.com`.
108
112
 
109
113
  Without `<SmkingRuntime />`, `<SmkingCms>` content still renders but the Tailwind utility classes from the dashboard's cva variants resolve to dead strings — the page reaches the browser unstyled. The wizard installer auto-adds this mount; if you're upgrading manually from < v0.16.1, add the one line above.
110
114
 
111
115
  ## CMS rendering (optional, v0.11.0+)
112
116
 
113
- The base install only wires AEO. If you author content in the smking dashboard's CMS and want to render it on your Next.js site, use the `<SmkingCms slug="…" />` Server Component.
117
+ The base install only wires AEO. If you author content in the Page Zero dashboard CMS and want to render it on your Next.js site, use the `<SmkingCms slug="…" />` Server Component.
114
118
 
115
119
  The SDK **does not have a "CMS root" config** — you choose any URL prefix (`/blog`, `/knowledge`, `/shop/articles`) and wire your own route. The component takes a `slug` prop, fetches the published page from `${SMKING_BASE_URL}/api/v1/public/page?slug=…`, and renders the Tiptap ProseMirror JSON as `<article class="smk-cms">…</article>` via `@tiptap/static-renderer/pm/react`. SEO `<title>` / `<meta>` / `og:*` / canonical hoist into `<head>` automatically via React 19.
116
120
 
117
- ### Catch-all route (handles flat + nested slugs)
121
+ ### Optional catch-all route (handles the CMS root + nested slugs)
118
122
 
119
- smking CMS slugs can be nested — e.g. `blog/123`, `blog/seo/intro`. Use Next.js catch-all `[...slug]` (three dots, not single `[slug]`) so one route handles every depth:
123
+ Page Zero CMS slugs can be nested, and the CMS root uses an empty slug. Use
124
+ Next.js optional catch-all `[[...slug]]` so one route handles `/blog`, flat
125
+ slugs, and every nested depth:
120
126
 
121
127
  ```tsx
122
- // app/blog/[...slug]/page.tsx
128
+ // app/blog/[[...slug]]/page.tsx
123
129
  import { SmkingCms } from "@soloworks/smking-next/cms";
124
130
 
125
131
  export default async function Page({
126
132
  params,
127
133
  }: {
128
- params: Promise<{ slug: string[] }>;
134
+ params: Promise<{ slug?: string[] }>;
129
135
  }) {
130
136
  const { slug } = await params;
131
137
  return (
132
138
  <SmkingCms
133
139
  apiKey={process.env.SMKING_API_KEY!}
134
- slug={slug.join("/")} // ← array → "blog/seo/intro" matches dashboard slug format
140
+ slug={slug?.join("/") ?? ""}
135
141
  />
136
142
  );
137
143
  }
138
144
  ```
139
145
 
140
- `[...slug]` accepts both flat (`/blog/hello` → `["hello"]`) and nested (`/blog/seo/intro` → `["seo", "intro"]`). The `.join("/")` reconstructs the dashboard slug string.
141
-
142
- Single-bracket `[slug]` (without the three dots) **only matches one segment** — pick this if you know your slugs are always flat.
146
+ At `/blog`, `params.slug` is undefined and maps to the empty CMS slug. Flat
147
+ and nested paths map to `"hello"` and `"seo/intro"` respectively. A required
148
+ `[...slug]` would miss the CMS root.
143
149
 
144
150
  ### Markup contract for CSS
145
151
 
@@ -148,17 +154,21 @@ Single-bracket `[slug]` (without the three dots) **only matches one segment**
148
154
  <h1 class="smk-cms__title">…</h1>
149
155
  <p>standard prose</p>
150
156
  <h2>headings</h2>
151
- <ul><li>lists</li></ul>
157
+ <ul>
158
+ <li>lists</li>
159
+ </ul>
152
160
  <blockquote>…</blockquote>
153
161
  <pre><code>code blocks</code></pre>
154
162
  <a href="…">links</a>
155
- <img src="…" alt="…">
156
- <div data-type="gallery"
157
- data-layout="grid"
158
- data-columns="3"
159
- class="smk-gallery smk-gallery--grid">
163
+ <img src="…" alt="…" />
164
+ <div
165
+ data-type="gallery"
166
+ data-layout="grid"
167
+ data-columns="3"
168
+ class="smk-gallery smk-gallery--grid"
169
+ >
160
170
  <figure class="smk-gallery__item">
161
- <img src="…" alt="…" loading="lazy">
171
+ <img src="…" alt="…" loading="lazy" />
162
172
  <figcaption>optional</figcaption>
163
173
  </figure>
164
174
  </div>
package/bin/install.mjs CHANGED
@@ -22,7 +22,13 @@
22
22
  * Conventional Next.js layout assumed: `app/` at repo root (or under
23
23
  * `src/app/` — detected). Both subcommands exit 1 on missing app/.
24
24
  */
25
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
+ import {
26
+ existsSync,
27
+ mkdirSync,
28
+ readFileSync,
29
+ readdirSync,
30
+ writeFileSync,
31
+ } from "node:fs";
26
32
  import { join } from "node:path";
27
33
 
28
34
  // ── install (file scaffold) ─────────────────────────────────────
@@ -124,7 +130,6 @@ function loadEnvFiles() {
124
130
  }
125
131
  }
126
132
 
127
-
128
133
  /**
129
134
  * @typedef {{ name: string, status: "pass" | "fail" | "info", detail: string }} DoctorCheck
130
135
  */
@@ -157,25 +162,54 @@ function checkEnv(name, prefix) {
157
162
  };
158
163
  }
159
164
 
165
+ /**
166
+ * Find the shallowest layout that owns html + body. This supports
167
+ * internationalized roots such as app/[locale]/layout.tsx without
168
+ * mistaking a nested section layout for the application root.
169
+ * @param {string} appDir
170
+ * @returns {string | null}
171
+ */
172
+ function findRootLayout(appDir) {
173
+ const conventional = ["tsx", "jsx", "ts", "js"]
174
+ .map((extension) => join(appDir, `layout.${extension}`))
175
+ .find((path) => existsSync(path));
176
+ if (conventional) return conventional;
177
+
178
+ /** @type {Array<{ path: string, depth: number }>} */
179
+ const nested = [];
180
+ function visit(directory, depth) {
181
+ if (depth > 4) return;
182
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
183
+ if (entry.name.startsWith(".")) continue;
184
+ const path = join(directory, entry.name);
185
+ if (entry.isDirectory()) {
186
+ visit(path, depth + 1);
187
+ continue;
188
+ }
189
+ if (!/^layout\.(tsx|jsx|ts|js)$/.test(entry.name)) continue;
190
+ const content = readFileSync(path, "utf-8");
191
+ if (/<html(?:\s|>)/.test(content) && /<body(?:\s|>)/.test(content)) {
192
+ nested.push({ path, depth });
193
+ }
194
+ }
195
+ }
196
+
197
+ visit(appDir, 0);
198
+ nested.sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path));
199
+ return nested[0]?.path ?? null;
200
+ }
201
+
160
202
  /**
161
203
  * @param {string} appDir
162
204
  * @returns {DoctorCheck}
163
205
  */
164
206
  function checkLayoutUsage(appDir) {
165
- // Mirror Next.js's layout file resolution. Order matters — TS > JS to
166
- // match what Next.js itself would pick.
167
- const candidates = [
168
- join(appDir, "layout.tsx"),
169
- join(appDir, "layout.jsx"),
170
- join(appDir, "layout.ts"),
171
- join(appDir, "layout.js"),
172
- ];
173
- const layoutPath = candidates.find((p) => existsSync(p));
207
+ const layoutPath = findRootLayout(appDir);
174
208
  if (!layoutPath) {
175
209
  return {
176
210
  name: "<SmkingAEO /> in root layout",
177
- status: "info",
178
- detail: `no layout file found under ${appDir}/ — skipped`,
211
+ status: "fail",
212
+ detail: `no root layout owning <html> and <body> found under ${appDir}/`,
179
213
  };
180
214
  }
181
215
 
@@ -212,10 +246,10 @@ async function checkApiReachable() {
212
246
  // would pass for any live host (a typo'd domain, google.com); the
213
247
  // endpoint either returns the AEO payload or a known auth/validation
214
248
  // status — both confirm the route exists.
215
- const url = `${baseUrl.replace(/\/$/, "")}/api/v1/public/aeo?path=%2F`;
249
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/public/aeo`;
250
+ const url = `${endpoint}?key=${encodeURIComponent(apiKey)}&path=%2F`;
216
251
  try {
217
252
  const res = await fetch(url, {
218
- headers: { authorization: `Bearer ${apiKey}` },
219
253
  signal: AbortSignal.timeout(3000),
220
254
  });
221
255
 
@@ -223,35 +257,45 @@ async function checkApiReachable() {
223
257
  return {
224
258
  name: "API reachable",
225
259
  status: "pass",
226
- detail: `${url} → HTTP ${res.status}`,
260
+ detail: `${endpoint} → HTTP ${res.status}; API key accepted`,
227
261
  };
228
262
  }
229
263
  if (res.status === 401) {
230
264
  return {
231
265
  name: "API reachable",
232
266
  status: "fail",
233
- detail: `HTTP 401 from ${url} — SMKING_API_KEY rejected`,
267
+ detail: `HTTP 401 from ${endpoint} — SMKING_API_KEY rejected`,
234
268
  };
235
269
  }
236
270
  if (res.status === 404) {
271
+ const payload = await res
272
+ .clone()
273
+ .json()
274
+ .catch(() => null);
275
+ if (payload?.status === "not_found") {
276
+ return {
277
+ name: "API reachable",
278
+ status: "pass",
279
+ detail: `${endpoint} → HTTP 404 not_found; API key accepted`,
280
+ };
281
+ }
237
282
  return {
238
283
  name: "API reachable",
239
284
  status: "fail",
240
- detail: `HTTP 404 from ${url} — base_url likely points at the wrong host`,
285
+ detail: `HTTP 404 from ${endpoint} — SMKING_BASE_URL likely points at the wrong host`,
241
286
  };
242
287
  }
243
288
  if (res.status >= 500) {
244
289
  return {
245
290
  name: "API reachable",
246
291
  status: "fail",
247
- detail: `upstream HTTP ${res.status} from ${url}`,
292
+ detail: `upstream HTTP ${res.status} from ${endpoint}`,
248
293
  };
249
294
  }
250
- // 4xx (other than 401/404) — likely validation error, endpoint exists
251
295
  return {
252
296
  name: "API reachable",
253
- status: "info",
254
- detail: `${url} → HTTP ${res.status} (endpoint exists)`,
297
+ status: "fail",
298
+ detail: `${endpoint} returned unexpected HTTP ${res.status}`,
255
299
  };
256
300
  } catch (err) {
257
301
  return {
@@ -304,19 +348,15 @@ async function runDoctor(jsonOutput) {
304
348
 
305
349
  for (const check of checks) {
306
350
  const icon =
307
- check.status === "pass"
308
- ? "✅"
309
- : check.status === "fail"
310
- ? "❌"
311
- : "ℹ️ ";
351
+ check.status === "pass" ? "✅" : check.status === "fail" ? "❌" : "ℹ️ ";
312
352
  console.log(`${icon} ${check.name} — ${check.detail}`);
313
353
  }
314
354
  console.log();
315
355
  if (hasFailure) {
316
- console.log("❌ smking: install incomplete — fix the items above.");
356
+ console.log("❌ Page Zero: install incomplete — fix the items above.");
317
357
  return 1;
318
358
  }
319
- console.log("✅ smking: install OK.");
359
+ console.log("✅ Page Zero: install OK.");
320
360
  return 0;
321
361
  }
322
362
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soloworks/smking-next",
3
- "version": "0.21.2",
3
+ "version": "0.21.4",
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
@@ -27,6 +27,35 @@
27
27
  * slug-prefix derived, tag mode targets `taxonomies.slug`.
28
28
  */
29
29
 
30
+ export interface YouTubeEmbedOptions {
31
+ autoplay?: boolean;
32
+ ccLangPref?: string;
33
+ ccLoadPolicy?: boolean;
34
+ color?: "red" | "white";
35
+ controls?: boolean;
36
+ disablekb?: boolean;
37
+ enablejsapi?: boolean;
38
+ end?: number;
39
+ fs?: boolean;
40
+ hl?: string;
41
+ ivLoadPolicy?: "show" | "hide";
42
+ list?: string;
43
+ listType?: "playlist" | "user_uploads";
44
+ loop?: boolean;
45
+ origin?: string;
46
+ playlist?: string;
47
+ playsinline?: boolean;
48
+ relatedMode?: "all" | "same-channel";
49
+ start?: number;
50
+ widgetReferrer?: string;
51
+ }
52
+
53
+ export interface HeroYouTubeProps extends YouTubeEmbedOptions {
54
+ url: string;
55
+ videoId: string;
56
+ thumbnailUrl: string;
57
+ }
58
+
30
59
  export interface HeroProps {
31
60
  title: string;
32
61
  /** Generic visible meta lines. Legacy drafts may still also carry subtitle. */
@@ -39,6 +68,9 @@ export interface HeroProps {
39
68
  /** Legacy single-line fallback for SDK Mode B consumers. */
40
69
  subtitle?: string;
41
70
  image?: { url: string; alt: string };
71
+ /** YouTube hero media. Article body renders the embed; cards / metadata use
72
+ * the thumbnail so list-like pages never mount a player. */
73
+ youtube?: HeroYouTubeProps;
42
74
  cta?: { label: string; href: string };
43
75
  /** Header layout — omitted = the original centred hero; "start" = the
44
76
  * Apple-newsroom-style left-aligned header; "cover" = title-only banner over
@@ -90,12 +122,14 @@ export interface NavSnapshotEntry {
90
122
  }
91
123
 
92
124
  export type SearchQuickLinkSource = "category-by-path" | "tag" | "url";
125
+ export type LinkDataAttributes = Record<string, string>;
93
126
 
94
127
  /**
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`.
128
+ * Optional links shown beside the search trigger on desktop and in a mobile
129
+ * hamburger dropdown outside the search panel. Category/tag links keep their
130
+ * semantic source + path so publish-time materialization can bake the
131
+ * customer's CMS mount prefix into `href`; URL links carry the author's
132
+ * literal `href`.
99
133
  */
100
134
  export interface SearchQuickLink {
101
135
  label: string;
@@ -104,6 +138,22 @@ export interface SearchQuickLink {
104
138
  path?: string;
105
139
  /** Materialized href for category/tag links, or the author-entered URL. */
106
140
  href?: string;
141
+ /** Optional author-selected data-* attributes for analytics integrations. */
142
+ dataAttributes?: LinkDataAttributes;
143
+ }
144
+
145
+ export interface SearchSuggestionLink {
146
+ label: string;
147
+ source: Exclude<SearchQuickLinkSource, "url">;
148
+ /** Category slug path or flat tag slug. */
149
+ path: string;
150
+ /** Materialized customer-site href. */
151
+ href: string;
152
+ }
153
+
154
+ export interface SearchSuggestionsSnapshot {
155
+ categories: SearchSuggestionLink[];
156
+ tags: SearchSuggestionLink[];
107
157
  }
108
158
 
109
159
  /**
@@ -122,22 +172,18 @@ export interface SearchProps {
122
172
  /** Content column vs wide breakout (default wide). Mirrors the media
123
173
  * blocks (image/embed). */
124
174
  widthMode?: "content" | "wide";
175
+ /** Desktop quick-link cluster alignment beside the search trigger. Defaults left. */
176
+ linkAlign?: "left" | "right";
177
+ /** Desktop quick-link visual treatment. Defaults badge. */
178
+ linkStyle?: "badge" | "plain";
125
179
  /** Optional quick links adjacent to search. */
126
180
  links?: SearchQuickLink[];
181
+ /** Publish-time default browse links shown before the visitor types. */
182
+ suggestions?: SearchSuggestionsSnapshot;
127
183
  /** Publish-time materialized snapshot — every published page on the site. */
128
184
  snapshot?: NavSnapshotEntry[];
129
185
  }
130
186
 
131
- export interface RecentPostsProps extends ModuleHeader {
132
- /** Author-set number of latest posts to show (1..50). */
133
- limit: number;
134
- /** Content column vs wide breakout (default content). Mirrors the media
135
- * blocks. */
136
- widthMode?: "content" | "wide";
137
- /** Publish-time materialized snapshot — whole-site newest-first. */
138
- snapshot?: NavSnapshotEntry[];
139
- }
140
-
141
187
  /** One author-picked source for nav-related-posts. Same shape as
142
188
  * `CategoryIndexItem` (a category path OR a tag), reused so the source picker
143
189
  * UI stays identical across blocks. */
@@ -188,6 +234,28 @@ export interface ArticleTagsProps {
188
234
  snapshot?: ArticleTagSnapshot[];
189
235
  }
190
236
 
237
+ /** One tag in the site-level tag cloud. */
238
+ export interface TagCloudEntry {
239
+ slug: string;
240
+ name: string;
241
+ /** Virtual tag archive href, e.g. `/blog/tag/tofu-life`. */
242
+ href: string;
243
+ /** Number of published article pages carrying this tag. */
244
+ count: number;
245
+ }
246
+
247
+ /**
248
+ * `tag-cloud` block — site-level tag browser. It is intentionally separate
249
+ * from `nav-category-index`: no cards, no latest-post preview, just tag chips.
250
+ */
251
+ export interface TagCloudProps {
252
+ heading?: string;
253
+ /** Content column vs wide breakout. Omitted = content. */
254
+ widthMode?: "content" | "wide";
255
+ /** Publish-time materialized tags attached to published article pages. */
256
+ snapshot?: TagCloudEntry[];
257
+ }
258
+
191
259
  /** One author-selected category OR tag in a nav-category-index block. */
192
260
  export interface CategoryIndexItem {
193
261
  /** Slug prefix (category-by-path) or taxonomy slug (tag), per `source`.
@@ -350,6 +418,8 @@ export interface ButtonItemProps {
350
418
  /** Apple-style looks: `filled` = primary capsule, `tinted` = soft primary
351
419
  * wash, `outline` = bordered. Omitted = filled. */
352
420
  variant?: "filled" | "tinted" | "outline";
421
+ /** Optional author-selected data-* attributes for analytics integrations. */
422
+ dataAttributes?: LinkDataAttributes;
353
423
  }
354
424
 
355
425
  /**
@@ -369,7 +439,7 @@ export interface ButtonGroupProps {
369
439
  * url = renders nothing on the customer side. `widthMode` matches the other
370
440
  * media blocks (content column vs wide breakout).
371
441
  */
372
- export interface EmbedProps {
442
+ export interface EmbedProps extends YouTubeEmbedOptions {
373
443
  url?: string;
374
444
  caption?: string;
375
445
  widthMode?: "content" | "wide";
@@ -416,6 +486,20 @@ export type LatestNewsSlot =
416
486
  source?: "category-by-path" | "tag";
417
487
  };
418
488
 
489
+ /** Whole-block source mode for `latest-news`. Omitted = `manual` so old
490
+ * blocks keep their curated slots exactly. */
491
+ export type LatestNewsMode =
492
+ | "manual"
493
+ | "latest"
494
+ | "auto-category"
495
+ | "category"
496
+ | "tag";
497
+
498
+ /** First-card layout for `latest-news`. Omitted = `stacked`, the default
499
+ * image-above/text-below card. `split` preserves the original desktop
500
+ * image-left/text-right presentation. */
501
+ export type LatestNewsLayout = "stacked" | "split";
502
+
419
503
  /**
420
504
  * One materialized `latest-news` card: the resolved article entry (the
421
505
  * article itself for an `article` slot, or the category's latest post for a
@@ -436,8 +520,20 @@ export interface LatestNewsCardSnapshot {
436
520
  * feeds; reuses `NavSnapshotEntry` for each resolved card.
437
521
  */
438
522
  export interface LatestNewsProps extends ModuleHeader {
523
+ /** First-card presentation. Omitted/default = image above text below. */
524
+ layout?: LatestNewsLayout;
525
+ /** Whole-block source mode. Omitted/manual uses `slots`; other modes resolve
526
+ * cards dynamically at publish time. */
527
+ mode?: LatestNewsMode;
439
528
  /** Author-curated slots, 3–6, in display order (first = feature card). */
440
529
  slots: LatestNewsSlot[];
530
+ /** Card count for automatic modes. The magazine grid supports 3–6 cards. */
531
+ limit?: number;
532
+ /** Selected category/tag path for `category` / `tag` modes. */
533
+ sourcePath?: string;
534
+ /** Editor-facing label for the selected category/tag; used as the card
535
+ * eyebrow in automatic source modes. */
536
+ sourceLabel?: string;
441
537
  /** Content column vs wide breakout (default wide). Mirrors the media
442
538
  * blocks. */
443
539
  widthMode?: "content" | "wide";
@@ -454,9 +550,9 @@ export type Block =
454
550
  props: NavTaxonomyListProps;
455
551
  }
456
552
  | { component: "search"; id: string; props: SearchProps }
457
- | { component: "nav-recent-posts"; id: string; props: RecentPostsProps }
458
553
  | { component: "nav-related-posts"; id: string; props: RelatedPostsProps }
459
554
  | { component: "article-tags"; id: string; props: ArticleTagsProps }
555
+ | { component: "tag-cloud"; id: string; props: TagCloudProps }
460
556
  | { component: "nav-category-index"; id: string; props: CategoryIndexProps }
461
557
  | { component: "image"; id: string; props: ImageProps }
462
558
  | { component: "slideshow"; id: string; props: SlideshowProps }
@@ -1,3 +1,5 @@
1
+ import { headers } from "next/headers";
2
+
1
3
  import type { DiscoverParams } from "../types";
2
4
  import { getAeoContent } from "../lib/client";
3
5
  import { safeJson } from "../lib/safe-json";
@@ -16,6 +18,18 @@ const SR_ONLY_STYLE: React.CSSProperties = {
16
18
 
17
19
  export interface SmkingAEOProps extends DiscoverParams {}
18
20
 
21
+ async function wantsOriginBypass(): Promise<boolean> {
22
+ try {
23
+ const requestHeaders = await headers();
24
+ return (
25
+ requestHeaders.get("x-smking-origin-mode")?.trim().toLowerCase() ===
26
+ "raw"
27
+ );
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
19
33
  /**
20
34
  * Server Component that injects AEO + SEO content for the current
21
35
  * request. Place once in the root layout, inside `<body>`:
@@ -57,6 +71,8 @@ export interface SmkingAEOProps extends DiscoverParams {}
57
71
  * arbitrary content fetched anywhere.
58
72
  */
59
73
  export async function SmkingAEO(props: SmkingAEOProps) {
74
+ if (await wantsOriginBypass()) return null;
75
+
60
76
  const aeo = await getAeoContent(props);
61
77
  if (!aeo || aeo.status !== "ready") return null;
62
78
 
@@ -21,13 +21,18 @@ export function SmkingRuntime({
21
21
  apiKey?: string;
22
22
  }) {
23
23
  const url = (
24
- baseUrl ?? process.env.SMKING_BASE_URL ?? "https://smking.app"
24
+ baseUrl ??
25
+ process.env.SMKING_BASE_URL ??
26
+ "https://getpagezero.com"
25
27
  ).replace(/\/$/, "");
26
28
  return (
27
29
  <>
28
30
  <link rel="stylesheet" href={`${url}/api/v1/public/runtime.css`} />
29
31
  {apiKey && (
30
- <link rel="stylesheet" href={`${url}/api/v1/public/theme.css?key=${apiKey}`} />
32
+ <link
33
+ rel="stylesheet"
34
+ href={`${url}/api/v1/public/theme.css?key=${apiKey}`}
35
+ />
31
36
  )}
32
37
  <script src={`${url}/api/v1/public/runtime.js`} async />
33
38
  </>
package/src/index.ts CHANGED
@@ -21,11 +21,11 @@ export type {
21
21
  FaqItem,
22
22
  HeroProps,
23
23
  ImageProps,
24
+ LinkDataAttributes,
24
25
  ModuleHeader,
25
26
  NavLayout,
26
27
  NavSnapshotEntry,
27
28
  NavTaxonomyListProps,
28
- RecentPostsProps,
29
29
  RelatedPostsProps,
30
30
  SearchQuickLink,
31
31
  SearchQuickLinkSource,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Drop-in `app/llms.txt/route.ts` GET handler. Proxies the canonical
3
- * llms.txt served by the smking SaaS at `/api/v1/public/llms-txt?key=...`.
3
+ * llms.txt served by Page Zero at `/api/v1/public/llms-txt?key=...`.
4
4
  *
5
5
  * ```ts
6
6
  * // app/llms.txt/route.ts
@@ -22,7 +22,7 @@ export async function GET(): Promise<Response> {
22
22
  if (!apiKey) {
23
23
  return new Response("Not found", { status: 404 });
24
24
  }
25
- const baseUrl = process.env.SMKING_BASE_URL ?? "https://saas.smking.com";
25
+ const baseUrl = process.env.SMKING_BASE_URL ?? "https://getpagezero.com";
26
26
 
27
27
  try {
28
28
  const res = await fetch(
package/src/lib/robots.ts CHANGED
@@ -64,9 +64,7 @@ export const SMKING_DEFAULT_CONTENT_SIGNAL =
64
64
  * customers who need that directive should switch to
65
65
  * `smkingRobotsTxt()` and serve via a route handler.
66
66
  */
67
- export function smkingRobotsRules(
68
- config: SmkingRobotsConfig = {},
69
- ): Array<{
67
+ export function smkingRobotsRules(config: SmkingRobotsConfig = {}): Array<{
70
68
  userAgent: string;
71
69
  allow?: string;
72
70
  disallow?: string;
@@ -169,7 +167,7 @@ function toArray<T>(value: T | T[] | undefined): T[] {
169
167
 
170
168
  /**
171
169
  * Drop-in `app/robots.ts` default export. Fetches the canonical robots.txt
172
- * served by the smking SaaS at `/api/v1/public/robots.txt?key=...` and
170
+ * served by Page Zero at `/api/v1/public/robots.txt?key=...` and
173
171
  * returns a `Response` with `text/plain`. Customer code stays one line:
174
172
  *
175
173
  * ```ts
@@ -201,7 +199,7 @@ export default async function smkingRobotsRoute(): Promise<Response> {
201
199
  }
202
200
  const apiKey = process.env.SMKING_API_KEY;
203
201
  if (!apiKey) return permissiveFallback();
204
- const baseUrl = process.env.SMKING_BASE_URL ?? "https://saas.smking.com";
202
+ const baseUrl = process.env.SMKING_BASE_URL ?? "https://getpagezero.com";
205
203
 
206
204
  try {
207
205
  const res = await fetch(
@@ -2,7 +2,7 @@ import type { MetadataRoute } from "next";
2
2
 
3
3
  /**
4
4
  * Drop-in `app/sitemap.ts` default export. Fetches the canonical sitemap
5
- * served by the smking SaaS at `/api/v1/public/sitemap.xml?key=...`, parses
5
+ * served by Page Zero at `/api/v1/public/sitemap.xml?key=...`, parses
6
6
  * the XML, and returns Next.js's `MetadataRoute.Sitemap` shape so the
7
7
  * framework can build the static sitemap at request time.
8
8
  *
@@ -22,13 +22,13 @@ import type { MetadataRoute } from "next";
22
22
  *
23
23
  * Required env:
24
24
  * - `SMKING_API_KEY` (publishable, the `pk_…` key)
25
- * - `SMKING_BASE_URL` (defaults to https://saas.smking.com)
25
+ * - `SMKING_BASE_URL` (defaults to https://getpagezero.com)
26
26
  */
27
27
  export default async function smkingSitemap(): Promise<MetadataRoute.Sitemap> {
28
28
  if (process.env.SMKING_DISABLE_TAKEOVER_SITEMAP === "1") return [];
29
29
  const apiKey = process.env.SMKING_API_KEY;
30
30
  if (!apiKey) return [];
31
- const baseUrl = process.env.SMKING_BASE_URL ?? "https://saas.smking.com";
31
+ const baseUrl = process.env.SMKING_BASE_URL ?? "https://getpagezero.com";
32
32
 
33
33
  let body: string;
34
34
  try {
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.18.0";
1
+ export const SDK_VERSION = "0.21.4";
package/src/types.ts CHANGED
@@ -81,11 +81,11 @@ export type {
81
81
  CategoryIndexProps,
82
82
  HeroProps,
83
83
  ImageProps,
84
+ LinkDataAttributes,
84
85
  ModuleHeader,
85
86
  NavLayout,
86
87
  NavSnapshotEntry,
87
88
  NavTaxonomyListProps,
88
- RecentPostsProps,
89
89
  RelatedPostsProps,
90
90
  SearchQuickLink,
91
91
  SearchQuickLinkSource,