@soloworks/smking-next 0.16.1 → 0.18.0

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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # @soloworks/smking-next
2
2
 
3
+ ## 0.18.0 — 2026-05-28
4
+
5
+ **CMS live-URL reporting — `<SmkingCms>` now tells the dashboard each page's real public URL.**
6
+
7
+ ### Added
8
+
9
+ - `getCmsPage()` appends `sdk` / `sdk_version` / `url` / `path` / `app_env` query params to its `GET /api/v1/public/page` (mirroring the AEO heartbeat). saas stores the real per-slug `pageUrl` — so the dashboard "View page ↗" link points at the actual customer URL instead of a guessed domain — and upserts `sdk_health_snapshots` with the CMS-side heartbeat.
10
+ - `getCmsPage({ url, path })` accept optional overrides; `<SmkingCms>` resolves them from `next/headers` when omitted (same pattern as `<SmkingAEO>`).
11
+
12
+ ### Changed
13
+
14
+ - `SDK_VERSION` moved into `version.ts`, now the single source shared by `client.ts` (AEO) and `cms-client.ts` (CMS) so the reported heartbeat version can't drift between the two call sites.
15
+
16
+ ### Why
17
+
18
+ CMS reads are high-frequency and ISR-cached, so the request stays a `GET` — switching to POST to carry a body would break the data cache. Piggybacking query params keeps the cache while letting saas learn each page's real URL + liveness.
19
+
20
+ ## 0.17.0 — 2026-05-26
21
+
22
+ **SDK self-report (heartbeat) — dashboard SDK Health chip stops getting WAF false-negatives.**
23
+
24
+ ### Added
25
+
26
+ - Every `getAeoContent()` POST now piggybacks an `sdk_meta` field on the request body so saas can upsert `sdk_health_snapshots` with `source='sdk_heartbeat'`. The dashboard "SDK Health" chip reads this signal as primary, falling back to active probe only when fresh (<24h) heartbeat is absent.
27
+ - Payload shape: `{ sdk: "next", sdk_version, app_env, host }`. All fields optional saas-side; host is parsed from the request URL via `new URL(url).hostname`.
28
+
29
+ ### Why
30
+
31
+ Customer Cloudflare WAFs commonly block saas's Vercel ASN egress (Vercel + Googlebot UA = reverse-DNS mismatch → 403 reject). Active probe from saas → customer site reliably fails on protected sites like sleepytofu.com even when SDK is fully operational. SDK-initiated heartbeat bypasses the WAF because the customer site is the originator — saas never needs to fetch back.
32
+
33
+ ### Migration
34
+
35
+ No customer action required. Patch your SDK and the dashboard chip updates automatically on the next page render that triggers `getAeoContent`. Saas accepts the field since 2026-05; older saas versions ignore unknown body fields silently.
36
+
37
+ Minor bump — backward-compatible feature. `^0.16` consumers must update their constraint to `^0.17`.
38
+
3
39
  ## 0.16.1 — 2026-05-25
4
40
 
5
41
  **New `<SmkingRuntime />` component — one-time mount in customer root layout to load saas-served CSS + Web Component runtime JS.**
package/README.md CHANGED
@@ -84,6 +84,30 @@ 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
+ ## Mount the runtime once (v0.16.1+)
88
+
89
+ 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.
90
+
91
+ ```tsx
92
+ // app/layout.tsx
93
+ import { SmkingRuntime } from "@soloworks/smking-next";
94
+
95
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
96
+ return (
97
+ <html>
98
+ <body>
99
+ <SmkingRuntime />
100
+ {children}
101
+ </body>
102
+ </html>
103
+ );
104
+ }
105
+ ```
106
+
107
+ Optional `baseUrl` prop overrides `process.env.SMKING_BASE_URL`. Default: `https://smking.app`.
108
+
109
+ Without `<SmkingRuntime />`, `<SmkingCms>` content still renders but the Tailwind utility classes from the dashboard's cva variants resolve to dead strings — the page reaches the browser unstyled. The wizard installer auto-adds this mount; if you're upgrading manually from < v0.16.1, add the one line above.
110
+
87
111
  ## CMS rendering (optional, v0.11.0+)
88
112
 
89
113
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soloworks/smking-next",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
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",
package/src/lib/client.ts CHANGED
@@ -4,10 +4,25 @@ import type {} from "next";
4
4
 
5
5
  import type { AeoResponse, DiscoverParams } from "../types";
6
6
  import { normalizePath, resolveRequestPath } from "./path";
7
+ import { SDK_VERSION } from "./version";
7
8
 
8
9
  const DEFAULT_REVALIDATE_SECONDS = 3600;
9
10
  const FETCH_TIMEOUT_MS = 2000;
10
11
 
12
+ /**
13
+ * Extract hostname from a full URL string. Returns null on parse failure
14
+ * (malformed URL would otherwise throw). Used to populate heartbeat
15
+ * `sdk_meta.host` without pulling next/headers into this client.
16
+ */
17
+ function safeHostname(input: string | undefined): string | null {
18
+ if (!input) return null;
19
+ try {
20
+ return new URL(input).hostname;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
11
26
  const _warnedKeys = new Set<string>();
12
27
  function warnOnce(key: string, message: string): void {
13
28
  if (process.env.NODE_ENV === "production") return;
@@ -81,7 +96,22 @@ export async function getAeoContent(
81
96
  const res = await fetch(`${baseUrl}/api/v1/public/aeo`, {
82
97
  method: "POST",
83
98
  headers: { "content-type": "application/json" },
84
- body: JSON.stringify({ key: params.apiKey, path, url }),
99
+ body: JSON.stringify({
100
+ key: params.apiKey,
101
+ path,
102
+ url,
103
+ // Heartbeat piggyback — saas upserts into sdk_health_snapshots so
104
+ // the dashboard can show 'SDK active (last seen Nm ago)' without
105
+ // needing an outbound probe (which Cloudflare WAFs commonly block).
106
+ // Saas accepts this optional field via /api/v1/public/aeo POST since
107
+ // 2026-05; older saas versions silently ignore it.
108
+ sdk_meta: {
109
+ sdk: "next",
110
+ sdk_version: SDK_VERSION,
111
+ app_env: process.env.NODE_ENV ?? null,
112
+ host: safeHostname(url),
113
+ },
114
+ }),
85
115
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
86
116
  next: {
87
117
  revalidate: params.revalidate ?? DEFAULT_REVALIDATE_SECONDS,
@@ -3,6 +3,8 @@
3
3
  import type {} from "next";
4
4
 
5
5
  import type { CmsParams, CmsResponse } from "../types";
6
+ import { resolveRequestPath } from "./path";
7
+ import { SDK_VERSION } from "./version";
6
8
 
7
9
  // CMS-specific TTL — shorter than AEO's 1h because CMS body is
8
10
  // hand-edited (frequent updates) vs AEO which is SaaS-generated
@@ -72,11 +74,37 @@ export async function getCmsPage(
72
74
  return null;
73
75
  }
74
76
 
77
+ let requestPath = params.path;
78
+ let requestUrl = params.url;
79
+ if (!requestPath || !requestUrl) {
80
+ try {
81
+ const resolved = await resolveRequestPath();
82
+ if (!requestPath) requestPath = resolved.path;
83
+ if (!requestUrl) requestUrl = resolved.url;
84
+ } catch {
85
+ // Fallback silently if outside request scope
86
+ }
87
+ }
88
+
75
89
  try {
76
- const url = `${baseUrl}/api/v1/public/page?key=${encodeURIComponent(
90
+ let apiUrl = `${baseUrl}/api/v1/public/page?key=${encodeURIComponent(
77
91
  params.apiKey,
78
92
  )}&slug=${encodeURIComponent(params.slug)}`;
79
- const res = await fetch(url, {
93
+
94
+ if (requestPath) {
95
+ apiUrl += `&path=${encodeURIComponent(requestPath)}`;
96
+ }
97
+ if (requestUrl) {
98
+ apiUrl += `&url=${encodeURIComponent(requestUrl)}`;
99
+ }
100
+
101
+ apiUrl += `&sdk=next&sdk_version=${encodeURIComponent(SDK_VERSION)}`;
102
+ const appEnv = params.appEnv ?? process.env.NODE_ENV;
103
+ if (appEnv) {
104
+ apiUrl += `&app_env=${encodeURIComponent(appEnv)}`;
105
+ }
106
+
107
+ const res = await fetch(apiUrl, {
80
108
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
81
109
  next: {
82
110
  revalidate: params.revalidate ?? DEFAULT_REVALIDATE_SECONDS,
@@ -0,0 +1 @@
1
+ export const SDK_VERSION = "0.18.0";
package/src/types.ts CHANGED
@@ -225,6 +225,12 @@ export interface CmsParams {
225
225
  * and changes more often; matches `smking/laravel`'s `cms_ttl`).
226
226
  */
227
227
  revalidate?: number;
228
+ /** Optional current request URL for live preview link on the SaaS dashboard. */
229
+ url?: string;
230
+ /** Optional request path. */
231
+ path?: string;
232
+ /** Optional app environment (e.g. 'development', 'production'). */
233
+ appEnv?: string;
228
234
  }
229
235
 
230
236
  export interface DiscoverParams {