@puzzle-ui/seo 0.0.0-stage → 0.5.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 +755 -2
- package/dist/LICENSE +21 -0
- package/dist/context/config.cjs +82 -0
- package/dist/context/config.cjs.map +1 -0
- package/dist/context/config.d.cts +6 -0
- package/dist/context/config.d.ts +6 -0
- package/dist/context/config.js +80 -0
- package/dist/context/config.js.map +1 -0
- package/dist/context/index.cjs +6 -0
- package/dist/context/index.d.cts +4 -0
- package/dist/context/index.d.ts +4 -0
- package/dist/context/index.js +3 -0
- package/dist/context/types.d.cts +46 -0
- package/dist/context/types.d.ts +46 -0
- package/dist/core/graph.cjs +464 -0
- package/dist/core/graph.cjs.map +1 -0
- package/dist/core/graph.d.cts +62 -0
- package/dist/core/graph.d.ts +62 -0
- package/dist/core/graph.js +456 -0
- package/dist/core/graph.js.map +1 -0
- package/dist/core/index.cjs +11 -0
- package/dist/core/index.d.cts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +1 -0
- package/dist/core/types.d.cts +92 -0
- package/dist/core/types.d.ts +92 -0
- package/dist/index.cjs +96 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata/index.cjs +4 -0
- package/dist/metadata/index.d.cts +3 -0
- package/dist/metadata/index.d.ts +3 -0
- package/dist/metadata/index.js +1 -0
- package/dist/metadata/resolve.cjs +122 -0
- package/dist/metadata/resolve.cjs.map +1 -0
- package/dist/metadata/resolve.d.cts +4 -0
- package/dist/metadata/resolve.d.ts +4 -0
- package/dist/metadata/resolve.js +121 -0
- package/dist/metadata/resolve.js.map +1 -0
- package/dist/metadata/types.d.cts +82 -0
- package/dist/metadata/types.d.ts +82 -0
- package/dist/next/index.cjs +13 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +15 -0
- package/dist/next/index.d.ts +14 -0
- package/dist/next/index.js +12 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/context.cjs +30 -0
- package/dist/react/context.cjs.map +1 -0
- package/dist/react/context.d.cts +14 -0
- package/dist/react/context.d.ts +14 -0
- package/dist/react/context.js +27 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/index.cjs +48 -0
- package/dist/react/index.d.cts +6 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +5 -0
- package/dist/react/metadata.cjs +100 -0
- package/dist/react/metadata.cjs.map +1 -0
- package/dist/react/metadata.d.cts +18 -0
- package/dist/react/metadata.d.ts +18 -0
- package/dist/react/metadata.js +99 -0
- package/dist/react/metadata.js.map +1 -0
- package/dist/react/provider.cjs +42 -0
- package/dist/react/provider.cjs.map +1 -0
- package/dist/react/provider.d.cts +3 -0
- package/dist/react/provider.d.ts +3 -0
- package/dist/react/provider.js +42 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/schema-components.cjs +294 -0
- package/dist/react/schema-components.cjs.map +1 -0
- package/dist/react/schema-components.d.cts +84 -0
- package/dist/react/schema-components.d.ts +80 -0
- package/dist/react/schema-components.js +254 -0
- package/dist/react/schema-components.js.map +1 -0
- package/dist/react/types.d.cts +81 -0
- package/dist/react/types.d.ts +80 -0
- package/dist/react-router/index.cjs +13 -0
- package/dist/react-router/index.cjs.map +1 -0
- package/dist/react-router/index.d.cts +21 -0
- package/dist/react-router/index.d.ts +20 -0
- package/dist/react-router/index.js +12 -0
- package/dist/react-router/index.js.map +1 -0
- package/dist/robots/generate.cjs +88 -0
- package/dist/robots/generate.cjs.map +1 -0
- package/dist/robots/generate.d.cts +4 -0
- package/dist/robots/generate.d.ts +4 -0
- package/dist/robots/generate.js +86 -0
- package/dist/robots/generate.js.map +1 -0
- package/dist/robots/index.cjs +5 -0
- package/dist/robots/index.d.cts +3 -0
- package/dist/robots/index.d.ts +3 -0
- package/dist/robots/index.js +1 -0
- package/dist/robots/types.d.cts +27 -0
- package/dist/robots/types.d.ts +27 -0
- package/dist/schema/create-registry.cjs +12 -0
- package/dist/schema/create-registry.cjs.map +1 -0
- package/dist/schema/create-registry.d.cts +2 -0
- package/dist/schema/create-registry.d.ts +2 -0
- package/dist/schema/create-registry.js +12 -0
- package/dist/schema/create-registry.js.map +1 -0
- package/dist/schema/generated/index.d.cts +2 -0
- package/dist/schema/generated/schemaorg.cjs +36386 -0
- package/dist/schema/generated/schemaorg.cjs.map +1 -0
- package/dist/schema/generated/schemaorg.d.cts +945 -0
- package/dist/schema/generated/schemaorg.d.ts +945 -0
- package/dist/schema/generated/schemaorg.js +36384 -0
- package/dist/schema/generated/schemaorg.js.map +1 -0
- package/dist/schema/index.cjs +11 -0
- package/dist/schema/index.d.cts +7 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/registry.cjs +194 -0
- package/dist/schema/registry.cjs.map +1 -0
- package/dist/schema/registry.d.cts +2 -0
- package/dist/schema/registry.d.ts +2 -0
- package/dist/schema/registry.js +194 -0
- package/dist/schema/registry.js.map +1 -0
- package/dist/schema/types.d.cts +79 -0
- package/dist/schema/types.d.ts +79 -0
- package/dist/schema/validate.cjs +131 -0
- package/dist/schema/validate.cjs.map +1 -0
- package/dist/schema/validate.d.cts +16 -0
- package/dist/schema/validate.d.ts +16 -0
- package/dist/schema/validate.js +131 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/server/handlers.cjs +96 -0
- package/dist/server/handlers.cjs.map +1 -0
- package/dist/server/handlers.d.cts +3 -0
- package/dist/server/handlers.d.ts +3 -0
- package/dist/server/handlers.js +95 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/index.cjs +6 -0
- package/dist/server/index.d.cts +4 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +3 -0
- package/dist/server/routes.cjs +17 -0
- package/dist/server/routes.cjs.map +1 -0
- package/dist/server/routes.d.cts +2 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +17 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/server/types.d.cts +61 -0
- package/dist/server/types.d.ts +61 -0
- package/dist/sitemap/generate.cjs +147 -0
- package/dist/sitemap/generate.cjs.map +1 -0
- package/dist/sitemap/generate.d.cts +3 -0
- package/dist/sitemap/generate.d.ts +3 -0
- package/dist/sitemap/generate.js +146 -0
- package/dist/sitemap/generate.js.map +1 -0
- package/dist/sitemap/index.cjs +11 -0
- package/dist/sitemap/index.d.cts +4 -0
- package/dist/sitemap/index.d.ts +4 -0
- package/dist/sitemap/index.js +3 -0
- package/dist/sitemap/types.d.cts +82 -0
- package/dist/sitemap/types.d.ts +82 -0
- package/dist/sitemap/xml.cjs +215 -0
- package/dist/sitemap/xml.cjs.map +1 -0
- package/dist/sitemap/xml.d.cts +16 -0
- package/dist/sitemap/xml.d.ts +16 -0
- package/dist/sitemap/xml.js +210 -0
- package/dist/sitemap/xml.js.map +1 -0
- package/dist/tanstack/index.cjs +13 -0
- package/dist/tanstack/index.cjs.map +1 -0
- package/dist/tanstack/index.d.cts +19 -0
- package/dist/tanstack/index.d.ts +18 -0
- package/dist/tanstack/index.js +12 -0
- package/dist/tanstack/index.js.map +1 -0
- package/dist/testing/diagnostics.cjs +27 -0
- package/dist/testing/diagnostics.cjs.map +1 -0
- package/dist/testing/diagnostics.d.cts +3 -0
- package/dist/testing/diagnostics.d.ts +3 -0
- package/dist/testing/diagnostics.js +27 -0
- package/dist/testing/diagnostics.js.map +1 -0
- package/dist/testing/fixture.cjs +80 -0
- package/dist/testing/fixture.cjs.map +1 -0
- package/dist/testing/fixture.d.cts +4 -0
- package/dist/testing/fixture.d.ts +4 -0
- package/dist/testing/fixture.js +78 -0
- package/dist/testing/fixture.js.map +1 -0
- package/dist/testing/index.cjs +9 -0
- package/dist/testing/index.d.cts +5 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/inspect.cjs +63 -0
- package/dist/testing/inspect.cjs.map +1 -0
- package/dist/testing/inspect.d.cts +5 -0
- package/dist/testing/inspect.d.ts +5 -0
- package/dist/testing/inspect.js +63 -0
- package/dist/testing/inspect.js.map +1 -0
- package/dist/testing/types.d.cts +40 -0
- package/dist/testing/types.d.ts +40 -0
- package/dist/urls/index.cjs +8 -0
- package/dist/urls/index.d.cts +3 -0
- package/dist/urls/index.d.ts +3 -0
- package/dist/urls/index.js +1 -0
- package/dist/urls/normalize.cjs +65 -0
- package/dist/urls/normalize.cjs.map +1 -0
- package/dist/urls/normalize.d.cts +14 -0
- package/dist/urls/normalize.d.ts +14 -0
- package/dist/urls/normalize.js +60 -0
- package/dist/urls/normalize.js.map +1 -0
- package/dist/urls/types.d.cts +22 -0
- package/dist/urls/types.d.ts +22 -0
- package/dist/validation/index.cjs +5 -0
- package/dist/validation/index.d.cts +3 -0
- package/dist/validation/index.d.ts +3 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/types.d.cts +75 -0
- package/dist/validation/types.d.ts +75 -0
- package/dist/validation/validate.cjs +452 -0
- package/dist/validation/validate.cjs.map +1 -0
- package/dist/validation/validate.d.cts +7 -0
- package/dist/validation/validate.d.ts +6 -0
- package/dist/validation/validate.js +450 -0
- package/dist/validation/validate.js.map +1 -0
- package/package.json +203 -4
package/README.md
CHANGED
|
@@ -1,3 +1,756 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @puzzle-ui/seo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-independent SEO toolkit for Puzzle.
|
|
4
|
+
|
|
5
|
+
The package provides:
|
|
6
|
+
|
|
7
|
+
- Deterministic Schema.org graph generation.
|
|
8
|
+
- A generated Schema.org registry.
|
|
9
|
+
- Schema validation.
|
|
10
|
+
- JSON-LD serialization.
|
|
11
|
+
- React components for structured data.
|
|
12
|
+
- Server-compatible rendering.
|
|
13
|
+
- Sitemap and robots.txt utilities.
|
|
14
|
+
|
|
15
|
+
## Graph Engine
|
|
16
|
+
|
|
17
|
+
The Graph Engine provides a deterministic JSON-LD graph builder with
|
|
18
|
+
node normalization, ID generation, reference resolution, merging,
|
|
19
|
+
validation and safe serialization.
|
|
20
|
+
|
|
21
|
+
### Creating nodes
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createSeoNode } from "@puzzle-ui/seo";
|
|
25
|
+
|
|
26
|
+
const organization = createSeoNode(
|
|
27
|
+
{
|
|
28
|
+
"@type": "Organization",
|
|
29
|
+
name: "Example",
|
|
30
|
+
url: "https://example.com",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
baseUrl: "https://example.com",
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Node IDs are generated deterministically using the node type and
|
|
39
|
+
available identity properties.
|
|
40
|
+
|
|
41
|
+
An explicit ID overrides the generated ID.
|
|
42
|
+
|
|
43
|
+
### Building a graph
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createSeoGraphBuilder } from "@puzzle-ui/seo";
|
|
47
|
+
|
|
48
|
+
const builder = createSeoGraphBuilder({
|
|
49
|
+
baseUrl: "https://example.com",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
builder.add({
|
|
53
|
+
"@type": "Organization",
|
|
54
|
+
name: "Example",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
builder.add({
|
|
58
|
+
"@type": "WebSite",
|
|
59
|
+
name: "Example Website",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const graph = builder.build();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Every builder instance maintains its own state.
|
|
66
|
+
|
|
67
|
+
### Inline nodes
|
|
68
|
+
|
|
69
|
+
Nested nodes are extracted into independent graph nodes.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const graph = createSeoGraphBuilder({
|
|
73
|
+
baseUrl: "https://example.com",
|
|
74
|
+
})
|
|
75
|
+
.add({
|
|
76
|
+
"@type": "WebPage",
|
|
77
|
+
name: "Home",
|
|
78
|
+
author: {
|
|
79
|
+
"@type": "Person",
|
|
80
|
+
name: "John Doe",
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
.build();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The author is represented by an @id reference in the WebPage
|
|
87
|
+
and registered as an independent graph node.
|
|
88
|
+
|
|
89
|
+
### References
|
|
90
|
+
|
|
91
|
+
References can point to nodes added before or after the referencing
|
|
92
|
+
node. Self-references and cyclic references are supported through @id.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const graph = buildSeoGraph(
|
|
96
|
+
[
|
|
97
|
+
{
|
|
98
|
+
"@type": "WebPage",
|
|
99
|
+
"@id": "https://example.com/#page",
|
|
100
|
+
publisher: {
|
|
101
|
+
"@id": "https://example.com/#organization",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"@type": "Organization",
|
|
106
|
+
"@id": "https://example.com/#organization",
|
|
107
|
+
name: "Example",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
{
|
|
111
|
+
baseUrl: "https://example.com",
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Merging
|
|
117
|
+
|
|
118
|
+
Nodes sharing the same @id are deduplicated and merged.
|
|
119
|
+
|
|
120
|
+
Complementary properties are combined, arrays are deduplicated
|
|
121
|
+
and conflicting scalar values are reported.
|
|
122
|
+
|
|
123
|
+
IDs and Schema.org types are normalized before merging. Distinct types
|
|
124
|
+
are combined without conflicts. Conflict properties use dotted paths,
|
|
125
|
+
such as name or address.addressLocality.
|
|
126
|
+
|
|
127
|
+
The default conflict strategy is deterministic.
|
|
128
|
+
|
|
129
|
+
Available strategies:
|
|
130
|
+
|
|
131
|
+
- deterministic: resolve conflicts consistently, independent of
|
|
132
|
+
insertion order.
|
|
133
|
+
- first: retain the first conflicting value.
|
|
134
|
+
- last: retain the last conflicting value.
|
|
135
|
+
- error: report conflicting values as validation errors.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const builder = createSeoGraphBuilder({
|
|
139
|
+
baseUrl: "https://example.com",
|
|
140
|
+
conflictStrategy: "error",
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Validation
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { validateSeoGraph } from "@puzzle-ui/seo";
|
|
148
|
+
|
|
149
|
+
const result = validateSeoGraph(graph, {
|
|
150
|
+
baseUrl: "https://example.com",
|
|
151
|
+
strictReferences: true,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!result.valid) {
|
|
155
|
+
console.error(result.issues);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Validation covers graph structure, node IDs, duplicate IDs,
|
|
160
|
+
node types and unresolved local references.
|
|
161
|
+
|
|
162
|
+
External references are allowed by default.
|
|
163
|
+
|
|
164
|
+
### Serialization
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { serializeSeoGraph } from "@puzzle-ui/seo";
|
|
168
|
+
|
|
169
|
+
const result = serializeSeoGraph(graph, {
|
|
170
|
+
pretty: true,
|
|
171
|
+
scriptSafe: true,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (result.errors.length > 0) {
|
|
175
|
+
console.error(result.errors);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const jsonLd = result.value;
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Serialization is deterministic and escapes HTML-sensitive
|
|
182
|
+
characters by default.
|
|
183
|
+
|
|
184
|
+
Unicode is preserved.
|
|
185
|
+
|
|
186
|
+
## React Schema Components
|
|
187
|
+
|
|
188
|
+
The React integration provides a central provider and a collection
|
|
189
|
+
of Schema.org components.
|
|
190
|
+
|
|
191
|
+
All components register their data in one graph. The provider renders
|
|
192
|
+
one JSON-LD script after its children have registered their nodes.
|
|
193
|
+
|
|
194
|
+
This avoids multiple independent JSON-LD scripts and allows nodes
|
|
195
|
+
registered in different components to reference and merge with each other.
|
|
196
|
+
|
|
197
|
+
### Provider setup
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import {
|
|
201
|
+
SeoProvider,
|
|
202
|
+
LDOrganization,
|
|
203
|
+
LDWebSite,
|
|
204
|
+
LDWebPage,
|
|
205
|
+
} from "@puzzle-ui/seo";
|
|
206
|
+
|
|
207
|
+
const config = {
|
|
208
|
+
siteUrl: "https://example.com",
|
|
209
|
+
identity: {
|
|
210
|
+
name: "Example",
|
|
211
|
+
url: "https://example.com",
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const Root = () => (
|
|
216
|
+
<SeoProvider config={config}>
|
|
217
|
+
<LDOrganization
|
|
218
|
+
id="#organization"
|
|
219
|
+
name="Example"
|
|
220
|
+
url="https://example.com"
|
|
221
|
+
/>
|
|
222
|
+
|
|
223
|
+
<LDWebSite
|
|
224
|
+
id="#website"
|
|
225
|
+
name="Example Website"
|
|
226
|
+
url="https://example.com"
|
|
227
|
+
publisher={{
|
|
228
|
+
"@id": "https://example.com/#organization",
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
|
|
232
|
+
<LDWebPage
|
|
233
|
+
id="#webpage"
|
|
234
|
+
name="Home"
|
|
235
|
+
url="https://example.com"
|
|
236
|
+
isPartOf={{
|
|
237
|
+
"@id": "https://example.com/#website",
|
|
238
|
+
}}
|
|
239
|
+
/>
|
|
240
|
+
</SeoProvider>
|
|
241
|
+
);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The provider creates an isolated graph builder for every render.
|
|
245
|
+
|
|
246
|
+
It accepts the following properties:
|
|
247
|
+
|
|
248
|
+
| Property | Description |
|
|
249
|
+
| -------------- | ---------------------------------- |
|
|
250
|
+
| config | Required SEO configuration |
|
|
251
|
+
| children | Descendant components |
|
|
252
|
+
| scriptId | ID of the central JSON-LD script |
|
|
253
|
+
| nonce | CSP nonce applied to the script |
|
|
254
|
+
| graphOptions | Graph builder options |
|
|
255
|
+
| schemaRegistry | Custom Schema.org registry |
|
|
256
|
+
| validation | Schema validation options or false |
|
|
257
|
+
|
|
258
|
+
The graph builder's base URL defaults to config.siteUrl.
|
|
259
|
+
|
|
260
|
+
### Automatic and explicit IDs
|
|
261
|
+
|
|
262
|
+
Every registered node receives a deterministic ID from the Graph Engine.
|
|
263
|
+
|
|
264
|
+
You can override the generated ID with the id property:
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
<LDOrganization id="#organization" name="Example" />
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The resulting ID is:
|
|
271
|
+
|
|
272
|
+
```text
|
|
273
|
+
https://example.com/#organization
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Use stable identifiers for entities that should be referenced by
|
|
277
|
+
other nodes, such as organizations, websites, authors and pages.
|
|
278
|
+
|
|
279
|
+
## Metadata
|
|
280
|
+
|
|
281
|
+
Puzzle SEO provides a framework-independent metadata resolver
|
|
282
|
+
and React integration for page-level SEO metadata.
|
|
283
|
+
|
|
284
|
+
### Configuration
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { SeoProvider } from "@puzzle-ui/seo";
|
|
288
|
+
|
|
289
|
+
const config = {
|
|
290
|
+
siteUrl: "https://example.com",
|
|
291
|
+
|
|
292
|
+
defaultLocale: "en",
|
|
293
|
+
supportedLocales: ["en", "nl", "de"],
|
|
294
|
+
|
|
295
|
+
title: {
|
|
296
|
+
default: "Example",
|
|
297
|
+
template: "%s | Example",
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
identity: {
|
|
301
|
+
name: "Example",
|
|
302
|
+
description: "Example website",
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Page metadata
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import { SeoMetadata, SeoHead } from "@puzzle-ui/seo";
|
|
311
|
+
|
|
312
|
+
const Page = () => (
|
|
313
|
+
<SeoMetadata
|
|
314
|
+
metadata={{
|
|
315
|
+
title: "About",
|
|
316
|
+
description: "About our company",
|
|
317
|
+
canonical: "https://example.com/about",
|
|
318
|
+
locale: "en",
|
|
319
|
+
|
|
320
|
+
openGraph: {
|
|
321
|
+
type: "website",
|
|
322
|
+
images: [
|
|
323
|
+
{
|
|
324
|
+
url: "/images/social.png",
|
|
325
|
+
width: 1200,
|
|
326
|
+
height: 630,
|
|
327
|
+
alt: "Example",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
twitter: {
|
|
333
|
+
card: "summary_large_image",
|
|
334
|
+
site: "@example",
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
robots: {
|
|
338
|
+
index: true,
|
|
339
|
+
follow: true,
|
|
340
|
+
},
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Metadata output
|
|
347
|
+
|
|
348
|
+
Use `SeoMetadata` to establish a page-level metadata scope
|
|
349
|
+
and render its metadata.
|
|
350
|
+
|
|
351
|
+
Alternatively, provide metadata through SeoProvider and
|
|
352
|
+
render it using SeoHead.
|
|
353
|
+
|
|
354
|
+
Use one metadata output strategy per document.
|
|
355
|
+
|
|
356
|
+
### Supported metadata
|
|
357
|
+
|
|
358
|
+
- Title and title templates.
|
|
359
|
+
- Description and site defaults.
|
|
360
|
+
- Canonical URLs.
|
|
361
|
+
- Robots directives.
|
|
362
|
+
- Open Graph metadata and images.
|
|
363
|
+
- Twitter/X cards and images.
|
|
364
|
+
- Hreflang alternate URLs.
|
|
365
|
+
- Article publication and modification dates.
|
|
366
|
+
- Keywords and author.
|
|
367
|
+
|
|
368
|
+
### Metadata resolution
|
|
369
|
+
|
|
370
|
+
Metadata is resolved using the following priority:
|
|
371
|
+
|
|
372
|
+
1. Explicit page metadata.
|
|
373
|
+
2. Configured metadata defaults.
|
|
374
|
+
3. Site-level defaults.
|
|
375
|
+
4. Identity defaults, where applicable.
|
|
376
|
+
|
|
377
|
+
Page locales are validated against supportedLocales.
|
|
378
|
+
A regional variant such as `nl-NL` is accepted when its
|
|
379
|
+
base language (`nl`) is supported. Locale casing is
|
|
380
|
+
normalized, so `NL-nl` resolves to `nl-NL`.
|
|
381
|
+
|
|
382
|
+
URLs are normalized and restricted to HTTP(S).
|
|
383
|
+
URLs containing credentials are rejected.
|
|
384
|
+
|
|
385
|
+
Canonical URLs are never inferred from siteUrl alone.
|
|
386
|
+
A canonical must be explicitly configured or supplied
|
|
387
|
+
by the page. `og:url` is required by Open Graph, so it
|
|
388
|
+
falls back to the canonical URL and then to the site root.
|
|
389
|
+
|
|
390
|
+
Metadata resolution is framework-independent and can
|
|
391
|
+
be used by server-side integrations.
|
|
392
|
+
|
|
393
|
+
### Available Schema.org components
|
|
394
|
+
|
|
395
|
+
The React integration provides the following components:
|
|
396
|
+
|
|
397
|
+
| Component | Schema.org type |
|
|
398
|
+
| -------------------------------- | ------------------- |
|
|
399
|
+
| LDOrganization | Organization |
|
|
400
|
+
| LDPerson | Person |
|
|
401
|
+
| LDWebSite / LDWebsite | WebSite |
|
|
402
|
+
| LDWebPage | WebPage |
|
|
403
|
+
| LDArticle | Article |
|
|
404
|
+
| LDBlogPosting | BlogPosting |
|
|
405
|
+
| LDBreadcrumbList / LDBreadcrumbs | BreadcrumbList |
|
|
406
|
+
| LDProduct | Product |
|
|
407
|
+
| LDOffer | Offer |
|
|
408
|
+
| LDAggregateRating | AggregateRating |
|
|
409
|
+
| LDReview | Review |
|
|
410
|
+
| LDLocalBusiness | LocalBusiness |
|
|
411
|
+
| LDService | Service |
|
|
412
|
+
| LDImageObject | ImageObject |
|
|
413
|
+
| LDVideoObject | VideoObject |
|
|
414
|
+
| LDEvent | Event |
|
|
415
|
+
| LDRecipe | Recipe |
|
|
416
|
+
| LDCourse | Course |
|
|
417
|
+
| LDJobPosting | JobPosting |
|
|
418
|
+
| LDPlace | Place |
|
|
419
|
+
| LDPostalAddress | PostalAddress |
|
|
420
|
+
| LDContactPoint | ContactPoint |
|
|
421
|
+
| LDSearchAction | SearchAction |
|
|
422
|
+
| LDItemList | ItemList |
|
|
423
|
+
| LDSoftwareApplication | SoftwareApplication |
|
|
424
|
+
| LDHowTo | HowTo |
|
|
425
|
+
| LDFaqPage / LDFAQPage | FAQPage |
|
|
426
|
+
| LDCollectionPage | CollectionPage |
|
|
427
|
+
| LDProfilePage | ProfilePage |
|
|
428
|
+
|
|
429
|
+
### Type-specific properties
|
|
430
|
+
|
|
431
|
+
Component properties are derived from the generated Schema.org
|
|
432
|
+
definitions.
|
|
433
|
+
|
|
434
|
+
The property names include inherited properties from parent types.
|
|
435
|
+
For example, Organization receives the common properties inherited
|
|
436
|
+
from Thing.
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
import {
|
|
440
|
+
LDOrganization,
|
|
441
|
+
LDSearchAction,
|
|
442
|
+
LDPostalAddress,
|
|
443
|
+
} from "@puzzle-ui/seo";
|
|
444
|
+
|
|
445
|
+
const StructuredData = () => (
|
|
446
|
+
<>
|
|
447
|
+
<LDOrganization
|
|
448
|
+
name="Example"
|
|
449
|
+
legalName="Example Ltd"
|
|
450
|
+
url="https://example.com"
|
|
451
|
+
/>
|
|
452
|
+
|
|
453
|
+
<LDSearchAction
|
|
454
|
+
query="required name"
|
|
455
|
+
target={{
|
|
456
|
+
"@type": "EntryPoint",
|
|
457
|
+
urlTemplate: "https://example.com/search?q={search_term_string}",
|
|
458
|
+
}}
|
|
459
|
+
/>
|
|
460
|
+
|
|
461
|
+
<LDPostalAddress
|
|
462
|
+
streetAddress="Main Street 1"
|
|
463
|
+
addressLocality="Madrid"
|
|
464
|
+
postalCode="28001"
|
|
465
|
+
addressCountry="ES"
|
|
466
|
+
/>
|
|
467
|
+
</>
|
|
468
|
+
);
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Properties that do not belong to a component's Schema.org type
|
|
472
|
+
are rejected by TypeScript.
|
|
473
|
+
|
|
474
|
+
`schema:generate` also generates the type-specific property map, including
|
|
475
|
+
inherited properties, so runtime definitions and TypeScript stay in sync.
|
|
476
|
+
|
|
477
|
+
For properties not represented in the generated definitions,
|
|
478
|
+
use the data property:
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
<LDOrganization
|
|
482
|
+
name="Example"
|
|
483
|
+
data={{
|
|
484
|
+
customProperty: "custom-value",
|
|
485
|
+
}}
|
|
486
|
+
/>
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Generic Schema component
|
|
490
|
+
|
|
491
|
+
Use Schema when you need a type that does not have a dedicated
|
|
492
|
+
React component.
|
|
493
|
+
|
|
494
|
+
```tsx
|
|
495
|
+
import { Schema } from "@puzzle-ui/seo";
|
|
496
|
+
|
|
497
|
+
<Schema
|
|
498
|
+
type="CustomType"
|
|
499
|
+
id="#custom"
|
|
500
|
+
data={{
|
|
501
|
+
name: "Example",
|
|
502
|
+
customProperty: "custom-value",
|
|
503
|
+
}}
|
|
504
|
+
/>;
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The generic component accepts arbitrary Schema.org type names.
|
|
508
|
+
The registry still controls validation.
|
|
509
|
+
|
|
510
|
+
### SchemaGraph
|
|
511
|
+
|
|
512
|
+
SchemaGraph registers an existing collection of nodes or a complete
|
|
513
|
+
graph into the provider's central graph.
|
|
514
|
+
|
|
515
|
+
```tsx
|
|
516
|
+
import { SchemaGraph, LDOrganization } from "@puzzle-ui/seo";
|
|
517
|
+
|
|
518
|
+
const nodes = [
|
|
519
|
+
{
|
|
520
|
+
"@type": "Organization",
|
|
521
|
+
"@id": "#organization",
|
|
522
|
+
name: "Example",
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"@type": "WebSite",
|
|
526
|
+
"@id": "#website",
|
|
527
|
+
name: "Example Website",
|
|
528
|
+
},
|
|
529
|
+
] as const;
|
|
530
|
+
|
|
531
|
+
const StructuredData = () => (
|
|
532
|
+
<>
|
|
533
|
+
<SchemaGraph nodes={nodes} />
|
|
534
|
+
|
|
535
|
+
<LDOrganization id="#organization" description="Example organization" />
|
|
536
|
+
</>
|
|
537
|
+
);
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
You can also supply an existing graph:
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
<SchemaGraph graph={existingGraph} />
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
All nodes are registered through the provider's existing graph builder.
|
|
547
|
+
Duplicate IDs are handled by the Graph Engine.
|
|
548
|
+
|
|
549
|
+
### SchemaReference
|
|
550
|
+
|
|
551
|
+
SchemaReference registers a reference entity using its ID.
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
import { SchemaReference, LDWebPage } from "@puzzle-ui/seo";
|
|
555
|
+
|
|
556
|
+
const StructuredData = () => (
|
|
557
|
+
<>
|
|
558
|
+
<SchemaReference id="#organization" type="Organization" />
|
|
559
|
+
|
|
560
|
+
<LDWebPage
|
|
561
|
+
name="About"
|
|
562
|
+
publisher={{
|
|
563
|
+
"@id": "https://example.com/#organization",
|
|
564
|
+
}}
|
|
565
|
+
/>
|
|
566
|
+
</>
|
|
567
|
+
);
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
The type is optional and defaults to Thing.
|
|
571
|
+
|
|
572
|
+
When a complete node with the same ID is registered elsewhere,
|
|
573
|
+
the Graph Engine merges the reference with that node.
|
|
574
|
+
|
|
575
|
+
For references to entities that are already defined in the graph,
|
|
576
|
+
you can also use an ordinary @id object directly in a property.
|
|
577
|
+
|
|
578
|
+
### SchemaScript and SeoJsonLd
|
|
579
|
+
|
|
580
|
+
SchemaScript and SeoJsonLd are aliases for registering existing
|
|
581
|
+
JSON-LD data into the central graph.
|
|
582
|
+
|
|
583
|
+
```tsx
|
|
584
|
+
import { SchemaScript, SeoJsonLd } from "@puzzle-ui/seo";
|
|
585
|
+
|
|
586
|
+
const StructuredData = () => (
|
|
587
|
+
<>
|
|
588
|
+
<SchemaScript
|
|
589
|
+
data={{
|
|
590
|
+
"@type": "Organization",
|
|
591
|
+
name: "Example",
|
|
592
|
+
}}
|
|
593
|
+
/>
|
|
594
|
+
|
|
595
|
+
<SeoJsonLd
|
|
596
|
+
data={[
|
|
597
|
+
{
|
|
598
|
+
"@type": "WebSite",
|
|
599
|
+
name: "Example Website",
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"@type": "WebPage",
|
|
603
|
+
name: "Home",
|
|
604
|
+
},
|
|
605
|
+
]}
|
|
606
|
+
/>
|
|
607
|
+
</>
|
|
608
|
+
);
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Both components support:
|
|
612
|
+
|
|
613
|
+
- A single Schema.org node.
|
|
614
|
+
- An array of Schema.org nodes.
|
|
615
|
+
- A complete graph.
|
|
616
|
+
|
|
617
|
+
Neither component renders an independent script element.
|
|
618
|
+
The provider remains responsible for rendering the central JSON-LD script.
|
|
619
|
+
|
|
620
|
+
### useSeo
|
|
621
|
+
|
|
622
|
+
Use useSeo to access the active SEO configuration and register
|
|
623
|
+
additional nodes from custom components.
|
|
624
|
+
|
|
625
|
+
```tsx
|
|
626
|
+
import { useSeo } from "@puzzle-ui/seo";
|
|
627
|
+
|
|
628
|
+
const CustomStructuredData = () => {
|
|
629
|
+
const seo = useSeo();
|
|
630
|
+
|
|
631
|
+
seo.registerNode({
|
|
632
|
+
"@type": "Organization",
|
|
633
|
+
"@id": "#organization",
|
|
634
|
+
name: seo.config.identity.name,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
return null;
|
|
638
|
+
};
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The hook must be used inside SeoProvider.
|
|
642
|
+
|
|
643
|
+
### Validation
|
|
644
|
+
|
|
645
|
+
Schema validation is enabled by default.
|
|
646
|
+
|
|
647
|
+
Strict validation rejects unknown Schema.org types and properties.
|
|
648
|
+
|
|
649
|
+
````tsx
|
|
650
|
+
<SeoProvider
|
|
651
|
+
config={config}
|
|
652
|
+
validation={{
|
|
653
|
+
strict: true,
|
|
654
|
+
}}
|
|
655
|
+
|
|
656
|
+
> <LDOrganization name="Example" />
|
|
657
|
+
> </SeoProvider>
|
|
658
|
+
> ```
|
|
659
|
+
|
|
660
|
+
Disable validation when integrating custom or non-standard
|
|
661
|
+
Schema.org definitions:
|
|
662
|
+
|
|
663
|
+
```tsx
|
|
664
|
+
<SeoProvider
|
|
665
|
+
config={config}
|
|
666
|
+
validation={false}
|
|
667
|
+
|
|
668
|
+
> <Schema
|
|
669
|
+
|
|
670
|
+
type="CustomType"
|
|
671
|
+
data={{
|
|
672
|
+
name: "Example",
|
|
673
|
+
}}
|
|
674
|
+
|
|
675
|
+
/>
|
|
676
|
+
</SeoProvider>
|
|
677
|
+
````
|
|
678
|
+
|
|
679
|
+
A custom registry can be supplied through schemaRegistry.
|
|
680
|
+
|
|
681
|
+
### CSP nonce
|
|
682
|
+
|
|
683
|
+
The central JSON-LD script supports a Content Security Policy nonce.
|
|
684
|
+
|
|
685
|
+
````tsx
|
|
686
|
+
<SeoProvider
|
|
687
|
+
config={config}
|
|
688
|
+
nonce={requestNonce}
|
|
689
|
+
> <LDOrganization name="Example" />
|
|
690
|
+
</SeoProvider>
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
The nonce is applied to the single central script element.
|
|
694
|
+
|
|
695
|
+
## API
|
|
696
|
+
|
|
697
|
+
### Graph Engine
|
|
698
|
+
|
|
699
|
+
| Function | Description |
|
|
700
|
+
| --------------------- | ---------------------------------------------- |
|
|
701
|
+
| createSeoNode | Normalize and create a node |
|
|
702
|
+
| normalizeSeoId | Normalize an absolute, relative or fragment ID |
|
|
703
|
+
| createSeoGraphBuilder | Create an isolated graph builder |
|
|
704
|
+
| buildSeoGraph | Build a graph from multiple nodes |
|
|
705
|
+
| mergeSeoNodes | Merge two nodes sharing an ID |
|
|
706
|
+
| resolveSeoReferences | Normalize an existing graph and its references |
|
|
707
|
+
| validateSeoGraph | Validate graph structure and references |
|
|
708
|
+
| serializeSeoGraph | Serialize JSON-LD and return validation issues |
|
|
709
|
+
| stringifySeoGraph | Serialize JSON-LD directly to a string |
|
|
710
|
+
|
|
711
|
+
### React integration
|
|
712
|
+
|
|
713
|
+
| Export | Description |
|
|
714
|
+
| --------------- | --------------------------------------------------- |
|
|
715
|
+
| SeoProvider | Central graph provider and JSON-LD renderer |
|
|
716
|
+
| Schema | Generic Schema.org node component |
|
|
717
|
+
| SchemaGraph | Register an existing graph or node collection |
|
|
718
|
+
| SchemaReference | Register an entity reference |
|
|
719
|
+
| SchemaScript | Register existing JSON-LD data |
|
|
720
|
+
| SeoJsonLd | Backwards-compatible JSON-LD registration component |
|
|
721
|
+
| useSeo | Access the active SEO configuration and graph |
|
|
722
|
+
|
|
723
|
+
The package also exports all dedicated LD components listed above.
|
|
724
|
+
|
|
725
|
+
## Schema.org source
|
|
726
|
+
|
|
727
|
+
The generated registry is based on Schema.org version 30.1.
|
|
728
|
+
|
|
729
|
+
The generated definitions are maintained separately from the
|
|
730
|
+
hand-written registry extensions.
|
|
731
|
+
|
|
732
|
+
Run the following commands from the package directory:
|
|
733
|
+
|
|
734
|
+
```sh
|
|
735
|
+
bun run schema:generate
|
|
736
|
+
bun run schema:check
|
|
737
|
+
````
|
|
738
|
+
|
|
739
|
+
The check command verifies that the committed generated definitions
|
|
740
|
+
match the pinned Schema.org source data.
|
|
741
|
+
|
|
742
|
+
## Package scripts
|
|
743
|
+
|
|
744
|
+
| Script | Description |
|
|
745
|
+
| ---------------- | ------------------------------------------------- |
|
|
746
|
+
| schema:generate | Generate Schema.org definitions |
|
|
747
|
+
| schema:check | Verify generated Schema.org definitions |
|
|
748
|
+
| build | Check generated definitions and build the package |
|
|
749
|
+
| typecheck | TypeScript source typecheck |
|
|
750
|
+
| typecheck:test | TypeScript test typecheck |
|
|
751
|
+
| test:unit | Run unit tests |
|
|
752
|
+
| test:integration | Build and run integration tests |
|
|
753
|
+
| test:boundaries | Run package boundary tests |
|
|
754
|
+
| test:package | Run package integration tests |
|
|
755
|
+
| test:schema | Run schema-related typechecks |
|
|
756
|
+
| vitest | Run the package test suites |
|