@soloworks/smking-next 0.21.4 → 0.21.6

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,26 @@
1
1
  # @soloworks/smking-next
2
2
 
3
+ ## 0.21.6 — 2026-07-20
4
+
5
+ **Next.js metadata can now be authoritative instead of duplicated.**
6
+
7
+ - Added `withSmkingMetadata()` for the App Router `generateMetadata` export.
8
+ Ready Page Zero SEO fields override the matching host fields while unrelated
9
+ host metadata stays intact.
10
+ - Added `<SmkingAEO includeSeo={false} />` so JSON-LD and hidden AEO content
11
+ remain rendered without emitting duplicate title and meta elements.
12
+
13
+ ## 0.21.5 — 2026-07-20
14
+
15
+ **AEO path discovery now works on Vercel without replacing host proxy responses.**
16
+
17
+ - The Page Zero proxy forwards the concrete request URL and pathname to App
18
+ Router Server Components before continuing the customer's existing proxy.
19
+ - `<SmkingAEO />` now looks up the requested route instead of falling back to
20
+ `/` when Vercel omits pathname headers.
21
+ - Customer redirects, rewrites, cookies, and response headers remain owned by
22
+ the customer's proxy.
23
+
3
24
  ## 0.21.4 — 2026-07-20
4
25
 
5
26
  **Doctor checks now validate the actual App Router root and the public API contract.**
package/bin/install.mjs CHANGED
@@ -15,8 +15,8 @@
15
15
  * drop-in files (sitemap.ts / robots.ts / llms.txt route). Idempotent:
16
16
  * already-existing files are skipped, never overwritten.
17
17
  *
18
- * - **doctor** — Self-check (env presence + <SmkingAEO /> usage + API
19
- * reachable). `--json` flag emits structured output for the
18
+ * - **doctor** — Self-check (env presence + authoritative AEO/SEO layout
19
+ * integration + API reachable). `--json` flag emits structured output for the
20
20
  * @smking/wizard install agent's `run_doctor` MCP tool.
21
21
  *
22
22
  * Conventional Next.js layout assumed: `app/` at repo root (or under
@@ -207,22 +207,30 @@ function checkLayoutUsage(appDir) {
207
207
  const layoutPath = findRootLayout(appDir);
208
208
  if (!layoutPath) {
209
209
  return {
210
- name: "<SmkingAEO /> in root layout",
210
+ name: "AEO + SEO in root layout",
211
211
  status: "fail",
212
212
  detail: `no root layout owning <html> and <body> found under ${appDir}/`,
213
213
  };
214
214
  }
215
215
 
216
216
  const content = readFileSync(layoutPath, "utf-8");
217
- if (!content.includes("SmkingAEO")) {
217
+ const missing = [];
218
+ if (!content.includes("SmkingAEO")) missing.push("SmkingAEO");
219
+ if (!content.includes("withSmkingMetadata(")) {
220
+ missing.push("withSmkingMetadata() in generateMetadata");
221
+ }
222
+ if (!/includeSeo\s*=\s*\{\s*false\s*\}/.test(content)) {
223
+ missing.push("includeSeo={false}");
224
+ }
225
+ if (missing.length > 0) {
218
226
  return {
219
- name: "<SmkingAEO /> in root layout",
227
+ name: "AEO + SEO in root layout",
220
228
  status: "fail",
221
- detail: `${layoutPath} does not import SmkingAEO. Add: import { SmkingAEO } from "@soloworks/smking-next"; then render <SmkingAEO apiKey={process.env.SMKING_API_KEY!} /> inside <body>.`,
229
+ detail: `${layoutPath} is missing ${missing.join(", ")}. Use withSmkingMetadata() for authoritative Next.js metadata and render <SmkingAEO includeSeo={false} /> inside <body>.`,
222
230
  };
223
231
  }
224
232
  return {
225
- name: "<SmkingAEO /> in root layout",
233
+ name: "AEO + SEO in root layout",
226
234
  status: "pass",
227
235
  detail: layoutPath,
228
236
  };
@@ -324,7 +332,7 @@ async function runDoctor(jsonOutput) {
324
332
  appDir
325
333
  ? checkLayoutUsage(appDir)
326
334
  : {
327
- name: "<SmkingAEO /> in root layout",
335
+ name: "AEO + SEO in root layout",
328
336
  status: "info",
329
337
  detail: "no app/ or src/app/ directory found — skipped",
330
338
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soloworks/smking-next",
3
- "version": "0.21.4",
3
+ "version": "0.21.6",
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",
@@ -16,7 +16,14 @@ const SR_ONLY_STYLE: React.CSSProperties = {
16
16
  border: 0,
17
17
  };
18
18
 
19
- export interface SmkingAEOProps extends DiscoverParams {}
19
+ export interface SmkingAEOProps extends DiscoverParams {
20
+ /**
21
+ * Keep the legacy React 19 metadata tags. Set false when the host uses
22
+ * `withSmkingMetadata()` from `generateMetadata`, which is the authoritative
23
+ * Next.js integration and avoids duplicate title/meta elements.
24
+ */
25
+ includeSeo?: boolean;
26
+ }
20
27
 
21
28
  async function wantsOriginBypass(): Promise<boolean> {
22
29
  try {
@@ -82,6 +89,7 @@ export async function SmkingAEO(props: SmkingAEOProps) {
82
89
  const hasBodyFragments = Boolean(
83
90
  aeo.summaryHtml || aeo.faqHtml || seo?.ogImageUrl,
84
91
  );
92
+ const includeSeo = props.includeSeo ?? true;
85
93
 
86
94
  return (
87
95
  <>
@@ -93,21 +101,23 @@ export async function SmkingAEO(props: SmkingAEOProps) {
93
101
  />
94
102
  )}
95
103
 
96
- {seo?.title && <title data-smking="aeo">{seo.title}</title>}
97
- {description && (
104
+ {includeSeo && seo?.title && (
105
+ <title data-smking="aeo">{seo.title}</title>
106
+ )}
107
+ {includeSeo && description && (
98
108
  <meta name="description" content={description} data-smking="aeo" />
99
109
  )}
100
- {seo?.ogTitle && (
110
+ {includeSeo && seo?.ogTitle && (
101
111
  <meta property="og:title" content={seo.ogTitle} data-smking="aeo" />
102
112
  )}
103
- {seo?.ogDescription && (
113
+ {includeSeo && seo?.ogDescription && (
104
114
  <meta
105
115
  property="og:description"
106
116
  content={seo.ogDescription}
107
117
  data-smking="aeo"
108
118
  />
109
119
  )}
110
- {seo?.ogImageUrl && (
120
+ {includeSeo && seo?.ogImageUrl && (
111
121
  <meta
112
122
  property="og:image"
113
123
  content={seo.ogImageUrl}
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export { SmkingCms } from "./components/smking-cms";
3
3
  export { SmkingRuntime } from "./components/smking-runtime";
4
4
  export { getAeoContent } from "./lib/client";
5
5
  export { getCmsPage } from "./lib/cms-client";
6
+ export { withSmkingMetadata } from "./lib/metadata";
6
7
  export type {
7
8
  AeoResponse,
8
9
  AeoStatus,
@@ -0,0 +1,40 @@
1
+ import type { Metadata } from "next";
2
+
3
+ import type { DiscoverParams } from "../types";
4
+ import { getAeoContent } from "./client";
5
+
6
+ /**
7
+ * Merge Page Zero's ready SEO fields over an existing App Router metadata
8
+ * object. Use this from the root layout's `generateMetadata` export so Next.js
9
+ * emits one authoritative title and description instead of duplicate tags.
10
+ */
11
+ export async function withSmkingMetadata(
12
+ hostMetadata: Metadata,
13
+ params: DiscoverParams,
14
+ ): Promise<Metadata> {
15
+ const aeo = await getAeoContent(params);
16
+ if (!aeo || aeo.status !== "ready") return hostMetadata;
17
+
18
+ const seo = aeo.seo ?? null;
19
+ const title = seo?.title ?? null;
20
+ const description = seo?.ogDescription ?? aeo.metaDescription ?? null;
21
+ const ogTitle = seo?.ogTitle ?? title;
22
+ const ogDescription = seo?.ogDescription ?? aeo.metaDescription ?? null;
23
+ const ogImageUrl = seo?.ogImageUrl ?? null;
24
+
25
+ return {
26
+ ...hostMetadata,
27
+ ...(title ? { title } : {}),
28
+ ...(description ? { description } : {}),
29
+ ...(ogTitle || ogDescription || ogImageUrl
30
+ ? {
31
+ openGraph: {
32
+ ...(hostMetadata.openGraph ?? {}),
33
+ ...(ogTitle ? { title: ogTitle } : {}),
34
+ ...(ogDescription ? { description: ogDescription } : {}),
35
+ ...(ogImageUrl ? { images: [ogImageUrl] } : {}),
36
+ },
37
+ }
38
+ : {}),
39
+ };
40
+ }
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
  /\/$/,
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.21.4";
1
+ export const SDK_VERSION = "0.21.6";