@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 +21 -0
- package/bin/install.mjs +16 -8
- package/package.json +1 -1
- package/src/components/smking-aeo.tsx +16 -6
- package/src/index.ts +1 -0
- package/src/lib/metadata.ts +40 -0
- package/src/lib/path.ts +16 -0
- package/src/lib/proxy.ts +6 -0
- package/src/lib/version.ts +1 -1
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 +
|
|
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: "
|
|
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
|
-
|
|
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: "
|
|
227
|
+
name: "AEO + SEO in root layout",
|
|
220
228
|
status: "fail",
|
|
221
|
-
detail: `${layoutPath}
|
|
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: "
|
|
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: "
|
|
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.
|
|
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 &&
|
|
97
|
-
|
|
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
|
/\/$/,
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.21.
|
|
1
|
+
export const SDK_VERSION = "0.21.6";
|