@soloworks/smking-next 0.12.0 → 0.12.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 +19 -0
- package/README.md +65 -0
- package/bin/{install.ts → install.mjs} +82 -32
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @soloworks/smking-next
|
|
2
2
|
|
|
3
|
+
## 0.12.2 — 2026-05-21
|
|
4
|
+
|
|
5
|
+
**Fix: `doctor` now auto-loads `.env.local` + `.env` matching Next.js convention.**
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `npx @soloworks/smking-next doctor` was reporting `SMKING_API_KEY not set` even when the wizard had just written keys to `.env.local`. Cause: standalone Node bin script doesn't get Next.js framework env loading — only `next dev` / `next build` parse `.env.local`. Customer mental model expected doctor to mirror `next dev`.
|
|
10
|
+
- `runDoctor` now calls `process.loadEnvFile()` (Node 20.6+ native, zero deps) for `.env` then `.env.local`. Existing shell env wins (Next.js precedence). Falls through silently if files don't exist or Node is too old — doctor still reports `not set` as it would have anyway.
|
|
11
|
+
|
|
12
|
+
## 0.12.1 — 2026-05-21
|
|
13
|
+
|
|
14
|
+
**Fix: `npx @soloworks/smking-next doctor` failed for all customers.**
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `bin/install.ts` shipped as raw TypeScript on the assumption that Node 22+'s `--experimental-strip-types` would handle it. It doesn't — by design, Node excludes files under `node_modules/` from type-stripping (packages should ship compiled JS). Every `npx @soloworks/smking-next install|doctor` invocation failed with "Type-Stripping is not supported for files under node_modules" the moment a customer's install reached the doctor check. smking-wizard reproduced the bug 3× and filed ticket `dr_e0b7d8e924cd`.
|
|
19
|
+
- Renamed to `bin/install.mjs` with TypeScript syntax stripped (JSDoc type hints retained for editor / dev ergonomics). Pure ESM, no build step, no runtime type stripping needed.
|
|
20
|
+
- Source modules (`src/*.ts`) unchanged — Next.js's bundler handles them at customer build time. Only the standalone-executable bin script needed JS form.
|
|
21
|
+
|
|
3
22
|
## 0.12.0 — 2026-05-15
|
|
4
23
|
|
|
5
24
|
**AI crawler + AI referral telemetry — `smkingProxy` Next.js middleware.**
|
package/README.md
CHANGED
|
@@ -84,6 +84,71 @@ Content-Type: application/json
|
|
|
84
84
|
|
|
85
85
|
Response: `{ revalidated: number, errors: number }`. `errors > 0` means some tags couldn't be revalidated (others still succeeded — partial-success delivery).
|
|
86
86
|
|
|
87
|
+
## CMS rendering (optional, v0.11.0+)
|
|
88
|
+
|
|
89
|
+
The base install only wires AEO. If you author content in the smking dashboard's CMS and want to render it on your Next.js site, use the `<SmkingCms slug="…" />` Server Component.
|
|
90
|
+
|
|
91
|
+
The SDK **does not have a "CMS root" config** — you choose any URL prefix (`/blog`, `/knowledge`, `/shop/articles`) and wire your own route. The component takes a `slug` prop, fetches the published page from `${SMKING_BASE_URL}/api/v1/public/page?slug=…`, and renders the Tiptap ProseMirror JSON as `<article class="smk-cms">…</article>` via `@tiptap/static-renderer/pm/react`. SEO `<title>` / `<meta>` / `og:*` / canonical hoist into `<head>` automatically via React 19.
|
|
92
|
+
|
|
93
|
+
### Catch-all route (handles flat + nested slugs)
|
|
94
|
+
|
|
95
|
+
smking CMS slugs can be nested — e.g. `blog/123`, `blog/seo/intro`. Use Next.js catch-all `[...slug]` (three dots, not single `[slug]`) so one route handles every depth:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// app/blog/[...slug]/page.tsx
|
|
99
|
+
import { SmkingCms } from "@soloworks/smking-next/cms";
|
|
100
|
+
|
|
101
|
+
export default async function Page({
|
|
102
|
+
params,
|
|
103
|
+
}: {
|
|
104
|
+
params: Promise<{ slug: string[] }>;
|
|
105
|
+
}) {
|
|
106
|
+
const { slug } = await params;
|
|
107
|
+
return (
|
|
108
|
+
<SmkingCms
|
|
109
|
+
apiKey={process.env.SMKING_API_KEY!}
|
|
110
|
+
slug={slug.join("/")} // ← array → "blog/seo/intro" matches dashboard slug format
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`[...slug]` accepts both flat (`/blog/hello` → `["hello"]`) and nested (`/blog/seo/intro` → `["seo", "intro"]`). The `.join("/")` reconstructs the dashboard slug string.
|
|
117
|
+
|
|
118
|
+
Single-bracket `[slug]` (without the three dots) **only matches one segment** — pick this if you know your slugs are always flat.
|
|
119
|
+
|
|
120
|
+
### Markup contract for CSS
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<article class="smk-cms" data-smking="cms">
|
|
124
|
+
<h1 class="smk-cms__title">…</h1>
|
|
125
|
+
<p>standard prose</p>
|
|
126
|
+
<h2>headings</h2>
|
|
127
|
+
<ul><li>lists</li></ul>
|
|
128
|
+
<blockquote>…</blockquote>
|
|
129
|
+
<pre><code>code blocks</code></pre>
|
|
130
|
+
<a href="…">links</a>
|
|
131
|
+
<img src="…" alt="…">
|
|
132
|
+
<div data-type="gallery"
|
|
133
|
+
data-layout="grid"
|
|
134
|
+
data-columns="3"
|
|
135
|
+
class="smk-gallery smk-gallery--grid">
|
|
136
|
+
<figure class="smk-gallery__item">
|
|
137
|
+
<img src="…" alt="…" loading="lazy">
|
|
138
|
+
<figcaption>optional</figcaption>
|
|
139
|
+
</figure>
|
|
140
|
+
</div>
|
|
141
|
+
</article>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
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).
|
|
145
|
+
|
|
146
|
+
### Cache invalidation
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
If `SMKING_WEBHOOK_SECRET` is unset, the webhook route returns 503 and cache invalidation falls back to TTL-based expiry.
|
|
151
|
+
|
|
87
152
|
## Versions
|
|
88
153
|
|
|
89
154
|
See [CHANGELOG.md](./CHANGELOG.md). Aligned with `smking/laravel` for SaaS-side parity.
|
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `npx @soloworks/smking-next [install|doctor] [--json]`
|
|
4
4
|
*
|
|
5
|
-
* Two subcommands share this entry point
|
|
6
|
-
*
|
|
5
|
+
* Two subcommands share this entry point. The file ships as plain ESM
|
|
6
|
+
* (`.mjs`) since Node's `--experimental-strip-types` is excluded from
|
|
7
|
+
* files under `node_modules` by design — so `bin/install.ts` (the
|
|
8
|
+
* previous v0.12.0 layout) fails with "Type-Stripping is not supported
|
|
9
|
+
* for files under node_modules" the moment a customer runs `npx
|
|
10
|
+
* @soloworks/smking-next`. Source modules (`src/*.ts`) are still TS —
|
|
11
|
+
* Next.js's bundler handles them at customer build time. Only this
|
|
12
|
+
* standalone-executable bin script needed to drop TS.
|
|
7
13
|
*
|
|
8
14
|
* - **install** (default) — One-shot scaffold for the three takeover
|
|
9
15
|
* drop-in files (sitemap.ts / robots.ts / llms.txt route). Idempotent:
|
|
@@ -19,36 +25,32 @@
|
|
|
19
25
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
26
|
import { join } from "node:path";
|
|
21
27
|
|
|
22
|
-
// ── install (
|
|
28
|
+
// ── install (file scaffold) ─────────────────────────────────────
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
label: string;
|
|
28
|
-
}
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {{ path: string, content: string, label: string }} FileSpec
|
|
32
|
+
*/
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
/** @type {FileSpec[]} */
|
|
35
|
+
const FILES = [
|
|
31
36
|
{
|
|
32
37
|
path: "sitemap.ts",
|
|
33
38
|
label: "sitemap.ts → /sitemap.xml",
|
|
34
|
-
content:
|
|
35
|
-
'export { default } from "@soloworks/smking-next/sitemap";\n',
|
|
39
|
+
content: 'export { default } from "@soloworks/smking-next/sitemap";\n',
|
|
36
40
|
},
|
|
37
41
|
{
|
|
38
42
|
path: "robots.ts",
|
|
39
43
|
label: "robots.ts → /robots.txt",
|
|
40
|
-
content:
|
|
41
|
-
'export { default } from "@soloworks/smking-next/robots";\n',
|
|
44
|
+
content: 'export { default } from "@soloworks/smking-next/robots";\n',
|
|
42
45
|
},
|
|
43
46
|
{
|
|
44
47
|
path: "llms.txt/route.ts",
|
|
45
48
|
label: "llms.txt/route.ts → /llms.txt",
|
|
46
|
-
content:
|
|
47
|
-
'export { GET } from "@soloworks/smking-next/llms-txt";\n',
|
|
49
|
+
content: 'export { GET } from "@soloworks/smking-next/llms-txt";\n',
|
|
48
50
|
},
|
|
49
51
|
];
|
|
50
52
|
|
|
51
|
-
function detectAppDir()
|
|
53
|
+
function detectAppDir() {
|
|
52
54
|
const candidates = ["app", "src/app"];
|
|
53
55
|
for (const c of candidates) {
|
|
54
56
|
if (existsSync(c)) return c;
|
|
@@ -56,7 +58,7 @@ function detectAppDir(): string | null {
|
|
|
56
58
|
return null;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function runInstall()
|
|
61
|
+
function runInstall() {
|
|
60
62
|
const appDir = detectAppDir();
|
|
61
63
|
if (!appDir) {
|
|
62
64
|
console.error(
|
|
@@ -90,15 +92,49 @@ function runInstall(): number {
|
|
|
90
92
|
return 0;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
// ── doctor (
|
|
95
|
+
// ── doctor (self-check) ─────────────────────────────────────────
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Load `.env.local` + `.env` from cwd into `process.env`, matching the
|
|
99
|
+
* Next.js convention so `npx @soloworks/smking-next doctor` sees the same
|
|
100
|
+
* env vars the customer's `next dev` / `next build` would. Standalone
|
|
101
|
+
* Node bin scripts don't get framework env loading for free — without
|
|
102
|
+
* this, doctor would fail "SMKING_API_KEY not set" on every install where
|
|
103
|
+
* the wizard wrote to `.env.local` instead of shell env.
|
|
104
|
+
*
|
|
105
|
+
* `process.loadEnvFile` is available Node 20.6+ (experimental → stable
|
|
106
|
+
* in 21.7+). Next.js 16 requires Node 20+ so the customer environment is
|
|
107
|
+
* guaranteed to have it. Existing process env wins (we don't overwrite —
|
|
108
|
+
* matches Next.js precedence: shell env > .env.local > .env).
|
|
109
|
+
*
|
|
110
|
+
* Files load in reverse-precedence order (lowest first) so later files
|
|
111
|
+
* win when both define the same key. Next.js's actual order: .env.local
|
|
112
|
+
* > .env.development.local > .env.development > .env. We do the minimal
|
|
113
|
+
* pair customers actually use.
|
|
114
|
+
*/
|
|
115
|
+
function loadEnvFiles() {
|
|
116
|
+
for (const file of [".env", ".env.local"]) {
|
|
117
|
+
if (!existsSync(file)) continue;
|
|
118
|
+
try {
|
|
119
|
+
process.loadEnvFile(file);
|
|
120
|
+
} catch {
|
|
121
|
+
// Older Node fallback or unreadable file — silently skip; doctor
|
|
122
|
+
// will report SMKING_API_KEY not set as it would have anyway.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
99
125
|
}
|
|
100
126
|
|
|
101
|
-
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @typedef {{ name: string, status: "pass" | "fail" | "info", detail: string }} DoctorCheck
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} name
|
|
134
|
+
* @param {string} [prefix]
|
|
135
|
+
* @returns {DoctorCheck}
|
|
136
|
+
*/
|
|
137
|
+
function checkEnv(name, prefix) {
|
|
102
138
|
const value = process.env[name];
|
|
103
139
|
if (!value) {
|
|
104
140
|
return {
|
|
@@ -121,7 +157,11 @@ function checkEnv(name: string, prefix?: string): DoctorCheck {
|
|
|
121
157
|
};
|
|
122
158
|
}
|
|
123
159
|
|
|
124
|
-
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} appDir
|
|
162
|
+
* @returns {DoctorCheck}
|
|
163
|
+
*/
|
|
164
|
+
function checkLayoutUsage(appDir) {
|
|
125
165
|
// Mirror Next.js's layout file resolution. Order matters — TS > JS to
|
|
126
166
|
// match what Next.js itself would pick.
|
|
127
167
|
const candidates = [
|
|
@@ -140,9 +180,6 @@ function checkLayoutUsage(appDir: string): DoctorCheck {
|
|
|
140
180
|
}
|
|
141
181
|
|
|
142
182
|
const content = readFileSync(layoutPath, "utf-8");
|
|
143
|
-
// Substring check is intentional rather than AST parsing. A customer
|
|
144
|
-
// commenting it out or aliasing the import shows up as fail/info either
|
|
145
|
-
// way, and AST adds a TypeScript parser dependency for trivial value.
|
|
146
183
|
if (!content.includes("SmkingAEO")) {
|
|
147
184
|
return {
|
|
148
185
|
name: "<SmkingAEO /> in root layout",
|
|
@@ -157,7 +194,10 @@ function checkLayoutUsage(appDir: string): DoctorCheck {
|
|
|
157
194
|
};
|
|
158
195
|
}
|
|
159
196
|
|
|
160
|
-
|
|
197
|
+
/**
|
|
198
|
+
* @returns {Promise<DoctorCheck>}
|
|
199
|
+
*/
|
|
200
|
+
async function checkApiReachable() {
|
|
161
201
|
const apiKey = process.env.SMKING_API_KEY;
|
|
162
202
|
const baseUrl = process.env.SMKING_BASE_URL;
|
|
163
203
|
if (!apiKey || !baseUrl) {
|
|
@@ -222,16 +262,26 @@ async function checkApiReachable(): Promise<DoctorCheck> {
|
|
|
222
262
|
}
|
|
223
263
|
}
|
|
224
264
|
|
|
225
|
-
|
|
265
|
+
/**
|
|
266
|
+
* @param {boolean} jsonOutput
|
|
267
|
+
* @returns {Promise<number>}
|
|
268
|
+
*/
|
|
269
|
+
async function runDoctor(jsonOutput) {
|
|
270
|
+
// Mirror Next.js env loading so doctor sees what `next dev` would.
|
|
271
|
+
// Customers run this from project root; .env.local is the wizard's
|
|
272
|
+
// write target for SMKING_API_KEY + SMKING_BASE_URL.
|
|
273
|
+
loadEnvFiles();
|
|
274
|
+
|
|
226
275
|
const appDir = detectAppDir();
|
|
227
|
-
|
|
276
|
+
/** @type {DoctorCheck[]} */
|
|
277
|
+
const checks = [
|
|
228
278
|
checkEnv("SMKING_API_KEY", "pk_"),
|
|
229
279
|
checkEnv("SMKING_BASE_URL", "http"),
|
|
230
280
|
appDir
|
|
231
281
|
? checkLayoutUsage(appDir)
|
|
232
282
|
: {
|
|
233
283
|
name: "<SmkingAEO /> in root layout",
|
|
234
|
-
status: "info"
|
|
284
|
+
status: "info",
|
|
235
285
|
detail: "no app/ or src/app/ directory found — skipped",
|
|
236
286
|
},
|
|
237
287
|
await checkApiReachable(),
|
|
@@ -272,7 +322,7 @@ async function runDoctor(jsonOutput: boolean): Promise<number> {
|
|
|
272
322
|
|
|
273
323
|
// ── Entry dispatcher ────────────────────────────────────────────
|
|
274
324
|
|
|
275
|
-
async function main()
|
|
325
|
+
async function main() {
|
|
276
326
|
const subcommand = process.argv[2];
|
|
277
327
|
const jsonOutput = process.argv.includes("--json");
|
|
278
328
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soloworks/smking-next",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"bin": {
|
|
44
|
-
"smking-next": "./bin/install.
|
|
44
|
+
"smking-next": "./bin/install.mjs"
|
|
45
45
|
},
|
|
46
46
|
"files": [
|
|
47
47
|
"src",
|