@reblu/site-next 1.3.1 → 1.4.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/README.md +36 -1
- package/dist/json-ld.cjs +56 -0
- package/dist/json-ld.cjs.map +1 -0
- package/dist/json-ld.d.cts +66 -0
- package/dist/json-ld.d.ts +66 -0
- package/dist/json-ld.js +30 -0
- package/dist/json-ld.js.map +1 -0
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @reblu/site-next
|
|
2
2
|
|
|
3
|
-
Next.js glue for Reblu SITEs (REB-418, REB-510).
|
|
3
|
+
Next.js glue for Reblu SITEs (REB-418, REB-510). Four responsibilities, all
|
|
4
4
|
intentionally Next-coupled so the lower SDK layers —
|
|
5
5
|
[`@reblu/site-client`](../site-client), [`@reblu/site-contracts`](../site-contracts),
|
|
6
6
|
[`@reblu/site-ui`](../site-ui) — stay framework-agnostic:
|
|
@@ -13,6 +13,8 @@ intentionally Next-coupled so the lower SDK layers —
|
|
|
13
13
|
client auth components (`LoginForm`, password reset, set-password,
|
|
14
14
|
email confirm, MFA step-up, `AuthLogo`) and the server-only Route Handler
|
|
15
15
|
factory that proxies them to base-api's `/api/site/auth/*`.
|
|
16
|
+
4. **`<JsonLd>`** (`./json-ld`) — safe `application/ld+json` `<script>`
|
|
17
|
+
rendering; see "Structured data (JSON-LD)" below.
|
|
16
18
|
|
|
17
19
|
## Install
|
|
18
20
|
|
|
@@ -123,6 +125,39 @@ export const POST = createProxyRoute('/api/site/auth/login')
|
|
|
123
125
|
`REBLU_API_BASE_URL`/`REBLU_API_KEY` env vars — read lazily per request, not at
|
|
124
126
|
module load.
|
|
125
127
|
|
|
128
|
+
## Structured data (JSON-LD)
|
|
129
|
+
|
|
130
|
+
`@reblu/site-next/json-ld` (REB-622, server-only, dependency-free) renders one
|
|
131
|
+
`application/ld+json` `<script>` tag the safe way:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { JsonLd } from '@reblu/site-next/json-ld'
|
|
135
|
+
|
|
136
|
+
<JsonLd id='page' data={page.jsonLd} />
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A `<script>` body is HTML's **raw-text** context: the parser ends it on the
|
|
140
|
+
first `</script` byte sequence it sees, no matter whether that sequence sits
|
|
141
|
+
inside a JSON string. `JSON.stringify(data)` alone leaves `<` verbatim, so
|
|
142
|
+
tenant-authored data serialised straight into a JSON-LD `<script>` is a
|
|
143
|
+
stored-XSS sink — a title containing `</script><img src=x onerror=alert(1)>`
|
|
144
|
+
breaks out of the element and runs. `jsonLdScriptContent(value)`, which
|
|
145
|
+
`<JsonLd>` wraps, escapes `<`, `>`, `&`, U+2028 and U+2029 to their `\uXXXX`
|
|
146
|
+
JSON form: every escape is a standard JSON character escape, so
|
|
147
|
+
`JSON.parse(jsonLdScriptContent(value))` always recovers `value` unchanged.
|
|
148
|
+
This is an **escaper**, not the strip/allowlist **sanitiser**
|
|
149
|
+
[`sanitizeHtml`](../site-blocks#security-boundaries) in `@reblu/site-blocks`
|
|
150
|
+
covers — that one guards HTML rendered via `dangerouslySetInnerHTML`; this one
|
|
151
|
+
guards the JSON-LD script boundary, a different sink `sanitizeHtml` was never
|
|
152
|
+
built to handle.
|
|
153
|
+
|
|
154
|
+
`<JsonLd>` ships from its own tsup entry, isolated from the root `index`
|
|
155
|
+
barrel the same way `./cache` and `./route-handlers` are: it has zero
|
|
156
|
+
dependencies of its own (no `@reblu/site-ui`, no `next/link`), so importing it
|
|
157
|
+
must never drag in `CtaButtons`/`EditableCta`'s client-component graph.
|
|
158
|
+
Measured: `dist/json-ld.js` is 701 B (451 B gzipped) and its only runtime
|
|
159
|
+
import is `react/jsx-runtime` — no parser, no sanitiser chain.
|
|
160
|
+
|
|
126
161
|
## Build
|
|
127
162
|
|
|
128
163
|
`tsup` → dual ESM/CJS + `.d.ts`. Code-splitting is **off** so each entry keeps its
|
package/dist/json-ld.cjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/json-ld.tsx
|
|
21
|
+
var json_ld_exports = {};
|
|
22
|
+
__export(json_ld_exports, {
|
|
23
|
+
JsonLd: () => JsonLd,
|
|
24
|
+
jsonLdScriptContent: () => jsonLdScriptContent
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(json_ld_exports);
|
|
27
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
28
|
+
var SCRIPT_BOUNDARY_UNSAFE = /[<>&\u2028\u2029]/g;
|
|
29
|
+
var ESCAPES = {
|
|
30
|
+
"<": "\\u003c",
|
|
31
|
+
">": "\\u003e",
|
|
32
|
+
"&": "\\u0026",
|
|
33
|
+
"\u2028": "\\u2028",
|
|
34
|
+
"\u2029": "\\u2029"
|
|
35
|
+
};
|
|
36
|
+
function jsonLdScriptContent(value) {
|
|
37
|
+
const json = JSON.stringify(value) ?? "null";
|
|
38
|
+
return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char]);
|
|
39
|
+
}
|
|
40
|
+
function JsonLd({ id, data }) {
|
|
41
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
42
|
+
"script",
|
|
43
|
+
{
|
|
44
|
+
id: `json-ld-${id}`,
|
|
45
|
+
type: "application/ld+json",
|
|
46
|
+
suppressHydrationWarning: true,
|
|
47
|
+
dangerouslySetInnerHTML: { __html: jsonLdScriptContent(data) }
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
52
|
+
0 && (module.exports = {
|
|
53
|
+
JsonLd,
|
|
54
|
+
jsonLdScriptContent
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=json-ld.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/json-ld.tsx"],"sourcesContent":["/**\n * `<JsonLd>` — safe `application/ld+json` script renderer (REB-622).\n *\n * A `<script>` element is HTML's \"raw text\" context: the parser closes it on the\n * first case-insensitive `</script` byte sequence it sees, no matter whether\n * that sequence sits inside a JSON string. `JSON.stringify` escapes for JSON\n * syntax, not for this HTML boundary, so it leaves `<` verbatim — turning any\n * JSON-LD payload built from tenant-authored data (page title, description,\n * FAQ copy, …) into a stored-XSS sink: a value containing\n * `</script><img src=x onerror=alert(1)>` breaks out of the element and runs.\n *\n * `jsonLdScriptContent` neutralises exactly that boundary while staying\n * byte-for-byte valid JSON: `<` / `>` / `&` are standard JSON\n * character escapes, so `JSON.parse` recovers the original value unchanged —\n * this is an ESCAPER, not a strip/allowlist sanitiser (see `sanitizeHtml` in\n * `@reblu/site-blocks` for that job, at the HTML boundary rather than the\n * script boundary). U+2028/U+2029 are escaped too: both are valid inside a\n * JSON string but are raw JS line terminators, and some `<script>` consumers\n * (older JS parsers, some static analysers) choke on them unescaped.\n *\n * This package, not `@reblu/site-blocks`, is the home for it: `site-blocks`'s\n * main entry bundles `parse5` + the `hast-util-*` chain + `marked` for\n * `sanitizeHtml` with `treeshake: false` — dist/index.js is ~463 KB, and PR #848\n * spent a full round removing exactly that weight (89 KB gzipped) from a client\n * bundle that only needed one HTML-escaping helper. `<JsonLd>` is Next-specific\n * (App Router's `suppressHydrationWarning` pattern) and this 40-line escaper has\n * zero dependencies of its own, so it ships as its OWN tsup entry\n * (`@reblu/site-next/json-ld`) rather than folding into the root `index` barrel\n * — importing it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +\n * `@reblu/site-ui` graph, the same isolation `./cache` and `./route-handlers`\n * already use for the same reason.\n *\n * @see https://nextjs.org/docs/app/guides/json-ld\n */\n\nconst SCRIPT_BOUNDARY_UNSAFE = /[<>&\\u2028\\u2029]/g\n\nconst ESCAPES: Record<string, string> = {\n '<': '\\\\u003c',\n '>': '\\\\u003e',\n '&': '\\\\u0026',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\n/**\n * Serialises `value` for embedding inside `<script type=\"application/ld+json\">`.\n * Escapes `<`, `>`, `&`, U+2028 and U+2029 — every escape is a standard JSON\n * character escape, so `JSON.parse(jsonLdScriptContent(value))` always recovers\n * `value` unchanged. This only ever ADDS escaping; it never strips or rewrites\n * data, unlike an HTML sanitiser.\n *\n * `JSON.stringify(undefined)` returns the JS value `undefined`, not a string\n * (`undefined` has no JSON representation), which would otherwise throw here.\n * `data` is typed `unknown` with zero friction toward `undefined`, so this\n * falls back to the JSON literal `'null'` rather than crashing an entire page\n * render over one missing field.\n */\nexport function jsonLdScriptContent(value: unknown): string {\n const json = JSON.stringify(value) ?? 'null'\n return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char])\n}\n\nexport interface JsonLdProps {\n /** Suffix for the rendered `<script>` id: `json-ld-${id}`. */\n id: string\n /** Structured data to serialise. Any JSON-serialisable value. */\n data: unknown\n}\n\n/**\n * Renders one `application/ld+json` `<script>` tag using the official Next.js\n * App Router pattern. `suppressHydrationWarning` is required here: React warns\n * on a server/client `textContent` mismatch for this element even though there\n * is no real divergence — `dangerouslySetInnerHTML` content is opaque to React's\n * hydration diff.\n */\nexport function JsonLd({ id, data }: JsonLdProps) {\n return (\n <script\n id={`json-ld-${id}`}\n type='application/ld+json'\n suppressHydrationWarning\n dangerouslySetInnerHTML={{ __html: jsonLdScriptContent(data) }}\n />\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+EI;AA5CJ,IAAM,yBAAyB;AAE/B,IAAM,UAAkC;AAAA,EACtC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAeO,SAAS,oBAAoB,OAAwB;AAC1D,QAAM,OAAO,KAAK,UAAU,KAAK,KAAK;AACtC,SAAO,KAAK,QAAQ,wBAAwB,CAAC,SAAS,QAAQ,IAAI,CAAC;AACrE;AAgBO,SAAS,OAAO,EAAE,IAAI,KAAK,GAAgB;AAChD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAI,WAAW,EAAE;AAAA,MACjB,MAAK;AAAA,MACL,0BAAwB;AAAA,MACxB,yBAAyB,EAAE,QAAQ,oBAAoB,IAAI,EAAE;AAAA;AAAA,EAC/D;AAEJ;","names":[]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<JsonLd>` — safe `application/ld+json` script renderer (REB-622).
|
|
5
|
+
*
|
|
6
|
+
* A `<script>` element is HTML's "raw text" context: the parser closes it on the
|
|
7
|
+
* first case-insensitive `</script` byte sequence it sees, no matter whether
|
|
8
|
+
* that sequence sits inside a JSON string. `JSON.stringify` escapes for JSON
|
|
9
|
+
* syntax, not for this HTML boundary, so it leaves `<` verbatim — turning any
|
|
10
|
+
* JSON-LD payload built from tenant-authored data (page title, description,
|
|
11
|
+
* FAQ copy, …) into a stored-XSS sink: a value containing
|
|
12
|
+
* `</script><img src=x onerror=alert(1)>` breaks out of the element and runs.
|
|
13
|
+
*
|
|
14
|
+
* `jsonLdScriptContent` neutralises exactly that boundary while staying
|
|
15
|
+
* byte-for-byte valid JSON: `<` / `>` / `&` are standard JSON
|
|
16
|
+
* character escapes, so `JSON.parse` recovers the original value unchanged —
|
|
17
|
+
* this is an ESCAPER, not a strip/allowlist sanitiser (see `sanitizeHtml` in
|
|
18
|
+
* `@reblu/site-blocks` for that job, at the HTML boundary rather than the
|
|
19
|
+
* script boundary). U+2028/U+2029 are escaped too: both are valid inside a
|
|
20
|
+
* JSON string but are raw JS line terminators, and some `<script>` consumers
|
|
21
|
+
* (older JS parsers, some static analysers) choke on them unescaped.
|
|
22
|
+
*
|
|
23
|
+
* This package, not `@reblu/site-blocks`, is the home for it: `site-blocks`'s
|
|
24
|
+
* main entry bundles `parse5` + the `hast-util-*` chain + `marked` for
|
|
25
|
+
* `sanitizeHtml` with `treeshake: false` — dist/index.js is ~463 KB, and PR #848
|
|
26
|
+
* spent a full round removing exactly that weight (89 KB gzipped) from a client
|
|
27
|
+
* bundle that only needed one HTML-escaping helper. `<JsonLd>` is Next-specific
|
|
28
|
+
* (App Router's `suppressHydrationWarning` pattern) and this 40-line escaper has
|
|
29
|
+
* zero dependencies of its own, so it ships as its OWN tsup entry
|
|
30
|
+
* (`@reblu/site-next/json-ld`) rather than folding into the root `index` barrel
|
|
31
|
+
* — importing it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +
|
|
32
|
+
* `@reblu/site-ui` graph, the same isolation `./cache` and `./route-handlers`
|
|
33
|
+
* already use for the same reason.
|
|
34
|
+
*
|
|
35
|
+
* @see https://nextjs.org/docs/app/guides/json-ld
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Serialises `value` for embedding inside `<script type="application/ld+json">`.
|
|
39
|
+
* Escapes `<`, `>`, `&`, U+2028 and U+2029 — every escape is a standard JSON
|
|
40
|
+
* character escape, so `JSON.parse(jsonLdScriptContent(value))` always recovers
|
|
41
|
+
* `value` unchanged. This only ever ADDS escaping; it never strips or rewrites
|
|
42
|
+
* data, unlike an HTML sanitiser.
|
|
43
|
+
*
|
|
44
|
+
* `JSON.stringify(undefined)` returns the JS value `undefined`, not a string
|
|
45
|
+
* (`undefined` has no JSON representation), which would otherwise throw here.
|
|
46
|
+
* `data` is typed `unknown` with zero friction toward `undefined`, so this
|
|
47
|
+
* falls back to the JSON literal `'null'` rather than crashing an entire page
|
|
48
|
+
* render over one missing field.
|
|
49
|
+
*/
|
|
50
|
+
declare function jsonLdScriptContent(value: unknown): string;
|
|
51
|
+
interface JsonLdProps {
|
|
52
|
+
/** Suffix for the rendered `<script>` id: `json-ld-${id}`. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Structured data to serialise. Any JSON-serialisable value. */
|
|
55
|
+
data: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Renders one `application/ld+json` `<script>` tag using the official Next.js
|
|
59
|
+
* App Router pattern. `suppressHydrationWarning` is required here: React warns
|
|
60
|
+
* on a server/client `textContent` mismatch for this element even though there
|
|
61
|
+
* is no real divergence — `dangerouslySetInnerHTML` content is opaque to React's
|
|
62
|
+
* hydration diff.
|
|
63
|
+
*/
|
|
64
|
+
declare function JsonLd({ id, data }: JsonLdProps): react_jsx_runtime.JSX.Element;
|
|
65
|
+
|
|
66
|
+
export { JsonLd, type JsonLdProps, jsonLdScriptContent };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<JsonLd>` — safe `application/ld+json` script renderer (REB-622).
|
|
5
|
+
*
|
|
6
|
+
* A `<script>` element is HTML's "raw text" context: the parser closes it on the
|
|
7
|
+
* first case-insensitive `</script` byte sequence it sees, no matter whether
|
|
8
|
+
* that sequence sits inside a JSON string. `JSON.stringify` escapes for JSON
|
|
9
|
+
* syntax, not for this HTML boundary, so it leaves `<` verbatim — turning any
|
|
10
|
+
* JSON-LD payload built from tenant-authored data (page title, description,
|
|
11
|
+
* FAQ copy, …) into a stored-XSS sink: a value containing
|
|
12
|
+
* `</script><img src=x onerror=alert(1)>` breaks out of the element and runs.
|
|
13
|
+
*
|
|
14
|
+
* `jsonLdScriptContent` neutralises exactly that boundary while staying
|
|
15
|
+
* byte-for-byte valid JSON: `<` / `>` / `&` are standard JSON
|
|
16
|
+
* character escapes, so `JSON.parse` recovers the original value unchanged —
|
|
17
|
+
* this is an ESCAPER, not a strip/allowlist sanitiser (see `sanitizeHtml` in
|
|
18
|
+
* `@reblu/site-blocks` for that job, at the HTML boundary rather than the
|
|
19
|
+
* script boundary). U+2028/U+2029 are escaped too: both are valid inside a
|
|
20
|
+
* JSON string but are raw JS line terminators, and some `<script>` consumers
|
|
21
|
+
* (older JS parsers, some static analysers) choke on them unescaped.
|
|
22
|
+
*
|
|
23
|
+
* This package, not `@reblu/site-blocks`, is the home for it: `site-blocks`'s
|
|
24
|
+
* main entry bundles `parse5` + the `hast-util-*` chain + `marked` for
|
|
25
|
+
* `sanitizeHtml` with `treeshake: false` — dist/index.js is ~463 KB, and PR #848
|
|
26
|
+
* spent a full round removing exactly that weight (89 KB gzipped) from a client
|
|
27
|
+
* bundle that only needed one HTML-escaping helper. `<JsonLd>` is Next-specific
|
|
28
|
+
* (App Router's `suppressHydrationWarning` pattern) and this 40-line escaper has
|
|
29
|
+
* zero dependencies of its own, so it ships as its OWN tsup entry
|
|
30
|
+
* (`@reblu/site-next/json-ld`) rather than folding into the root `index` barrel
|
|
31
|
+
* — importing it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +
|
|
32
|
+
* `@reblu/site-ui` graph, the same isolation `./cache` and `./route-handlers`
|
|
33
|
+
* already use for the same reason.
|
|
34
|
+
*
|
|
35
|
+
* @see https://nextjs.org/docs/app/guides/json-ld
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Serialises `value` for embedding inside `<script type="application/ld+json">`.
|
|
39
|
+
* Escapes `<`, `>`, `&`, U+2028 and U+2029 — every escape is a standard JSON
|
|
40
|
+
* character escape, so `JSON.parse(jsonLdScriptContent(value))` always recovers
|
|
41
|
+
* `value` unchanged. This only ever ADDS escaping; it never strips or rewrites
|
|
42
|
+
* data, unlike an HTML sanitiser.
|
|
43
|
+
*
|
|
44
|
+
* `JSON.stringify(undefined)` returns the JS value `undefined`, not a string
|
|
45
|
+
* (`undefined` has no JSON representation), which would otherwise throw here.
|
|
46
|
+
* `data` is typed `unknown` with zero friction toward `undefined`, so this
|
|
47
|
+
* falls back to the JSON literal `'null'` rather than crashing an entire page
|
|
48
|
+
* render over one missing field.
|
|
49
|
+
*/
|
|
50
|
+
declare function jsonLdScriptContent(value: unknown): string;
|
|
51
|
+
interface JsonLdProps {
|
|
52
|
+
/** Suffix for the rendered `<script>` id: `json-ld-${id}`. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Structured data to serialise. Any JSON-serialisable value. */
|
|
55
|
+
data: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Renders one `application/ld+json` `<script>` tag using the official Next.js
|
|
59
|
+
* App Router pattern. `suppressHydrationWarning` is required here: React warns
|
|
60
|
+
* on a server/client `textContent` mismatch for this element even though there
|
|
61
|
+
* is no real divergence — `dangerouslySetInnerHTML` content is opaque to React's
|
|
62
|
+
* hydration diff.
|
|
63
|
+
*/
|
|
64
|
+
declare function JsonLd({ id, data }: JsonLdProps): react_jsx_runtime.JSX.Element;
|
|
65
|
+
|
|
66
|
+
export { JsonLd, type JsonLdProps, jsonLdScriptContent };
|
package/dist/json-ld.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/json-ld.tsx
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
var SCRIPT_BOUNDARY_UNSAFE = /[<>&\u2028\u2029]/g;
|
|
4
|
+
var ESCAPES = {
|
|
5
|
+
"<": "\\u003c",
|
|
6
|
+
">": "\\u003e",
|
|
7
|
+
"&": "\\u0026",
|
|
8
|
+
"\u2028": "\\u2028",
|
|
9
|
+
"\u2029": "\\u2029"
|
|
10
|
+
};
|
|
11
|
+
function jsonLdScriptContent(value) {
|
|
12
|
+
const json = JSON.stringify(value) ?? "null";
|
|
13
|
+
return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char]);
|
|
14
|
+
}
|
|
15
|
+
function JsonLd({ id, data }) {
|
|
16
|
+
return /* @__PURE__ */ jsx(
|
|
17
|
+
"script",
|
|
18
|
+
{
|
|
19
|
+
id: `json-ld-${id}`,
|
|
20
|
+
type: "application/ld+json",
|
|
21
|
+
suppressHydrationWarning: true,
|
|
22
|
+
dangerouslySetInnerHTML: { __html: jsonLdScriptContent(data) }
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
JsonLd,
|
|
28
|
+
jsonLdScriptContent
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=json-ld.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/json-ld.tsx"],"sourcesContent":["/**\n * `<JsonLd>` — safe `application/ld+json` script renderer (REB-622).\n *\n * A `<script>` element is HTML's \"raw text\" context: the parser closes it on the\n * first case-insensitive `</script` byte sequence it sees, no matter whether\n * that sequence sits inside a JSON string. `JSON.stringify` escapes for JSON\n * syntax, not for this HTML boundary, so it leaves `<` verbatim — turning any\n * JSON-LD payload built from tenant-authored data (page title, description,\n * FAQ copy, …) into a stored-XSS sink: a value containing\n * `</script><img src=x onerror=alert(1)>` breaks out of the element and runs.\n *\n * `jsonLdScriptContent` neutralises exactly that boundary while staying\n * byte-for-byte valid JSON: `<` / `>` / `&` are standard JSON\n * character escapes, so `JSON.parse` recovers the original value unchanged —\n * this is an ESCAPER, not a strip/allowlist sanitiser (see `sanitizeHtml` in\n * `@reblu/site-blocks` for that job, at the HTML boundary rather than the\n * script boundary). U+2028/U+2029 are escaped too: both are valid inside a\n * JSON string but are raw JS line terminators, and some `<script>` consumers\n * (older JS parsers, some static analysers) choke on them unescaped.\n *\n * This package, not `@reblu/site-blocks`, is the home for it: `site-blocks`'s\n * main entry bundles `parse5` + the `hast-util-*` chain + `marked` for\n * `sanitizeHtml` with `treeshake: false` — dist/index.js is ~463 KB, and PR #848\n * spent a full round removing exactly that weight (89 KB gzipped) from a client\n * bundle that only needed one HTML-escaping helper. `<JsonLd>` is Next-specific\n * (App Router's `suppressHydrationWarning` pattern) and this 40-line escaper has\n * zero dependencies of its own, so it ships as its OWN tsup entry\n * (`@reblu/site-next/json-ld`) rather than folding into the root `index` barrel\n * — importing it must never drag in `CtaButtons`/`EditableCta`'s `next/link` +\n * `@reblu/site-ui` graph, the same isolation `./cache` and `./route-handlers`\n * already use for the same reason.\n *\n * @see https://nextjs.org/docs/app/guides/json-ld\n */\n\nconst SCRIPT_BOUNDARY_UNSAFE = /[<>&\\u2028\\u2029]/g\n\nconst ESCAPES: Record<string, string> = {\n '<': '\\\\u003c',\n '>': '\\\\u003e',\n '&': '\\\\u0026',\n '\\u2028': '\\\\u2028',\n '\\u2029': '\\\\u2029',\n}\n\n/**\n * Serialises `value` for embedding inside `<script type=\"application/ld+json\">`.\n * Escapes `<`, `>`, `&`, U+2028 and U+2029 — every escape is a standard JSON\n * character escape, so `JSON.parse(jsonLdScriptContent(value))` always recovers\n * `value` unchanged. This only ever ADDS escaping; it never strips or rewrites\n * data, unlike an HTML sanitiser.\n *\n * `JSON.stringify(undefined)` returns the JS value `undefined`, not a string\n * (`undefined` has no JSON representation), which would otherwise throw here.\n * `data` is typed `unknown` with zero friction toward `undefined`, so this\n * falls back to the JSON literal `'null'` rather than crashing an entire page\n * render over one missing field.\n */\nexport function jsonLdScriptContent(value: unknown): string {\n const json = JSON.stringify(value) ?? 'null'\n return json.replace(SCRIPT_BOUNDARY_UNSAFE, (char) => ESCAPES[char])\n}\n\nexport interface JsonLdProps {\n /** Suffix for the rendered `<script>` id: `json-ld-${id}`. */\n id: string\n /** Structured data to serialise. Any JSON-serialisable value. */\n data: unknown\n}\n\n/**\n * Renders one `application/ld+json` `<script>` tag using the official Next.js\n * App Router pattern. `suppressHydrationWarning` is required here: React warns\n * on a server/client `textContent` mismatch for this element even though there\n * is no real divergence — `dangerouslySetInnerHTML` content is opaque to React's\n * hydration diff.\n */\nexport function JsonLd({ id, data }: JsonLdProps) {\n return (\n <script\n id={`json-ld-${id}`}\n type='application/ld+json'\n suppressHydrationWarning\n dangerouslySetInnerHTML={{ __html: jsonLdScriptContent(data) }}\n />\n )\n}\n"],"mappings":";AA+EI;AA5CJ,IAAM,yBAAyB;AAE/B,IAAM,UAAkC;AAAA,EACtC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,UAAU;AAAA,EACV,UAAU;AACZ;AAeO,SAAS,oBAAoB,OAAwB;AAC1D,QAAM,OAAO,KAAK,UAAU,KAAK,KAAK;AACtC,SAAO,KAAK,QAAQ,wBAAwB,CAAC,SAAS,QAAQ,IAAI,CAAC;AACrE;AAgBO,SAAS,OAAO,EAAE,IAAI,KAAK,GAAgB;AAChD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,IAAI,WAAW,EAAE;AAAA,MACjB,MAAK;AAAA,MACL,0BAAwB;AAAA,MACxB,yBAAyB,EAAE,QAAQ,oBAAoB,IAAI,EAAE;AAAA;AAAA,EAC/D;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reblu/site-next",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Next.js RSC glue for Reblu SITEs — cache + CTA + auth server/client components.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Reblu — Adolfo Unturbe",
|
|
@@ -80,6 +80,16 @@
|
|
|
80
80
|
"types": "./dist/route-handlers.d.cts",
|
|
81
81
|
"default": "./dist/route-handlers.cjs"
|
|
82
82
|
}
|
|
83
|
+
},
|
|
84
|
+
"./json-ld": {
|
|
85
|
+
"import": {
|
|
86
|
+
"types": "./dist/json-ld.d.ts",
|
|
87
|
+
"default": "./dist/json-ld.js"
|
|
88
|
+
},
|
|
89
|
+
"require": {
|
|
90
|
+
"types": "./dist/json-ld.d.cts",
|
|
91
|
+
"default": "./dist/json-ld.cjs"
|
|
92
|
+
}
|
|
83
93
|
}
|
|
84
94
|
},
|
|
85
95
|
"publishConfig": {
|
|
@@ -91,9 +101,9 @@
|
|
|
91
101
|
"react": ">=19"
|
|
92
102
|
},
|
|
93
103
|
"dependencies": {
|
|
94
|
-
"@reblu/site-client": "^
|
|
104
|
+
"@reblu/site-client": "^3.0.0",
|
|
95
105
|
"@reblu/site-ui": "^0.3.1",
|
|
96
|
-
"@reblu/site-contracts": "^0.
|
|
106
|
+
"@reblu/site-contracts": "^0.4.0"
|
|
97
107
|
},
|
|
98
108
|
"devDependencies": {
|
|
99
109
|
"@testing-library/jest-dom": "^6.9.1",
|