@soloworks/smking-next 0.21.3 → 0.21.5

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,25 @@
1
1
  # @soloworks/smking-next
2
2
 
3
+ ## 0.21.5 — 2026-07-20
4
+
5
+ **AEO path discovery now works on Vercel without replacing host proxy responses.**
6
+
7
+ - The Page Zero proxy forwards the concrete request URL and pathname to App
8
+ Router Server Components before continuing the customer's existing proxy.
9
+ - `<SmkingAEO />` now looks up the requested route instead of falling back to
10
+ `/` when Vercel omits pathname headers.
11
+ - Customer redirects, rewrites, cookies, and response headers remain owned by
12
+ the customer's proxy.
13
+
14
+ ## 0.21.4 — 2026-07-20
15
+
16
+ **Doctor checks now validate the actual App Router root and the public API contract.**
17
+
18
+ - Recursively finds nested root layouts such as `app/[locale]/layout.tsx`; a missing root is now a failure.
19
+ - Probes the public AEO endpoint with its documented `?key=` parameter.
20
+ - Treats a valid `not_found` response as proof that the API key was accepted and fails on unexpected `4xx` responses.
21
+ - Uses Page Zero as the default SaaS origin and in customer-facing CLI output.
22
+
3
23
  ## 0.21.3 — 2026-07-03
4
24
 
5
25
  **AEO original-vs-enhanced audits can now fetch the true host HTML.**
@@ -531,36 +551,33 @@ Next.js's `MetadataRoute.Robots` type doesn't model Cloudflare's `Content-Signal
531
551
 
532
552
  ```ts
533
553
  // app/robots.ts — Next.js MetadataRoute (NO Content-Signal)
534
- import type { MetadataRoute } from 'next';
535
- import { smkingRobotsRules } from '@soloworks/smking-next/robots';
554
+ import type { MetadataRoute } from "next";
555
+ import { smkingRobotsRules } from "@soloworks/smking-next/robots";
536
556
 
537
557
  export default function robots(): MetadataRoute.Robots {
538
558
  return {
539
- rules: [
540
- { userAgent: '*', disallow: ['/admin/'] },
541
- ...smkingRobotsRules(),
542
- ],
543
- sitemap: 'https://example.com/sitemap.xml',
559
+ rules: [{ userAgent: "*", disallow: ["/admin/"] }, ...smkingRobotsRules()],
560
+ sitemap: "https://example.com/sitemap.xml",
544
561
  };
545
562
  }
546
563
  ```
547
564
 
548
565
  ```ts
549
566
  // app/robots.txt/route.ts — full robots.txt body (Content-Signal included)
550
- import { smkingRobotsTxt } from '@soloworks/smking-next/robots';
567
+ import { smkingRobotsTxt } from "@soloworks/smking-next/robots";
551
568
 
552
569
  export function GET() {
553
570
  return new Response(
554
571
  smkingRobotsTxt({
555
- rules: [{ userAgent: '*', disallow: ['/admin/'] }],
556
- sitemap: 'https://example.com/sitemap.xml',
572
+ rules: [{ userAgent: "*", disallow: ["/admin/"] }],
573
+ sitemap: "https://example.com/sitemap.xml",
557
574
  }),
558
- { headers: { 'Content-Type': 'text/plain' } },
575
+ { headers: { "Content-Type": "text/plain" } },
559
576
  );
560
577
  }
561
578
  ```
562
579
 
563
- 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`.
580
+ 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`.
564
581
 
565
582
  ### Default policy
566
583
 
@@ -590,8 +607,8 @@ Minimal-surface rewrite. The package is now three focused files instead of a 21-
590
607
  ### Public surface
591
608
 
592
609
  ```ts
593
- import { SmkingAEO, getAeoContent } from '@soloworks/smking-next';
594
- import { POST, GET } from '@soloworks/smking-next/route';
610
+ import { SmkingAEO, getAeoContent } from "@soloworks/smking-next";
611
+ import { POST, GET } from "@soloworks/smking-next/route";
595
612
  ```
596
613
 
597
614
  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.3",
3
+ "version": "0.21.5",
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",
@@ -65,11 +65,6 @@
65
65
  "perplexity",
66
66
  "smking"
67
67
  ],
68
- "scripts": {
69
- "typecheck": "tsc --noEmit",
70
- "test": "vitest run",
71
- "test:watch": "vitest"
72
- },
73
68
  "peerDependencies": {
74
69
  "next": "^15.0.0 || ^16.0.0",
75
70
  "react": "^18.0.0 || ^19.0.0"
@@ -91,5 +86,10 @@
91
86
  "react-dom": "19.2.4",
92
87
  "typescript": "^5",
93
88
  "vitest": "^4.1.5"
89
+ },
90
+ "scripts": {
91
+ "typecheck": "tsc --noEmit",
92
+ "test": "vitest run",
93
+ "test:watch": "vitest"
94
94
  }
95
- }
95
+ }
package/src/cms-blocks.ts CHANGED
@@ -122,12 +122,14 @@ export interface NavSnapshotEntry {
122
122
  }
123
123
 
124
124
  export type SearchQuickLinkSource = "category-by-path" | "tag" | "url";
125
+ export type LinkDataAttributes = Record<string, string>;
125
126
 
126
127
  /**
127
- * Optional links shown beside the search trigger on desktop and inside the
128
- * search panel on small screens. Category/tag links keep their semantic source
129
- * + path so publish-time materialization can bake the customer's CMS mount
130
- * 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`.
131
133
  */
132
134
  export interface SearchQuickLink {
133
135
  label: string;
@@ -136,6 +138,22 @@ export interface SearchQuickLink {
136
138
  path?: string;
137
139
  /** Materialized href for category/tag links, or the author-entered URL. */
138
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[];
139
157
  }
140
158
 
141
159
  /**
@@ -154,12 +172,14 @@ export interface SearchProps {
154
172
  /** Content column vs wide breakout (default wide). Mirrors the media
155
173
  * blocks (image/embed). */
156
174
  widthMode?: "content" | "wide";
157
- /** Quick-link cluster alignment beside the search trigger. Defaults left. */
175
+ /** Desktop quick-link cluster alignment beside the search trigger. Defaults left. */
158
176
  linkAlign?: "left" | "right";
159
- /** Quick-link visual treatment. Defaults badge. */
177
+ /** Desktop quick-link visual treatment. Defaults badge. */
160
178
  linkStyle?: "badge" | "plain";
161
179
  /** Optional quick links adjacent to search. */
162
180
  links?: SearchQuickLink[];
181
+ /** Publish-time default browse links shown before the visitor types. */
182
+ suggestions?: SearchSuggestionsSnapshot;
163
183
  /** Publish-time materialized snapshot — every published page on the site. */
164
184
  snapshot?: NavSnapshotEntry[];
165
185
  }
@@ -214,6 +234,28 @@ export interface ArticleTagsProps {
214
234
  snapshot?: ArticleTagSnapshot[];
215
235
  }
216
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
+
217
259
  /** One author-selected category OR tag in a nav-category-index block. */
218
260
  export interface CategoryIndexItem {
219
261
  /** Slug prefix (category-by-path) or taxonomy slug (tag), per `source`.
@@ -376,6 +418,8 @@ export interface ButtonItemProps {
376
418
  /** Apple-style looks: `filled` = primary capsule, `tinted` = soft primary
377
419
  * wash, `outline` = bordered. Omitted = filled. */
378
420
  variant?: "filled" | "tinted" | "outline";
421
+ /** Optional author-selected data-* attributes for analytics integrations. */
422
+ dataAttributes?: LinkDataAttributes;
379
423
  }
380
424
 
381
425
  /**
@@ -451,6 +495,11 @@ export type LatestNewsMode =
451
495
  | "category"
452
496
  | "tag";
453
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
+
454
503
  /**
455
504
  * One materialized `latest-news` card: the resolved article entry (the
456
505
  * article itself for an `article` slot, or the category's latest post for a
@@ -471,6 +520,8 @@ export interface LatestNewsCardSnapshot {
471
520
  * feeds; reuses `NavSnapshotEntry` for each resolved card.
472
521
  */
473
522
  export interface LatestNewsProps extends ModuleHeader {
523
+ /** First-card presentation. Omitted/default = image above text below. */
524
+ layout?: LatestNewsLayout;
474
525
  /** Whole-block source mode. Omitted/manual uses `slots`; other modes resolve
475
526
  * cards dynamically at publish time. */
476
527
  mode?: LatestNewsMode;
@@ -501,6 +552,7 @@ export type Block =
501
552
  | { component: "search"; id: string; props: SearchProps }
502
553
  | { component: "nav-related-posts"; id: string; props: RelatedPostsProps }
503
554
  | { component: "article-tags"; id: string; props: ArticleTagsProps }
555
+ | { component: "tag-cloud"; id: string; props: TagCloudProps }
504
556
  | { component: "nav-category-index"; id: string; props: CategoryIndexProps }
505
557
  | { component: "image"; id: string; props: ImageProps }
506
558
  | { component: "slideshow"; id: string; props: SlideshowProps }
@@ -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,6 +21,7 @@ export type {
21
21
  FaqItem,
22
22
  HeroProps,
23
23
  ImageProps,
24
+ LinkDataAttributes,
24
25
  ModuleHeader,
25
26
  NavLayout,
26
27
  NavSnapshotEntry,
@@ -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/path.ts CHANGED
@@ -39,6 +39,22 @@ export async function resolveRequestPath(): Promise<{
39
39
  return { path, url };
40
40
  }
41
41
 
42
+ /**
43
+ * Add the concrete request URL to the headers consumed by Server Components.
44
+ *
45
+ * Next.js does not expose the current pathname from `headers()` by default on
46
+ * Vercel. The Page Zero proxy runs before the App Router, so it records the
47
+ * verified `NextRequest.nextUrl` on the same request before the host proxy
48
+ * continues. Existing host proxy responses remain untouched.
49
+ */
50
+ export function setRequestPathHeaders(
51
+ requestHeaders: Headers,
52
+ requestUrl: URL,
53
+ ): void {
54
+ requestHeaders.set("x-url", requestUrl.toString());
55
+ requestHeaders.set("x-pathname", requestUrl.pathname);
56
+ }
57
+
42
58
  /**
43
59
  * Normalize a path so cache tags align across hand-written and
44
60
  * auto-resolved callers. `<SmkingAEO path="products/abc" />` and
package/src/lib/proxy.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { NextResponse, type NextRequest, after } from "next/server";
2
2
  import { classifyAiHit } from "./crawlers";
3
+ import { setRequestPathHeaders } from "./path";
3
4
 
4
5
  /**
5
6
  * SmKing AI traffic ingestion proxy (v0.12+, Next.js 16 `proxy.ts`).
@@ -68,6 +69,11 @@ const INGEST_TIMEOUT_MS = 2000;
68
69
  */
69
70
  export function smkingProxy(config: SmkingProxyConfig = {}): ProxyMiddleware {
70
71
  return (request: NextRequest) => {
72
+ // Server Components cannot read the current pathname from `headers()` on
73
+ // Vercel unless middleware forwards it. Mutating this request keeps the
74
+ // host proxy's redirect, rewrite, cookie, and response behavior intact.
75
+ setRequestPathHeaders(request.headers, request.nextUrl);
76
+
71
77
  const apiKey = config.apiKey ?? process.env.SMKING_API_KEY;
72
78
  const baseUrl = (config.baseUrl ?? process.env.SMKING_BASE_URL)?.replace(
73
79
  /\/$/,
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.21.3";
1
+ export const SDK_VERSION = "0.21.5";
package/src/types.ts CHANGED
@@ -81,6 +81,7 @@ export type {
81
81
  CategoryIndexProps,
82
82
  HeroProps,
83
83
  ImageProps,
84
+ LinkDataAttributes,
84
85
  ModuleHeader,
85
86
  NavLayout,
86
87
  NavSnapshotEntry,