@soloworks/smking-next 0.20.1 → 0.20.2
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 +24 -0
- package/package.json +5 -1
- package/src/components/smking-cms.tsx +33 -2
- package/src/lib/cms-client.ts +14 -4
- package/src/lib/preview-route.ts +42 -0
- package/src/types.ts +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# @soloworks/smking-next
|
|
2
2
|
|
|
3
|
+
## 0.20.2 — 2026-06-08
|
|
4
|
+
|
|
5
|
+
**Feature (additive — shipped as a patch): CMS draft preview via Next.js Draft Mode.**
|
|
6
|
+
|
|
7
|
+
The dashboard 預覽 button can now show unpublished draft content on the real
|
|
8
|
+
customer page using a short-lived (15min) token. Everything here is additive,
|
|
9
|
+
so customers on `^0.20` get it automatically — no constraint change needed.
|
|
10
|
+
|
|
11
|
+
- New `./preview` export — a Draft Mode route handler. The install wizard
|
|
12
|
+
writes `app/smking-preview/route.ts` for you; to add it by hand:
|
|
13
|
+
```ts
|
|
14
|
+
// app/smking-preview/route.ts
|
|
15
|
+
export { GET } from "@soloworks/smking-next/preview";
|
|
16
|
+
```
|
|
17
|
+
It enables Draft Mode + stashes the token in an httpOnly cookie, then
|
|
18
|
+
redirects to the page.
|
|
19
|
+
- `<SmkingCms>` now renders draftBlocks (status `"preview"`) when Draft Mode
|
|
20
|
+
is on, fetched with `cache: "no-store"`. Published render is unchanged.
|
|
21
|
+
- New `CmsParams.previewToken` + `CmsStatus` gains `"preview"`.
|
|
22
|
+
|
|
23
|
+
**Install-only + fail-open:** pages without the preview route never enter
|
|
24
|
+
Draft Mode → always the published render. Existing installs are unaffected
|
|
25
|
+
until they add the route (re-run `npx @soloworks/smking-wizard` to get it).
|
|
26
|
+
|
|
3
27
|
## 0.20.1 — 2026-06-03
|
|
4
28
|
|
|
5
29
|
**Chore: removed dead carousel / nav-recent-posts / nav-search block types.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soloworks/smking-next",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
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",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"./proxy": {
|
|
39
39
|
"types": "./src/lib/proxy.ts",
|
|
40
40
|
"default": "./src/lib/proxy.ts"
|
|
41
|
+
},
|
|
42
|
+
"./preview": {
|
|
43
|
+
"types": "./src/lib/preview-route.ts",
|
|
44
|
+
"default": "./src/lib/preview-route.ts"
|
|
41
45
|
}
|
|
42
46
|
},
|
|
43
47
|
"bin": {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cookies, draftMode } from "next/headers";
|
|
2
|
+
|
|
1
3
|
import { getCmsPage } from "../lib/cms-client";
|
|
2
4
|
import { safeJson } from "../lib/safe-json";
|
|
3
5
|
import type { CmsParams } from "../types";
|
|
@@ -35,10 +37,39 @@ import type { CmsParams } from "../types";
|
|
|
35
37
|
*
|
|
36
38
|
* Emits `<title>` / `<meta>` / `og:*` / canonical inline; React 19
|
|
37
39
|
* hoists them into `<head>` automatically.
|
|
40
|
+
*
|
|
41
|
+
* Do NOT also mount `<SmkingAEO>` on a page rendered by `<SmkingCms>`:
|
|
42
|
+
* both serve the same engine `@graph` (via `/public/page` and
|
|
43
|
+
* `/public/aeo` respectively), so stacking them emits a duplicate
|
|
44
|
+
* `<script type="application/ld+json">`. Pick one per page — `<SmkingCms>`
|
|
45
|
+
* for smking-hosted CMS pages, `<SmkingAEO>` for your own pages.
|
|
38
46
|
*/
|
|
39
47
|
export async function SmkingCms(props: CmsParams) {
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
// Draft preview: when Next Draft Mode is on (set by the
|
|
49
|
+
// @soloworks/smking-next/preview route), carry the short-lived token from
|
|
50
|
+
// the cookie so getCmsPage hits the draft branch. Install-only + fail-open:
|
|
51
|
+
// pages without the preview route never enable draft mode → published render.
|
|
52
|
+
let previewToken: string | undefined;
|
|
53
|
+
try {
|
|
54
|
+
const { isEnabled } = await draftMode();
|
|
55
|
+
if (isEnabled) {
|
|
56
|
+
previewToken = (await cookies()).get("smking_preview_token")?.value;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// draftMode/cookies unavailable (static export / outside request) →
|
|
60
|
+
// fall through to the published render.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await getCmsPage(
|
|
64
|
+
previewToken ? { ...props, previewToken } : props,
|
|
65
|
+
);
|
|
66
|
+
if (
|
|
67
|
+
!data ||
|
|
68
|
+
(data.status !== "ready" && data.status !== "preview") ||
|
|
69
|
+
!data.page
|
|
70
|
+
) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
42
73
|
|
|
43
74
|
// SEO head tags emit inline. React 19 hoists `<title>` and `<meta>`
|
|
44
75
|
// tags found anywhere in the tree into `<head>` automatically (last
|
package/src/lib/cms-client.ts
CHANGED
|
@@ -103,13 +103,23 @@ export async function getCmsPage(
|
|
|
103
103
|
if (appEnv) {
|
|
104
104
|
apiUrl += `&app_env=${encodeURIComponent(appEnv)}`;
|
|
105
105
|
}
|
|
106
|
+
if (params.previewToken) {
|
|
107
|
+
apiUrl += `&preview_token=${encodeURIComponent(params.previewToken)}`;
|
|
108
|
+
}
|
|
106
109
|
|
|
107
110
|
const res = await fetch(apiUrl, {
|
|
108
111
|
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
112
|
+
// Preview (draft) reads must never hit the ISR data cache — always
|
|
113
|
+
// fresh, and they expire when the token does. Published reads keep
|
|
114
|
+
// the 5min ISR backstop + revalidate tags for webhook invalidation.
|
|
115
|
+
...(params.previewToken
|
|
116
|
+
? { cache: "no-store" as const }
|
|
117
|
+
: {
|
|
118
|
+
next: {
|
|
119
|
+
revalidate: params.revalidate ?? DEFAULT_REVALIDATE_SECONDS,
|
|
120
|
+
tags: [`smking:cms_page:${params.slug}`],
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
113
123
|
});
|
|
114
124
|
if (!res.ok) return null;
|
|
115
125
|
return (await res.json()) as CmsResponse;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cookies, draftMode } from "next/headers";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Drop-in CMS draft preview entry. Customer adds:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // app/smking-preview/route.ts
|
|
9
|
+
* export { GET } from "@soloworks/smking-next/preview";
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Flow: the dashboard 預覽鈕 opens `/smking-preview?token=…&path=/blog/x` →
|
|
13
|
+
* this enables Next.js Draft Mode + stashes the short-lived token in an
|
|
14
|
+
* httpOnly cookie → redirects to the page, which `<SmkingCms>` then renders
|
|
15
|
+
* from draftBlocks (status "preview").
|
|
16
|
+
*
|
|
17
|
+
* Fail-safe / install-only:
|
|
18
|
+
* - No token → plain redirect to the live page (Draft Mode NOT enabled).
|
|
19
|
+
* - Only same-origin relative paths are honored (open-redirect guard):
|
|
20
|
+
* anything not starting with a single "/" redirects to "/".
|
|
21
|
+
* - Sites without this route never enter draft mode → published render.
|
|
22
|
+
*/
|
|
23
|
+
export async function GET(request: Request): Promise<Response> {
|
|
24
|
+
const url = new URL(request.url);
|
|
25
|
+
const token = url.searchParams.get("token");
|
|
26
|
+
const rawPath = url.searchParams.get("path") ?? "/";
|
|
27
|
+
const path =
|
|
28
|
+
rawPath.startsWith("/") && !rawPath.startsWith("//") ? rawPath : "/";
|
|
29
|
+
|
|
30
|
+
if (token) {
|
|
31
|
+
(await draftMode()).enable();
|
|
32
|
+
(await cookies()).set("smking_preview_token", token, {
|
|
33
|
+
httpOnly: true,
|
|
34
|
+
sameSite: "lax",
|
|
35
|
+
path: "/",
|
|
36
|
+
maxAge: 60 * 15,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// redirect() throws NEXT_REDIRECT — expected control flow in route handlers.
|
|
41
|
+
redirect(path);
|
|
42
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface AeoResponse {
|
|
|
54
54
|
|
|
55
55
|
// ── CMS body content (mirrors SaaS /api/v1/public/page) ───────────────
|
|
56
56
|
|
|
57
|
-
export type CmsStatus = "ready" | "pending" | "not_found";
|
|
57
|
+
export type CmsStatus = "ready" | "preview" | "pending" | "not_found";
|
|
58
58
|
|
|
59
59
|
export type CmsContentType = "article" | "landing" | "listing";
|
|
60
60
|
|
|
@@ -141,7 +141,9 @@ export interface CmsPage {
|
|
|
141
141
|
/**
|
|
142
142
|
* v0.M1+ engine-precomputed schema.org @graph. `<SmkingCms>` inlines it
|
|
143
143
|
* as `<script type="application/ld+json">`. Null until the page is
|
|
144
|
-
* republished under the structured-data engine.
|
|
144
|
+
* republished under the structured-data engine. Don't also render
|
|
145
|
+
* `<SmkingAEO>` on the same page — it serves the same @graph and would
|
|
146
|
+
* duplicate the ld+json script.
|
|
145
147
|
*/
|
|
146
148
|
jsonLd?: Record<string, unknown> | null;
|
|
147
149
|
}
|
|
@@ -184,6 +186,14 @@ export interface CmsParams {
|
|
|
184
186
|
path?: string;
|
|
185
187
|
/** Optional app environment (e.g. 'development', 'production'). */
|
|
186
188
|
appEnv?: string;
|
|
189
|
+
/**
|
|
190
|
+
* Short-lived preview JWT (from the dashboard 預覽鈕, carried via the
|
|
191
|
+
* `smking_preview_token` cookie that `@soloworks/smking-next/preview`
|
|
192
|
+
* sets). When present, fetches draftBlocks (status "preview") with
|
|
193
|
+
* `cache: "no-store"`. Absent → normal published render. Install-only:
|
|
194
|
+
* pages without the preview route never see this.
|
|
195
|
+
*/
|
|
196
|
+
previewToken?: string;
|
|
187
197
|
}
|
|
188
198
|
|
|
189
199
|
export interface DiscoverParams {
|