@soloworks/smking-next 0.20.1 → 0.20.3
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 +35 -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 +54 -0
- package/src/types.ts +12 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @soloworks/smking-next
|
|
2
2
|
|
|
3
|
+
## 0.20.3 — 2026-06-08
|
|
4
|
+
|
|
5
|
+
**CMS draft preview URL now carries a visible `?smking_preview=1` marker.**
|
|
6
|
+
|
|
7
|
+
The `/smking-preview` route still enables Next.js Draft Mode + stashes the
|
|
8
|
+
token in an httpOnly cookie, but now redirects to `…/path?smking_preview=1`
|
|
9
|
+
instead of the bare path. This makes a preview tab's URL visibly distinct from
|
|
10
|
+
the published page so authors can tell draft from live at a glance (parity with
|
|
11
|
+
the Laravel SDK). The token stays in the cookie — never the URL. Additive, so
|
|
12
|
+
`^0.20` gets it automatically.
|
|
13
|
+
|
|
14
|
+
## 0.20.2 — 2026-06-08
|
|
15
|
+
|
|
16
|
+
**Feature (additive — shipped as a patch): CMS draft preview via Next.js Draft Mode.**
|
|
17
|
+
|
|
18
|
+
The dashboard 預覽 button can now show unpublished draft content on the real
|
|
19
|
+
customer page using a short-lived (15min) token. Everything here is additive,
|
|
20
|
+
so customers on `^0.20` get it automatically — no constraint change needed.
|
|
21
|
+
|
|
22
|
+
- New `./preview` export — a Draft Mode route handler. The install wizard
|
|
23
|
+
writes `app/smking-preview/route.ts` for you; to add it by hand:
|
|
24
|
+
```ts
|
|
25
|
+
// app/smking-preview/route.ts
|
|
26
|
+
export { GET } from "@soloworks/smking-next/preview";
|
|
27
|
+
```
|
|
28
|
+
It enables Draft Mode + stashes the token in an httpOnly cookie, then
|
|
29
|
+
redirects to the page.
|
|
30
|
+
- `<SmkingCms>` now renders draftBlocks (status `"preview"`) when Draft Mode
|
|
31
|
+
is on, fetched with `cache: "no-store"`. Published render is unchanged.
|
|
32
|
+
- New `CmsParams.previewToken` + `CmsStatus` gains `"preview"`.
|
|
33
|
+
|
|
34
|
+
**Install-only + fail-open:** pages without the preview route never enter
|
|
35
|
+
Draft Mode → always the published render. Existing installs are unaffected
|
|
36
|
+
until they add the route (re-run `npx @soloworks/smking-wizard` to get it).
|
|
37
|
+
|
|
3
38
|
## 0.20.1 — 2026-06-03
|
|
4
39
|
|
|
5
40
|
**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.3",
|
|
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,54 @@
|
|
|
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 `/blog/x?smking_preview=1`, which
|
|
15
|
+
* `<SmkingCms>` then renders from draftBlocks (status "preview").
|
|
16
|
+
*
|
|
17
|
+
* The `?smking_preview=1` marker makes the preview tab's URL visibly distinct
|
|
18
|
+
* from the live page at a glance (parity with the Laravel SDK's
|
|
19
|
+
* `?smking_preview=` URL). The token itself stays in the httpOnly Draft Mode
|
|
20
|
+
* cookie — never the URL — so it can't leak via the address bar / referrer.
|
|
21
|
+
*
|
|
22
|
+
* Fail-safe / install-only:
|
|
23
|
+
* - No token → plain redirect to the live page (Draft Mode NOT enabled).
|
|
24
|
+
* - Only same-origin relative paths are honored (open-redirect guard):
|
|
25
|
+
* anything not starting with a single "/" redirects to "/".
|
|
26
|
+
* - Sites without this route never enter draft mode → published render.
|
|
27
|
+
*/
|
|
28
|
+
export async function GET(request: Request): Promise<Response> {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
const token = url.searchParams.get("token");
|
|
31
|
+
const rawPath = url.searchParams.get("path") ?? "/";
|
|
32
|
+
const path =
|
|
33
|
+
rawPath.startsWith("/") && !rawPath.startsWith("//") ? rawPath : "/";
|
|
34
|
+
|
|
35
|
+
let target = path;
|
|
36
|
+
if (token) {
|
|
37
|
+
(await draftMode()).enable();
|
|
38
|
+
(await cookies()).set("smking_preview_token", token, {
|
|
39
|
+
httpOnly: true,
|
|
40
|
+
sameSite: "lax",
|
|
41
|
+
path: "/",
|
|
42
|
+
maxAge: 60 * 15,
|
|
43
|
+
});
|
|
44
|
+
// Visible marker so the preview tab's URL differs from the live page at a
|
|
45
|
+
// glance (parity with the Laravel SDK). The token rides the httpOnly
|
|
46
|
+
// cookie above — deliberately kept OUT of the URL: it'd be a dead param
|
|
47
|
+
// (<SmkingCms> reads the cookie) and would leak into address bar / referrer.
|
|
48
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
49
|
+
target = `${path}${sep}smking_preview=1`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// redirect() throws NEXT_REDIRECT — expected control flow in route handlers.
|
|
53
|
+
redirect(target);
|
|
54
|
+
}
|
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 {
|