@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.
Files changed (220) hide show
  1. package/README.md +755 -2
  2. package/dist/LICENSE +21 -0
  3. package/dist/context/config.cjs +82 -0
  4. package/dist/context/config.cjs.map +1 -0
  5. package/dist/context/config.d.cts +6 -0
  6. package/dist/context/config.d.ts +6 -0
  7. package/dist/context/config.js +80 -0
  8. package/dist/context/config.js.map +1 -0
  9. package/dist/context/index.cjs +6 -0
  10. package/dist/context/index.d.cts +4 -0
  11. package/dist/context/index.d.ts +4 -0
  12. package/dist/context/index.js +3 -0
  13. package/dist/context/types.d.cts +46 -0
  14. package/dist/context/types.d.ts +46 -0
  15. package/dist/core/graph.cjs +464 -0
  16. package/dist/core/graph.cjs.map +1 -0
  17. package/dist/core/graph.d.cts +62 -0
  18. package/dist/core/graph.d.ts +62 -0
  19. package/dist/core/graph.js +456 -0
  20. package/dist/core/graph.js.map +1 -0
  21. package/dist/core/index.cjs +11 -0
  22. package/dist/core/index.d.cts +3 -0
  23. package/dist/core/index.d.ts +3 -0
  24. package/dist/core/index.js +1 -0
  25. package/dist/core/types.d.cts +92 -0
  26. package/dist/core/types.d.ts +92 -0
  27. package/dist/index.cjs +96 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +48 -0
  30. package/dist/index.d.ts +38 -0
  31. package/dist/index.js +19 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/metadata/index.cjs +4 -0
  34. package/dist/metadata/index.d.cts +3 -0
  35. package/dist/metadata/index.d.ts +3 -0
  36. package/dist/metadata/index.js +1 -0
  37. package/dist/metadata/resolve.cjs +122 -0
  38. package/dist/metadata/resolve.cjs.map +1 -0
  39. package/dist/metadata/resolve.d.cts +4 -0
  40. package/dist/metadata/resolve.d.ts +4 -0
  41. package/dist/metadata/resolve.js +121 -0
  42. package/dist/metadata/resolve.js.map +1 -0
  43. package/dist/metadata/types.d.cts +82 -0
  44. package/dist/metadata/types.d.ts +82 -0
  45. package/dist/next/index.cjs +13 -0
  46. package/dist/next/index.cjs.map +1 -0
  47. package/dist/next/index.d.cts +15 -0
  48. package/dist/next/index.d.ts +14 -0
  49. package/dist/next/index.js +12 -0
  50. package/dist/next/index.js.map +1 -0
  51. package/dist/react/context.cjs +30 -0
  52. package/dist/react/context.cjs.map +1 -0
  53. package/dist/react/context.d.cts +14 -0
  54. package/dist/react/context.d.ts +14 -0
  55. package/dist/react/context.js +27 -0
  56. package/dist/react/context.js.map +1 -0
  57. package/dist/react/index.cjs +48 -0
  58. package/dist/react/index.d.cts +6 -0
  59. package/dist/react/index.d.ts +6 -0
  60. package/dist/react/index.js +5 -0
  61. package/dist/react/metadata.cjs +100 -0
  62. package/dist/react/metadata.cjs.map +1 -0
  63. package/dist/react/metadata.d.cts +18 -0
  64. package/dist/react/metadata.d.ts +18 -0
  65. package/dist/react/metadata.js +99 -0
  66. package/dist/react/metadata.js.map +1 -0
  67. package/dist/react/provider.cjs +42 -0
  68. package/dist/react/provider.cjs.map +1 -0
  69. package/dist/react/provider.d.cts +3 -0
  70. package/dist/react/provider.d.ts +3 -0
  71. package/dist/react/provider.js +42 -0
  72. package/dist/react/provider.js.map +1 -0
  73. package/dist/react/schema-components.cjs +294 -0
  74. package/dist/react/schema-components.cjs.map +1 -0
  75. package/dist/react/schema-components.d.cts +84 -0
  76. package/dist/react/schema-components.d.ts +80 -0
  77. package/dist/react/schema-components.js +254 -0
  78. package/dist/react/schema-components.js.map +1 -0
  79. package/dist/react/types.d.cts +81 -0
  80. package/dist/react/types.d.ts +80 -0
  81. package/dist/react-router/index.cjs +13 -0
  82. package/dist/react-router/index.cjs.map +1 -0
  83. package/dist/react-router/index.d.cts +21 -0
  84. package/dist/react-router/index.d.ts +20 -0
  85. package/dist/react-router/index.js +12 -0
  86. package/dist/react-router/index.js.map +1 -0
  87. package/dist/robots/generate.cjs +88 -0
  88. package/dist/robots/generate.cjs.map +1 -0
  89. package/dist/robots/generate.d.cts +4 -0
  90. package/dist/robots/generate.d.ts +4 -0
  91. package/dist/robots/generate.js +86 -0
  92. package/dist/robots/generate.js.map +1 -0
  93. package/dist/robots/index.cjs +5 -0
  94. package/dist/robots/index.d.cts +3 -0
  95. package/dist/robots/index.d.ts +3 -0
  96. package/dist/robots/index.js +1 -0
  97. package/dist/robots/types.d.cts +27 -0
  98. package/dist/robots/types.d.ts +27 -0
  99. package/dist/schema/create-registry.cjs +12 -0
  100. package/dist/schema/create-registry.cjs.map +1 -0
  101. package/dist/schema/create-registry.d.cts +2 -0
  102. package/dist/schema/create-registry.d.ts +2 -0
  103. package/dist/schema/create-registry.js +12 -0
  104. package/dist/schema/create-registry.js.map +1 -0
  105. package/dist/schema/generated/index.d.cts +2 -0
  106. package/dist/schema/generated/schemaorg.cjs +36386 -0
  107. package/dist/schema/generated/schemaorg.cjs.map +1 -0
  108. package/dist/schema/generated/schemaorg.d.cts +945 -0
  109. package/dist/schema/generated/schemaorg.d.ts +945 -0
  110. package/dist/schema/generated/schemaorg.js +36384 -0
  111. package/dist/schema/generated/schemaorg.js.map +1 -0
  112. package/dist/schema/index.cjs +11 -0
  113. package/dist/schema/index.d.cts +7 -0
  114. package/dist/schema/index.d.ts +6 -0
  115. package/dist/schema/index.js +5 -0
  116. package/dist/schema/registry.cjs +194 -0
  117. package/dist/schema/registry.cjs.map +1 -0
  118. package/dist/schema/registry.d.cts +2 -0
  119. package/dist/schema/registry.d.ts +2 -0
  120. package/dist/schema/registry.js +194 -0
  121. package/dist/schema/registry.js.map +1 -0
  122. package/dist/schema/types.d.cts +79 -0
  123. package/dist/schema/types.d.ts +79 -0
  124. package/dist/schema/validate.cjs +131 -0
  125. package/dist/schema/validate.cjs.map +1 -0
  126. package/dist/schema/validate.d.cts +16 -0
  127. package/dist/schema/validate.d.ts +16 -0
  128. package/dist/schema/validate.js +131 -0
  129. package/dist/schema/validate.js.map +1 -0
  130. package/dist/server/handlers.cjs +96 -0
  131. package/dist/server/handlers.cjs.map +1 -0
  132. package/dist/server/handlers.d.cts +3 -0
  133. package/dist/server/handlers.d.ts +3 -0
  134. package/dist/server/handlers.js +95 -0
  135. package/dist/server/handlers.js.map +1 -0
  136. package/dist/server/index.cjs +6 -0
  137. package/dist/server/index.d.cts +4 -0
  138. package/dist/server/index.d.ts +4 -0
  139. package/dist/server/index.js +3 -0
  140. package/dist/server/routes.cjs +17 -0
  141. package/dist/server/routes.cjs.map +1 -0
  142. package/dist/server/routes.d.cts +2 -0
  143. package/dist/server/routes.d.ts +2 -0
  144. package/dist/server/routes.js +17 -0
  145. package/dist/server/routes.js.map +1 -0
  146. package/dist/server/types.d.cts +61 -0
  147. package/dist/server/types.d.ts +61 -0
  148. package/dist/sitemap/generate.cjs +147 -0
  149. package/dist/sitemap/generate.cjs.map +1 -0
  150. package/dist/sitemap/generate.d.cts +3 -0
  151. package/dist/sitemap/generate.d.ts +3 -0
  152. package/dist/sitemap/generate.js +146 -0
  153. package/dist/sitemap/generate.js.map +1 -0
  154. package/dist/sitemap/index.cjs +11 -0
  155. package/dist/sitemap/index.d.cts +4 -0
  156. package/dist/sitemap/index.d.ts +4 -0
  157. package/dist/sitemap/index.js +3 -0
  158. package/dist/sitemap/types.d.cts +82 -0
  159. package/dist/sitemap/types.d.ts +82 -0
  160. package/dist/sitemap/xml.cjs +215 -0
  161. package/dist/sitemap/xml.cjs.map +1 -0
  162. package/dist/sitemap/xml.d.cts +16 -0
  163. package/dist/sitemap/xml.d.ts +16 -0
  164. package/dist/sitemap/xml.js +210 -0
  165. package/dist/sitemap/xml.js.map +1 -0
  166. package/dist/tanstack/index.cjs +13 -0
  167. package/dist/tanstack/index.cjs.map +1 -0
  168. package/dist/tanstack/index.d.cts +19 -0
  169. package/dist/tanstack/index.d.ts +18 -0
  170. package/dist/tanstack/index.js +12 -0
  171. package/dist/tanstack/index.js.map +1 -0
  172. package/dist/testing/diagnostics.cjs +27 -0
  173. package/dist/testing/diagnostics.cjs.map +1 -0
  174. package/dist/testing/diagnostics.d.cts +3 -0
  175. package/dist/testing/diagnostics.d.ts +3 -0
  176. package/dist/testing/diagnostics.js +27 -0
  177. package/dist/testing/diagnostics.js.map +1 -0
  178. package/dist/testing/fixture.cjs +80 -0
  179. package/dist/testing/fixture.cjs.map +1 -0
  180. package/dist/testing/fixture.d.cts +4 -0
  181. package/dist/testing/fixture.d.ts +4 -0
  182. package/dist/testing/fixture.js +78 -0
  183. package/dist/testing/fixture.js.map +1 -0
  184. package/dist/testing/index.cjs +9 -0
  185. package/dist/testing/index.d.cts +5 -0
  186. package/dist/testing/index.d.ts +5 -0
  187. package/dist/testing/index.js +4 -0
  188. package/dist/testing/inspect.cjs +63 -0
  189. package/dist/testing/inspect.cjs.map +1 -0
  190. package/dist/testing/inspect.d.cts +5 -0
  191. package/dist/testing/inspect.d.ts +5 -0
  192. package/dist/testing/inspect.js +63 -0
  193. package/dist/testing/inspect.js.map +1 -0
  194. package/dist/testing/types.d.cts +40 -0
  195. package/dist/testing/types.d.ts +40 -0
  196. package/dist/urls/index.cjs +8 -0
  197. package/dist/urls/index.d.cts +3 -0
  198. package/dist/urls/index.d.ts +3 -0
  199. package/dist/urls/index.js +1 -0
  200. package/dist/urls/normalize.cjs +65 -0
  201. package/dist/urls/normalize.cjs.map +1 -0
  202. package/dist/urls/normalize.d.cts +14 -0
  203. package/dist/urls/normalize.d.ts +14 -0
  204. package/dist/urls/normalize.js +60 -0
  205. package/dist/urls/normalize.js.map +1 -0
  206. package/dist/urls/types.d.cts +22 -0
  207. package/dist/urls/types.d.ts +22 -0
  208. package/dist/validation/index.cjs +5 -0
  209. package/dist/validation/index.d.cts +3 -0
  210. package/dist/validation/index.d.ts +3 -0
  211. package/dist/validation/index.js +1 -0
  212. package/dist/validation/types.d.cts +75 -0
  213. package/dist/validation/types.d.ts +75 -0
  214. package/dist/validation/validate.cjs +452 -0
  215. package/dist/validation/validate.cjs.map +1 -0
  216. package/dist/validation/validate.d.cts +7 -0
  217. package/dist/validation/validate.d.ts +6 -0
  218. package/dist/validation/validate.js +450 -0
  219. package/dist/validation/validate.js.map +1 -0
  220. package/package.json +203 -4
package/README.md CHANGED
@@ -1,3 +1,756 @@
1
- # Temporary Holding Version
1
+ # @puzzle-ui/seo
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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 |