@soloworks/smking-next
v0.23.2
Published
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.
Maintainers
Readme
@soloworks/smking-next
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.
- Server-rendered AEO — pass the known page path to preserve host-owned SSR/SSG/ISR. The optional root-layout automatic mode reads the current request path and therefore requires request-aware rendering.
- Fail-fast, fail-open. 2-second timeout + Next.js ISR — if Page Zero is down, your page renders without injection. Never blocks.
- Push updates — webhook handler invalidates only the changed paths via
revalidateTag.
Install
The public installation and migration guide is the installation source shared by Dashboard and CLI. Use your own coding assistant to follow it. CLI 0.10+ connects your workspace/site and writes confirmed local settings; it does not install packages, edit layouts or activate v2.
pnpm dlx @soloworks/smking-wizard@latest connect
pnpm dlx @soloworks/smking-wizard@latest instructions --framework=nextjsChoose AEO, Blog or both according to your site. Follow the guide for version-compatible integration, webhook and surface-aware doctor/build verification.
Authoritative metadata
Use withSmkingMetadata() from the page's generateMetadata export with its
known path, then mount <SmkingAEO path="/about" includeSeo={false} /> for
JSON-LD and hidden AEO fragments. This preserves the host's rendering choice
and produces one authoritative title and description. A root-layout helper
without a path is the optional request-aware automatic mode, not a static-page integration.
來源提供 seo.canonicalUrl 時,withSmkingMetadata() 會合併至原生
alternates.canonical,保留宿主的語系及其他替代連結;來源未提供時保留宿主
canonical。舊版 <SmkingAEO> 仍不自行輸出 canonical,避免重複訊號。
此欄位整合為尚未發布的修正,已發布 0.23.0 尚未包含。
export async function generateMetadata(): Promise<Metadata> {
return withSmkingMetadata(hostMetadata, {
apiKey: process.env.SMKING_API_KEY!,
path: "/about",
});
}
<SmkingAEO
apiKey={process.env.SMKING_API_KEY!}
path="/about"
includeSeo={false}
/>How outage tolerance works
渲染方式由宿主選擇(未發布修正)
AEO 應在伺服器 HTML 中提供 JSON-LD、摘要與 FAQ,不代表一定要逐請求渲染。v2 明確指定 path 的 SmkingAEO/getAeoContent/withSmkingMetadata,以及指定 slug 的 CMS,不再為附帶偵測強制 connection() 或讀取請求資訊;可由宿主採 SSR、SSG/ISR。未指定 path 的全站自動用法仍需要當下請求路徑,會影響預渲染;要保留靜態頁,請在 page/generateMetadata 傳入已知路徑,不要同時在 root layout 掛自動模式。
v2 頁面觀測改由既有 smkingProxy 獨立回報;回報失敗、沒有密鑰或未安裝 proxy 都不影響內容讀取。爬蟲統計亦獨立,不靠瀏覽器 JavaScript。安裝/升級時保留 proxy 與既有伺服器端 SMKING_WEBHOOK_SECRET,否則不會自動發現新路徑;低流量 /health 排程不變。完整範例、raw audit 與快取限制見 原生快取整合。此修正尚未發布,不能以既有 beta 部署代替驗證。
以下描述預設 v1;v2 的副本與故障契約見下一節。
getAeoContent wraps fetch with AbortSignal.timeout(2000) and Next.js ISR (next: { revalidate: 3600, tags: ['smking:aeo:<path>'] }):
- Cache hit (the common path): zero network. Tags allow webhook-driven invalidation.
- Cache miss + Page Zero healthy: one network roundtrip, response cached for 1h.
- 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. - 5xx / 4xx / parse error: same fail-open path.
No circuit breaker, no retry, no status command — Next.js infrastructure already covers what those would do.
API
v2 選用模式與限制
這一版要求 Next.js 16、Node.js 執行環境;一般 App Router 與可選 Cache Components 分別驗證。Next.js 15 請繼續使用穩定版 0.22.2,不能忽略新版的相容性要求。安裝不會自動改成 v2,只有明確設定 SMKING_CONTENT_API_VERSION=2 才切換;正式來源與站台須先完成啟用驗收。
- 已驗證正文存於 Next.js 資料快取,來源讀取經過 Page Zero CDN;不新增客戶資料庫。
- 可用的原生暖副本可繼續服務,但 SDK 不保證失效、淘汰或部署後仍有副本。宿主快取與來源都不可用時,AEO 不注入,CMS 明確失敗。CMS 確認不存在或撤回時回 404,不以空 200 假裝正文可用。
- 簽章 webhook 是主要更新機制:普通更新使用
revalidateTag(tag, "max");明確撤回使用{ expire: 0 }並失效對應路徑。通知不讀取來源、比對快取版本或承諾頁面已更新;漏通知由 TTL 補償,歷史撤回後的普通更新也走原生重新驗證。revalidate: 0表示直接讀取,不提供暖副本保護。 - 既有便利入口遇明確失權時,以來源/金鑰隔離的共同標籤傳播;
after()完成前仍有窗口。自架多程序須提供支援共用標籤的快取處理器;外部頁面 CDN 若快取 HTML,須另行完成失效驗證。SDK 不強迫改為動態渲染,也不控制客戶額外 CDN。 fresh_until/usable_until在採納來源回應時核對,並不把已驗證宿主副本的年齡當成撤回。宿主快取期限決定重新驗證頻率;確認撤回與失權另行處理,不能靠較短 TTL 代替。
0.23.0-beta.3 統一頁面與通知在不同編譯入口的快取身分,使用原生 Function.bind 回呼、明確版本化的資源 keyParts,以及來源/金鑰/識別參數。升級會使用新命名空間,必須先確認來源健康並重新暖機必要公開頁面,不宣稱舊 beta 副本可直接沿用。維護者修改驗證或結果契約時須提升命名空間版本;升級 Next.js 或 Node.js 須重跑 Webpack、Turbopack 執行矩陣,不只檢查建置。
0.23.0 沿用已驗收的 0.23.0-beta.4 單一原生快取實作,不含未發布的雙副本試作。來源恢復後靠後續請求/重新驗證取得內容,不必重新編輯文章。Cache Components 是客戶選擇,不是 SDK 安裝前提。純讀取入口和兩種範例見 Next.js 原生快取整合。
無訪客的健康回報
未發布修正的頁面觀測由 smkingProxy 使用 after() 非阻塞送出,不附帶在內容渲染。低流量健康回報另安裝受保護的入口,不假造訪客路徑:
// app/api/smking/health/route.ts
export { GET } from "@soloworks/smking-next/health";
export const runtime = "nodejs";設定 CRON_SECRET(獨立隨機密鑰)、SMKING_SITE_URL=https://你的站台,以及既有的 SMKING_BASE_URL、SMKING_API_KEY、SMKING_WEBHOOK_SECRET。SMKING_SITE_URL 必須是與來源登記一致的 HTTPS origin,不包含路徑、查詢或帳密;所有密鑰只放宿主環境變數,不能提交或使用 NEXT_PUBLIC_。
Vercel 或外部排程以 Authorization: Bearer <CRON_SECRET> 呼叫該 GET。入口只送簽章 POST 健康回報,paths=[],不讀公開內容、不觸發 AI、不新增 revision。只在來源接受回報時回 200;設定缺漏/回報失敗回 503,未授權回 401,所有回應 no-store。失敗交由下次排程重試,不能把一次登記當作已持久排程。
依宿主方案支援的頻率設定 vercel.json(以下為每日;較密頻率須先確認方案限制):
{
"crons": [{ "path": "/api/smking/health", "schedule": "0 0 * * *" }]
}依 Vercel 官方說明,排程會帶 CRON_SECRET 的 Bearer 標頭,失敗不自動重試,Preview 不會自動執行;測試來源若搭配宿主正式部署排程,來源仍可維持隔離 Preview。上線前須核對無訪客時實際執行、回報落庫時間與失敗監控;每日範例不代表符合站台健康告警時限。
<SmkingAEO /> props
interface SmkingAEOProps {
apiKey: string; // required
baseUrl?: string; // override SMKING_BASE_URL env
path?: string; // explicit path; auto-resolved from headers() otherwise
url?: string; // explicit absolute request URL when path is overridden
revalidate?: number; // ISR seconds; default 3600 (1h)
includeSeo?: boolean; // false when using withSmkingMetadata()
}Path auto-detection works for any URL schema (/products/[slug], /shop/[cat]/[id]/[variant]). Pass path explicitly only for static-export contexts where headers() is unavailable.
getAeoContent(params)
Lower-level helper if you want to fetch the AEO response and render yourself. Same params, returns Promise<AeoResponse | null>.
import { getAeoContent } from '@soloworks/smking-next';
const aeo = await getAeoContent({ apiKey: ..., path: '/products/abc' });
if (aeo?.status === 'ready') {
// aeo.jsonLd, aeo.faq, aeo.summary, aeo.seo, aeo.chatLinks, ...
}Publish webhook
The public installation guide describes
app/api/smking/webhook/route.ts, which re-exports POST from
@soloworks/smking-next/webhook. The handler verifies the
SMKING_WEBHOOK_SECRET HMAC and invalidates the affected AEO or Blog cache
tags.
Mount the runtime once (v0.16.1+)
Add <SmkingRuntime /> once in your root layout — it emits a <link> to the saas-served CSS and a <script async> to the bundled Web Component runtime IIFE. Browser caches both per saas-controlled stale-while-revalidate headers, so the cost amortises across every <SmkingCms> instance on the page.
// app/layout.tsx
import { SmkingRuntime } from "@soloworks/smking-next";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<SmkingRuntime apiKey={process.env.SMKING_API_KEY!} />
{children}
</body>
</html>
);
}Optional baseUrl prop overrides process.env.SMKING_BASE_URL. Default: https://getpagezero.com.
Without <SmkingRuntime />, <SmkingCms> content still renders but the Tailwind utility classes from the dashboard's cva variants resolve to dead strings. Follow the public guide to add this mount; the connection CLI does not edit layouts.
Page Zero Blog (optional)
If you publish Blog pages from the Page Zero dashboard, render them with the
<SmkingCms slug="…" /> Server Component.
Choose a URL path such as /blog, /knowledge, or /shop/articles and set
the same value as SMKING_CMS_PATH. Configure it according to the public guide.
Root AEO requests skip that path so <SmkingCms> is the only source of SEO and
JSON-LD on Blog pages.
Optional catch-all route (handles the CMS root + nested slugs)
Page Zero CMS slugs can be nested, and the CMS root uses an empty slug. Use
Next.js optional catch-all [[...slug]] so one route handles /blog, flat
slugs, and every nested depth:
// app/blog/[[...slug]]/page.tsx
import { SmkingCms, withSmkingCmsMetadata } from "@soloworks/smking-next/cms";
import type { ResolvingMetadata } from "next";
type Props = {
params: Promise<{ slug?: string[] }>;
};
async function cmsParams({ params }: Props) {
const { slug } = await params;
return { apiKey: process.env.SMKING_API_KEY!, slug: slug?.join("/") ?? "" };
}
export async function generateMetadata(props: Props, parent: ResolvingMetadata) {
return withSmkingCmsMetadata({}, await cmsParams(props), parent);
}
export default async function Page(props: Props) {
return <SmkingCms {...(await cmsParams(props))} includeSeo={false} />;
}The native CMS metadata helper requires 0.23.2 or newer. Keep
layout defaults: pass parent to preserve nested Open Graph fields and language
alternates. generateMetadata overrides populated CMS SEO fields and
includeSeo={false} disables duplicate inline tags without removing body or
JSON-LD. Published content and Draft Mode use the same request-local read for
metadata and body. Existing installs keep inline SEO by default.
At /blog, params.slug is undefined and maps to the empty CMS slug. Flat
and nested paths map to "hello" and "seo/intro" respectively. A required
[...slug] would miss the CMS root.
Markup contract for CSS
<article class="smk-cms" data-smking="cms">
<h1 class="smk-cms__title">…</h1>
<p>standard prose</p>
<h2>headings</h2>
<ul>
<li>lists</li>
</ul>
<blockquote>…</blockquote>
<pre><code>code blocks</code></pre>
<a href="…">links</a>
<img src="…" alt="…" />
<div
data-type="gallery"
data-layout="grid"
data-columns="3"
class="smk-gallery smk-gallery--grid"
>
<figure class="smk-gallery__item">
<img src="…" alt="…" loading="lazy" />
<figcaption>optional</figcaption>
</figure>
</div>
</article>Standard nodes inherit your site's prose styling. Gallery is the only opinionated structure — provide CSS for .smk-gallery (typically a grid with grid-template-columns: repeat(var(--smk-gallery-cols, 3), 1fr) since the SDK injects --smk-gallery-cols inline).
Cache invalidation
CMS responses cache for 5 minutes by default via Next.js data cache (tags: ["smking:cms_page:<slug>"]). When you publish, rename, or archive a page in the dashboard, smking SaaS POSTs a signed webhook to https://<your-site>/api/smking/webhook (mount with one-line re-export — see Install). The handler verifies HMAC against SMKING_WEBHOOK_SECRET and calls revalidateTag so the next visitor reads fresh content.
If SMKING_WEBHOOK_SECRET is unset, the webhook route returns 503 and cache invalidation falls back to TTL-based expiry.
Versions
See CHANGELOG.md. Aligned with smking/laravel for SaaS-side parity.
License
MIT
