@jdevalk/astro-seo-graph 0.3.1 → 0.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/AGENTS.md +501 -442
- package/package.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -31,13 +31,11 @@ Two packages:
|
|
|
31
31
|
│ @jdevalk/seo-graph-core │
|
|
32
32
|
│ │
|
|
33
33
|
│ makeIds() IdFactory │
|
|
34
|
-
│ buildArticle() buildPerson() │
|
|
35
34
|
│ buildWebSite() buildWebPage() │
|
|
36
|
-
│
|
|
35
|
+
│ buildArticle() buildPiece() │
|
|
37
36
|
│ buildBreadcrumbList() │
|
|
38
37
|
│ buildImageObject() buildVideoObject() │
|
|
39
38
|
│ buildSiteNavigationElement() │
|
|
40
|
-
│ buildCustomPiece() │
|
|
41
39
|
│ assembleGraph() │
|
|
42
40
|
│ deduplicateByGraphId() │
|
|
43
41
|
└──────────────┬────────────────────────────────────┘
|
|
@@ -153,8 +151,11 @@ resolution.
|
|
|
153
151
|
|
|
154
152
|
## Piece builders reference
|
|
155
153
|
|
|
156
|
-
Every builder takes an input object and
|
|
157
|
-
|
|
154
|
+
Every builder takes an input object and returns a `GraphEntity` (a plain object
|
|
155
|
+
with `@type` and usually `@id`). The specialized builders (`buildWebSite`,
|
|
156
|
+
`buildWebPage`, `buildArticle`, etc.) also take the `IdFactory` as a second
|
|
157
|
+
parameter. The generic `buildPiece` builder takes only the input object — you
|
|
158
|
+
set the `@id` directly in the input.
|
|
158
159
|
|
|
159
160
|
### buildWebSite
|
|
160
161
|
|
|
@@ -170,7 +171,7 @@ buildWebSite(
|
|
|
170
171
|
about: { '@id': ids.person }, // optional — what this site is about
|
|
171
172
|
inLanguage: 'en-US', // optional — default content language
|
|
172
173
|
hasPart: { '@id': ids.navigation }, // optional — navigation ref
|
|
173
|
-
|
|
174
|
+
// ...additional schema-dts properties accepted at top level
|
|
174
175
|
},
|
|
175
176
|
ids,
|
|
176
177
|
);
|
|
@@ -178,7 +179,7 @@ buildWebSite(
|
|
|
178
179
|
|
|
179
180
|
**Adding a SearchAction** (recommended for sites with search):
|
|
180
181
|
|
|
181
|
-
|
|
182
|
+
Add a `potentialAction` with a `SearchAction` directly at the top level. This
|
|
182
183
|
tells search engines and agents how to search your site:
|
|
183
184
|
|
|
184
185
|
```ts
|
|
@@ -187,122 +188,24 @@ buildWebSite(
|
|
|
187
188
|
url: 'https://example.com/',
|
|
188
189
|
name: 'My Site',
|
|
189
190
|
publisher: { '@id': ids.person },
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
urlTemplate: 'https://example.com/?s={search_term_string}',
|
|
196
|
-
},
|
|
197
|
-
'query-input': {
|
|
198
|
-
'@type': 'PropertyValueSpecification',
|
|
199
|
-
valueRequired: true,
|
|
200
|
-
valueName: 'search_term_string',
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
ids,
|
|
206
|
-
);
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
This is the pattern used by most WordPress sites and many other CMSes.
|
|
210
|
-
|
|
211
|
-
### buildPerson
|
|
212
|
-
|
|
213
|
-
Creates a `Person` entity. Typically the site owner or author.
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
buildPerson(
|
|
217
|
-
{
|
|
218
|
-
name: 'Jane Doe', // required
|
|
219
|
-
familyName: 'Doe', // optional
|
|
220
|
-
birthDate: '1990-01-15', // optional
|
|
221
|
-
gender: 'female', // optional
|
|
222
|
-
nationality: { '@id': ids.country('US') }, // optional
|
|
223
|
-
description: 'Software engineer...', // optional
|
|
224
|
-
jobTitle: 'Lead Engineer', // optional
|
|
225
|
-
knowsLanguage: ['en', 'es'], // optional
|
|
226
|
-
url: 'https://example.com/about/', // optional
|
|
227
|
-
image: { '@id': ids.personImage }, // optional — ref to ImageObject
|
|
228
|
-
sameAs: [
|
|
229
|
-
// optional — social/professional profiles
|
|
230
|
-
'https://twitter.com/janedoe',
|
|
231
|
-
'https://github.com/janedoe',
|
|
232
|
-
'https://linkedin.com/in/janedoe',
|
|
233
|
-
],
|
|
234
|
-
worksFor: [
|
|
235
|
-
// optional — EmployeeRole objects
|
|
236
|
-
{
|
|
237
|
-
'@type': 'EmployeeRole',
|
|
238
|
-
roleName: 'Lead Engineer',
|
|
239
|
-
startDate: '2022-01-01',
|
|
240
|
-
worksFor: { '@id': ids.organization('acme') },
|
|
191
|
+
potentialAction: {
|
|
192
|
+
'@type': 'SearchAction',
|
|
193
|
+
target: {
|
|
194
|
+
'@type': 'EntryPoint',
|
|
195
|
+
urlTemplate: 'https://example.com/?s={search_term_string}',
|
|
241
196
|
},
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
);
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### buildOrganization
|
|
250
|
-
|
|
251
|
-
Creates an `Organization` or any subtype (`LocalBusiness`, `Restaurant`, etc.).
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
import type { LocalBusiness } from 'schema-dts';
|
|
255
|
-
|
|
256
|
-
// Basic organization
|
|
257
|
-
buildOrganization(
|
|
258
|
-
{
|
|
259
|
-
slug: 'acme', // required — stable slug for @id
|
|
260
|
-
name: 'Acme Corp', // required
|
|
261
|
-
url: 'https://acme.com/', // optional
|
|
262
|
-
description: 'We make things.', // optional
|
|
263
|
-
logo: 'https://acme.com/logo.png', // optional — URL string or ImageObject ref
|
|
264
|
-
sameAs: ['https://twitter.com/acme'], // optional
|
|
265
|
-
extra: {}, // optional — escape hatch
|
|
266
|
-
},
|
|
267
|
-
ids,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
// Subtype (e.g. LocalBusiness, Restaurant, etc.)
|
|
271
|
-
buildOrganization<LocalBusiness>(
|
|
272
|
-
{
|
|
273
|
-
slug: 'my-restaurant',
|
|
274
|
-
name: 'Chez Example',
|
|
275
|
-
url: 'https://example.com/',
|
|
276
|
-
extra: {
|
|
277
|
-
address: {
|
|
278
|
-
'@type': 'PostalAddress',
|
|
279
|
-
streetAddress: '123 Main St',
|
|
280
|
-
addressLocality: 'Springfield',
|
|
281
|
-
addressRegion: 'IL',
|
|
282
|
-
postalCode: '62701',
|
|
283
|
-
addressCountry: 'US',
|
|
197
|
+
'query-input': {
|
|
198
|
+
'@type': 'PropertyValueSpecification',
|
|
199
|
+
valueRequired: true,
|
|
200
|
+
valueName: 'search_term_string',
|
|
284
201
|
},
|
|
285
|
-
telephone: '+1-555-123-4567',
|
|
286
|
-
priceRange: '$$',
|
|
287
|
-
servesCuisine: 'French',
|
|
288
|
-
openingHoursSpecification: [
|
|
289
|
-
{
|
|
290
|
-
'@type': 'OpeningHoursSpecification',
|
|
291
|
-
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
|
|
292
|
-
opens: '11:00',
|
|
293
|
-
closes: '22:00',
|
|
294
|
-
},
|
|
295
|
-
],
|
|
296
202
|
},
|
|
297
203
|
},
|
|
298
204
|
ids,
|
|
299
|
-
'Restaurant',
|
|
300
205
|
);
|
|
301
206
|
```
|
|
302
207
|
|
|
303
|
-
|
|
304
|
-
(e.g. `'Restaurant'`, `'LocalBusiness'`, `'Dentist'`). Use the matching
|
|
305
|
-
`schema-dts` type as the generic parameter for autocomplete on `extra`.
|
|
208
|
+
This is the pattern used by most WordPress sites and many other CMSes.
|
|
306
209
|
|
|
307
210
|
### buildWebPage
|
|
308
211
|
|
|
@@ -326,7 +229,7 @@ buildWebPage(
|
|
|
326
229
|
license: 'https://creativecommons.org/licenses/by/4.0/', // optional — license URL
|
|
327
230
|
isAccessibleForFree: true, // optional
|
|
328
231
|
potentialAction: [], // optional — defaults to ReadAction
|
|
329
|
-
|
|
232
|
+
// ...additional schema-dts properties accepted at top level
|
|
330
233
|
},
|
|
331
234
|
ids,
|
|
332
235
|
'WebPage',
|
|
@@ -361,7 +264,7 @@ buildArticle(
|
|
|
361
264
|
articleSection: 'Technology', // optional — top-level category
|
|
362
265
|
wordCount: 1500, // optional
|
|
363
266
|
articleBody: 'The full text...', // optional — plain text, max ~10K chars
|
|
364
|
-
|
|
267
|
+
// ...additional schema-dts properties accepted at top level
|
|
365
268
|
},
|
|
366
269
|
ids,
|
|
367
270
|
'Article',
|
|
@@ -387,7 +290,7 @@ buildBreadcrumbList({
|
|
|
387
290
|
{ name: 'Blog', url: 'https://example.com/blog/' },
|
|
388
291
|
{ name: 'My Post', url: 'https://example.com/blog/my-post/' },
|
|
389
292
|
],
|
|
390
|
-
|
|
293
|
+
// ...additional schema-dts properties accepted at top level
|
|
391
294
|
}, ids);
|
|
392
295
|
````
|
|
393
296
|
|
|
@@ -411,7 +314,7 @@ buildImageObject(
|
|
|
411
314
|
height: 630, // required
|
|
412
315
|
inLanguage: 'en-US', // optional
|
|
413
316
|
caption: 'A photo of...', // optional
|
|
414
|
-
|
|
317
|
+
// ...additional schema-dts properties accepted at top level
|
|
415
318
|
},
|
|
416
319
|
ids,
|
|
417
320
|
);
|
|
@@ -445,7 +348,7 @@ buildVideoObject(
|
|
|
445
348
|
uploadDate: new Date('2026-01-15'), // optional
|
|
446
349
|
duration: 'PT30M', // optional — ISO 8601
|
|
447
350
|
transcript: 'Full transcript text...', // optional
|
|
448
|
-
|
|
351
|
+
// ...additional schema-dts properties accepted at top level
|
|
449
352
|
},
|
|
450
353
|
ids,
|
|
451
354
|
);
|
|
@@ -471,37 +374,143 @@ buildSiteNavigationElement(
|
|
|
471
374
|
{ name: 'Blog', url: 'https://example.com/blog/' },
|
|
472
375
|
{ name: 'About', url: 'https://example.com/about/' },
|
|
473
376
|
],
|
|
474
|
-
|
|
377
|
+
// ...additional schema-dts properties accepted at top level
|
|
475
378
|
},
|
|
476
379
|
ids,
|
|
477
380
|
);
|
|
478
381
|
```
|
|
479
382
|
|
|
480
|
-
###
|
|
383
|
+
### buildPiece
|
|
384
|
+
|
|
385
|
+
The generic typed builder for any schema.org type. This is the go-to builder
|
|
386
|
+
for `Person`, `Organization`, `Blog`, `Product`, `Recipe`, `Event`, `Course`,
|
|
387
|
+
`SoftwareApplication`, `VacationRental`, `FAQPage`, `PodcastSeries`,
|
|
388
|
+
`PodcastEpisode`, and any other schema.org type not covered by the specialized
|
|
389
|
+
builders (`buildWebSite`, `buildWebPage`, `buildArticle`, etc.).
|
|
481
390
|
|
|
482
|
-
|
|
391
|
+
Pass a `schema-dts` type as the generic parameter for full autocomplete.
|
|
392
|
+
The `@type` value in the input narrows union types to the matching leaf — so
|
|
393
|
+
`buildPiece<Product>` with `'@type': 'Product'` gives `ProductLeaf` autocomplete.
|
|
394
|
+
No need to import Leaf types separately.
|
|
395
|
+
|
|
396
|
+
Callers are responsible for setting `@id` using the `IdFactory` (e.g.
|
|
397
|
+
`ids.person`, `ids.organization('slug')`) or a custom ID string.
|
|
483
398
|
|
|
484
399
|
```ts
|
|
485
|
-
|
|
400
|
+
import type { Person, Organization, Restaurant, Blog, Product, Recipe, Event } from 'schema-dts';
|
|
401
|
+
|
|
402
|
+
// Person (site-wide)
|
|
403
|
+
buildPiece<Person>({
|
|
404
|
+
'@type': 'Person',
|
|
405
|
+
'@id': ids.person,
|
|
406
|
+
name: 'Jane Doe',
|
|
407
|
+
url: 'https://example.com/about/',
|
|
408
|
+
image: { '@id': ids.personImage },
|
|
409
|
+
sameAs: ['https://twitter.com/janedoe', 'https://github.com/janedoe'],
|
|
410
|
+
jobTitle: 'Lead Engineer',
|
|
411
|
+
worksFor: [
|
|
412
|
+
{
|
|
413
|
+
'@type': 'EmployeeRole',
|
|
414
|
+
roleName: 'Lead Engineer',
|
|
415
|
+
startDate: '2022-01-01',
|
|
416
|
+
worksFor: { '@id': ids.organization('acme') },
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Organization
|
|
422
|
+
buildPiece<Organization>({
|
|
423
|
+
'@type': 'Organization',
|
|
424
|
+
'@id': ids.organization('acme'),
|
|
425
|
+
name: 'Acme Corp',
|
|
426
|
+
url: 'https://acme.com/',
|
|
427
|
+
logo: 'https://acme.com/logo.png',
|
|
428
|
+
sameAs: ['https://twitter.com/acme'],
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Organization subtype (e.g. Restaurant) — use the subtype directly as the generic
|
|
432
|
+
buildPiece<Restaurant>({
|
|
433
|
+
'@type': 'Restaurant',
|
|
434
|
+
'@id': ids.organization('chez-example'),
|
|
435
|
+
name: 'Chez Example',
|
|
436
|
+
url: 'https://chezexample.com/',
|
|
437
|
+
servesCuisine: 'French',
|
|
438
|
+
priceRange: '$$$',
|
|
439
|
+
address: {
|
|
440
|
+
'@type': 'PostalAddress',
|
|
441
|
+
streetAddress: '123 Rue de la Paix',
|
|
442
|
+
addressLocality: 'Paris',
|
|
443
|
+
addressCountry: 'FR',
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Product
|
|
448
|
+
buildPiece<Product>({
|
|
449
|
+
'@type': 'Product',
|
|
450
|
+
'@id': `${url}#product`,
|
|
451
|
+
name: 'Running Shoe',
|
|
452
|
+
brand: 'Nike',
|
|
453
|
+
sku: 'ABC123',
|
|
454
|
+
offers: { '@type': 'Offer', price: 99.99, priceCurrency: 'USD' },
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Blog
|
|
458
|
+
buildPiece<Blog>({
|
|
459
|
+
'@type': 'Blog',
|
|
460
|
+
'@id': `${siteUrl}/blog/#blog`,
|
|
461
|
+
name: 'My Blog',
|
|
462
|
+
url: `${siteUrl}/blog/`,
|
|
463
|
+
publisher: { '@id': ids.person },
|
|
464
|
+
inLanguage: 'en-US',
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Recipe
|
|
468
|
+
buildPiece<Recipe>({
|
|
486
469
|
'@type': 'Recipe',
|
|
487
|
-
'@id':
|
|
470
|
+
'@id': `${url}#recipe`,
|
|
488
471
|
name: 'Simple Pasta',
|
|
489
472
|
author: { '@id': ids.person },
|
|
490
|
-
prepTime: '
|
|
473
|
+
prepTime: 'PT10M',
|
|
491
474
|
cookTime: 'PT20M',
|
|
492
|
-
|
|
475
|
+
totalTime: 'PT30M',
|
|
476
|
+
recipeYield: '4 servings',
|
|
477
|
+
recipeCategory: 'Main course',
|
|
478
|
+
recipeCuisine: 'Italian',
|
|
479
|
+
recipeIngredient: ['400g spaghetti', '200g guanciale', '4 egg yolks'],
|
|
493
480
|
recipeInstructions: [
|
|
494
|
-
{
|
|
495
|
-
|
|
496
|
-
text: 'Boil the pasta.',
|
|
497
|
-
},
|
|
481
|
+
{ '@type': 'HowToStep', text: 'Boil the spaghetti.' },
|
|
482
|
+
{ '@type': 'HowToStep', text: 'Fry the guanciale.' },
|
|
498
483
|
],
|
|
499
484
|
});
|
|
485
|
+
|
|
486
|
+
// Event
|
|
487
|
+
buildPiece<Event>({
|
|
488
|
+
'@type': 'Event',
|
|
489
|
+
'@id': 'https://example.com/events/conf/#event',
|
|
490
|
+
name: 'JavaScript Conference 2026',
|
|
491
|
+
startDate: '2026-09-15T09:00:00+02:00',
|
|
492
|
+
endDate: '2026-09-17T18:00:00+02:00',
|
|
493
|
+
location: {
|
|
494
|
+
'@type': 'Place',
|
|
495
|
+
name: 'Congress Center',
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Without a generic, the input is untyped — any properties are accepted:
|
|
501
|
+
|
|
502
|
+
```ts
|
|
503
|
+
buildPiece({
|
|
504
|
+
'@type': 'Event',
|
|
505
|
+
'@id': 'https://example.com/events/conf/#event',
|
|
506
|
+
name: 'JavaScript Conference 2026',
|
|
507
|
+
});
|
|
500
508
|
```
|
|
501
509
|
|
|
502
|
-
**
|
|
503
|
-
|
|
504
|
-
|
|
510
|
+
**Always prefer the typed generic** (`buildPiece<Event>`) over the
|
|
511
|
+
untyped form. The generic gives you autocomplete for every property on the
|
|
512
|
+
chosen type, making it much harder to miss recommended fields like
|
|
513
|
+
`potentialAction`, `geo`, or `offers`.
|
|
505
514
|
|
|
506
515
|
### assembleGraph
|
|
507
516
|
|
|
@@ -523,6 +532,16 @@ const graph = assembleGraph([
|
|
|
523
532
|
**Always call this last.** It handles deduplication: if multiple pages produce
|
|
524
533
|
the same `WebSite` or `Person` entity (same `@id`), the first occurrence wins.
|
|
525
534
|
|
|
535
|
+
**Dangling reference validation:** Pass `warnOnDanglingReferences: true` to
|
|
536
|
+
validate that every `{ '@id': '...' }` reference in the graph resolves to an
|
|
537
|
+
actual entity. This helps catch broken links — for example, a `WebSite`
|
|
538
|
+
referencing a `Person` that was never included in the pieces array.
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
const graph = assembleGraph(pieces, { warnOnDanglingReferences: true });
|
|
542
|
+
// Warns: [seo-graph] Dangling reference in WebSite: { "@id": "..." } does not match any entity in the graph.
|
|
543
|
+
```
|
|
544
|
+
|
|
526
545
|
### deduplicateByGraphId
|
|
527
546
|
|
|
528
547
|
The dedup engine on its own, for custom assembly workflows.
|
|
@@ -549,19 +568,21 @@ about page.
|
|
|
549
568
|
**For every page** (site-wide entities):
|
|
550
569
|
|
|
551
570
|
- `buildWebSite` — publisher points to Person
|
|
552
|
-
- `
|
|
571
|
+
- `buildPiece<Person>` — the blog author
|
|
553
572
|
- `buildImageObject` — person's profile photo (use `id: ids.personImage`)
|
|
554
|
-
- `
|
|
573
|
+
- `buildPiece<Blog>` — a `Blog` entity representing the blog as a publication
|
|
555
574
|
|
|
556
575
|
The `Blog` entity is a `CreativeWork` that represents the blog as a whole,
|
|
557
576
|
separate from the `WebSite`. Individual `BlogPosting` entries reference the
|
|
558
577
|
Blog via `isPartOf`. This is the pattern used by jonoalderson.com.
|
|
559
578
|
|
|
560
579
|
```ts
|
|
580
|
+
import type { Blog } from 'schema-dts';
|
|
581
|
+
|
|
561
582
|
const blogId = `${siteUrl}/blog/#blog`;
|
|
562
583
|
|
|
563
584
|
// Include on every page as a site-wide entity
|
|
564
|
-
|
|
585
|
+
buildPiece<Blog>({
|
|
565
586
|
'@type': 'Blog',
|
|
566
587
|
'@id': blogId,
|
|
567
588
|
name: 'My Blog',
|
|
@@ -577,13 +598,15 @@ buildCustomPiece({
|
|
|
577
598
|
Use `BlogPosting` instead of `Article` and link it to the Blog:
|
|
578
599
|
|
|
579
600
|
```ts
|
|
601
|
+
import type { Person, Blog } from 'schema-dts';
|
|
602
|
+
|
|
580
603
|
const blogId = `${siteUrl}/blog/#blog`;
|
|
581
604
|
|
|
582
605
|
const pieces = [
|
|
583
606
|
buildWebSite({ url: siteUrl, name: 'My Blog', publisher: { '@id': ids.person } }, ids),
|
|
584
|
-
|
|
607
|
+
buildPiece<Person>({ '@type': 'Person', '@id': ids.person, name: 'Jane Doe', url: aboutUrl, image: { '@id': ids.personImage }, sameAs: [...] }),
|
|
585
608
|
buildImageObject({ id: ids.personImage, url: profilePhotoUrl, width: 400, height: 400 }, ids),
|
|
586
|
-
|
|
609
|
+
buildPiece<Blog>({
|
|
587
610
|
'@type': 'Blog',
|
|
588
611
|
'@id': blogId,
|
|
589
612
|
name: 'My Blog',
|
|
@@ -599,11 +622,10 @@ const pieces = [
|
|
|
599
622
|
dateModified,
|
|
600
623
|
author: { '@id': ids.person },
|
|
601
624
|
publisher: { '@id': ids.person },
|
|
602
|
-
isPartOf: { '@id': ids.webPage(url) },
|
|
625
|
+
isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }],
|
|
603
626
|
image: { '@id': ids.primaryImage(url) },
|
|
604
627
|
articleSection: category,
|
|
605
628
|
wordCount,
|
|
606
|
-
extra: { isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }] },
|
|
607
629
|
}, ids, 'BlogPosting'),
|
|
608
630
|
buildBreadcrumbList({ url, items: [{ name: 'Home', url: siteUrl }, { name: 'Blog', url: blogUrl }, { name: title, url }] }, ids),
|
|
609
631
|
buildImageObject({ pageUrl: url, url: featureImageUrl, width: 1200, height: 630 }, ids),
|
|
@@ -611,9 +633,9 @@ const pieces = [
|
|
|
611
633
|
const graph = assembleGraph(pieces);
|
|
612
634
|
```
|
|
613
635
|
|
|
614
|
-
**Note:** The `
|
|
615
|
-
|
|
616
|
-
|
|
636
|
+
**Note:** The `isPartOf` array links the posting to both the `WebPage` and the
|
|
637
|
+
`Blog`. If you don't need the `Blog` link, just use
|
|
638
|
+
`isPartOf: { '@id': ids.webPage(url) }` directly.
|
|
617
639
|
|
|
618
640
|
**Blog listing** (`/blog/`):
|
|
619
641
|
|
|
@@ -715,14 +737,16 @@ A multi-author blog owned by a company.
|
|
|
715
737
|
entities.
|
|
716
738
|
|
|
717
739
|
```ts
|
|
740
|
+
import type { Organization, Blog, Person } from 'schema-dts';
|
|
741
|
+
|
|
718
742
|
const ids = makeIds({ siteUrl: 'https://acme.com' });
|
|
719
743
|
|
|
720
744
|
// Site-wide
|
|
721
745
|
const blogId = 'https://acme.com/blog/#blog';
|
|
722
746
|
const siteEntities = [
|
|
723
|
-
|
|
747
|
+
buildPiece<Organization>({ '@type': 'Organization', '@id': ids.organization('acme'), name: 'Acme Corp', url: 'https://acme.com/', logo: logoUrl, sameAs: [...] }),
|
|
724
748
|
buildWebSite({ url: 'https://acme.com/', name: 'Acme Blog', publisher: { '@id': ids.organization('acme') } }, ids),
|
|
725
|
-
|
|
749
|
+
buildPiece<Blog>({
|
|
726
750
|
'@type': 'Blog',
|
|
727
751
|
'@id': blogId,
|
|
728
752
|
name: 'The Acme Blog',
|
|
@@ -735,7 +759,7 @@ const siteEntities = [
|
|
|
735
759
|
const authorId = 'https://acme.com/team/jane/#person';
|
|
736
760
|
const postPieces = [
|
|
737
761
|
...siteEntities,
|
|
738
|
-
|
|
762
|
+
buildPiece<Person>({ '@type': 'Person', '@id': authorId, name: 'Jane Doe', url: 'https://acme.com/team/jane/' }),
|
|
739
763
|
buildWebPage({ url, name: title, isPartOf: { '@id': ids.website }, datePublished }, ids),
|
|
740
764
|
buildArticle({
|
|
741
765
|
url,
|
|
@@ -744,8 +768,7 @@ const postPieces = [
|
|
|
744
768
|
datePublished,
|
|
745
769
|
author: { '@id': authorId },
|
|
746
770
|
publisher: { '@id': ids.organization('acme') },
|
|
747
|
-
isPartOf: { '@id': ids.webPage(url) },
|
|
748
|
-
extra: { isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }] },
|
|
771
|
+
isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }],
|
|
749
772
|
}, ids, 'BlogPosting'),
|
|
750
773
|
buildBreadcrumbList({ url, items: [{ name: 'Home', url: siteUrl }, { name: 'Blog', url: blogUrl }, { name: title, url }] }, ids),
|
|
751
774
|
];
|
|
@@ -755,16 +778,23 @@ const postPieces = [
|
|
|
755
778
|
|
|
756
779
|
### E-commerce / product page
|
|
757
780
|
|
|
758
|
-
Use `
|
|
759
|
-
builder because product schemas vary wildly.
|
|
781
|
+
Use `buildPiece<Product>` for `Product` and `buildPiece<ProductGroup>` for `ProductGroup` entities.
|
|
760
782
|
|
|
761
783
|
**Simple product (single variant):**
|
|
762
784
|
|
|
763
785
|
```ts
|
|
786
|
+
import type { Organization, Product } from 'schema-dts';
|
|
787
|
+
|
|
764
788
|
const ids = makeIds({ siteUrl: 'https://shop.example.com' });
|
|
765
789
|
|
|
766
790
|
const pieces = [
|
|
767
|
-
|
|
791
|
+
buildPiece<Organization>({
|
|
792
|
+
'@type': 'Organization',
|
|
793
|
+
'@id': ids.organization('shop'),
|
|
794
|
+
name: 'Example Shop',
|
|
795
|
+
url: siteUrl,
|
|
796
|
+
logo: logoUrl,
|
|
797
|
+
}),
|
|
768
798
|
buildWebSite(
|
|
769
799
|
{ url: siteUrl, name: 'Example Shop', publisher: { '@id': ids.organization('shop') } },
|
|
770
800
|
ids,
|
|
@@ -789,13 +819,12 @@ const pieces = [
|
|
|
789
819
|
},
|
|
790
820
|
ids,
|
|
791
821
|
),
|
|
792
|
-
|
|
822
|
+
buildPiece<Product>({
|
|
793
823
|
'@type': 'Product',
|
|
794
824
|
'@id': `${url}#product`,
|
|
795
825
|
name: productName,
|
|
796
826
|
description: productDescription,
|
|
797
|
-
|
|
798
|
-
brand: { '@type': 'Brand', name: 'Nike' },
|
|
827
|
+
brand: 'Nike',
|
|
799
828
|
sku: 'ABC123',
|
|
800
829
|
offers: {
|
|
801
830
|
'@type': 'Offer',
|
|
@@ -818,6 +847,7 @@ const pieces = [
|
|
|
818
847
|
},
|
|
819
848
|
seller: { '@id': ids.organization('shop') },
|
|
820
849
|
},
|
|
850
|
+
image: productImageUrl,
|
|
821
851
|
}),
|
|
822
852
|
];
|
|
823
853
|
```
|
|
@@ -828,6 +858,8 @@ When a product has multiple variants (e.g. sizes, colors), use `ProductGroup`
|
|
|
828
858
|
as the parent and individual `Product` entities for each variant:
|
|
829
859
|
|
|
830
860
|
```ts
|
|
861
|
+
import type { Product, ProductGroup } from 'schema-dts';
|
|
862
|
+
|
|
831
863
|
const variants = [
|
|
832
864
|
{
|
|
833
865
|
sku: 'SHOE-BLK-10',
|
|
@@ -857,25 +889,23 @@ const variants = [
|
|
|
857
889
|
|
|
858
890
|
const pieces = [
|
|
859
891
|
// ...site-wide + WebPage + BreadcrumbList...
|
|
860
|
-
|
|
892
|
+
buildPiece<ProductGroup>({
|
|
861
893
|
'@type': 'ProductGroup',
|
|
862
|
-
'@id': `${url}#product
|
|
894
|
+
'@id': `${url}#product`,
|
|
863
895
|
name: 'Running Shoe',
|
|
864
896
|
description: productDescription,
|
|
865
|
-
brand:
|
|
897
|
+
brand: 'Nike',
|
|
898
|
+
url,
|
|
866
899
|
productGroupID: 'running-shoe',
|
|
867
900
|
variesBy: ['https://schema.org/color', 'https://schema.org/size'],
|
|
868
|
-
hasVariant: variants.map((v) => ({ '@id': `${url}#
|
|
901
|
+
hasVariant: variants.map((v) => ({ '@id': `${url}#product-${v.sku}` })),
|
|
869
902
|
}),
|
|
870
903
|
...variants.map((v) =>
|
|
871
|
-
|
|
904
|
+
buildPiece<Product>({
|
|
872
905
|
'@type': 'Product',
|
|
873
|
-
'@id': `${url}#
|
|
906
|
+
'@id': `${url}#product-${v.sku}`,
|
|
874
907
|
name: v.name,
|
|
875
908
|
sku: v.sku,
|
|
876
|
-
color: v.color,
|
|
877
|
-
size: v.size,
|
|
878
|
-
image: [productImageUrl],
|
|
879
909
|
offers: {
|
|
880
910
|
'@type': 'Offer',
|
|
881
911
|
price: v.price,
|
|
@@ -896,6 +926,9 @@ const pieces = [
|
|
|
896
926
|
shippingDestination: { '@type': 'DefinedRegion', addressCountry: 'US' },
|
|
897
927
|
},
|
|
898
928
|
},
|
|
929
|
+
color: v.color,
|
|
930
|
+
size: v.size,
|
|
931
|
+
image: [productImageUrl],
|
|
899
932
|
}),
|
|
900
933
|
),
|
|
901
934
|
];
|
|
@@ -913,48 +946,43 @@ import type { Restaurant } from 'schema-dts';
|
|
|
913
946
|
const ids = makeIds({ siteUrl: 'https://chezexample.com' });
|
|
914
947
|
|
|
915
948
|
const pieces = [
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
addressCountry: 'FR',
|
|
930
|
-
},
|
|
931
|
-
telephone: '+33-1-23-45-67-89',
|
|
932
|
-
priceRange: '$$$',
|
|
933
|
-
servesCuisine: 'French',
|
|
934
|
-
geo: {
|
|
935
|
-
'@type': 'GeoCoordinates',
|
|
936
|
-
latitude: 48.8698,
|
|
937
|
-
longitude: 2.3311,
|
|
938
|
-
},
|
|
939
|
-
openingHoursSpecification: [
|
|
940
|
-
{
|
|
941
|
-
'@type': 'OpeningHoursSpecification',
|
|
942
|
-
dayOfWeek: ['Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
943
|
-
opens: '12:00',
|
|
944
|
-
closes: '14:30',
|
|
945
|
-
},
|
|
946
|
-
{
|
|
947
|
-
'@type': 'OpeningHoursSpecification',
|
|
948
|
-
dayOfWeek: ['Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
949
|
-
opens: '19:00',
|
|
950
|
-
closes: '22:30',
|
|
951
|
-
},
|
|
952
|
-
],
|
|
953
|
-
},
|
|
949
|
+
buildPiece<Restaurant>({
|
|
950
|
+
'@type': 'Restaurant',
|
|
951
|
+
'@id': ids.organization('chez-example'),
|
|
952
|
+
name: 'Chez Example',
|
|
953
|
+
url: 'https://chezexample.com/',
|
|
954
|
+
logo: logoUrl,
|
|
955
|
+
sameAs: ['https://instagram.com/chezexample'],
|
|
956
|
+
address: {
|
|
957
|
+
'@type': 'PostalAddress',
|
|
958
|
+
streetAddress: '123 Rue de la Paix',
|
|
959
|
+
addressLocality: 'Paris',
|
|
960
|
+
postalCode: '75002',
|
|
961
|
+
addressCountry: 'FR',
|
|
954
962
|
},
|
|
955
|
-
|
|
956
|
-
'
|
|
957
|
-
|
|
963
|
+
telephone: '+33-1-23-45-67-89',
|
|
964
|
+
priceRange: '$$$',
|
|
965
|
+
servesCuisine: 'French',
|
|
966
|
+
geo: {
|
|
967
|
+
'@type': 'GeoCoordinates',
|
|
968
|
+
latitude: 48.8698,
|
|
969
|
+
longitude: 2.3311,
|
|
970
|
+
},
|
|
971
|
+
openingHoursSpecification: [
|
|
972
|
+
{
|
|
973
|
+
'@type': 'OpeningHoursSpecification',
|
|
974
|
+
dayOfWeek: ['Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
975
|
+
opens: '12:00',
|
|
976
|
+
closes: '14:30',
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
'@type': 'OpeningHoursSpecification',
|
|
980
|
+
dayOfWeek: ['Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
981
|
+
opens: '19:00',
|
|
982
|
+
closes: '22:30',
|
|
983
|
+
},
|
|
984
|
+
],
|
|
985
|
+
}),
|
|
958
986
|
buildWebSite(
|
|
959
987
|
{
|
|
960
988
|
url: siteUrl,
|
|
@@ -981,20 +1009,21 @@ const pieces = [
|
|
|
981
1009
|
A freelancer or agency showcasing work.
|
|
982
1010
|
|
|
983
1011
|
```ts
|
|
1012
|
+
import type { Person } from 'schema-dts';
|
|
1013
|
+
|
|
984
1014
|
const ids = makeIds({ siteUrl: 'https://janedoe.design' });
|
|
985
1015
|
|
|
986
1016
|
// Homepage — CollectionPage showcasing work
|
|
987
1017
|
const pieces = [
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
),
|
|
1018
|
+
buildPiece<Person>({
|
|
1019
|
+
'@type': 'Person',
|
|
1020
|
+
'@id': ids.person,
|
|
1021
|
+
name: 'Jane Doe',
|
|
1022
|
+
jobTitle: 'Product Designer',
|
|
1023
|
+
url: siteUrl,
|
|
1024
|
+
image: { '@id': ids.personImage },
|
|
1025
|
+
sameAs: [dribbble, linkedin],
|
|
1026
|
+
}),
|
|
998
1027
|
buildImageObject({ id: ids.personImage, url: headshot, width: 400, height: 400 }, ids),
|
|
999
1028
|
buildWebSite({ url: siteUrl, name: 'Jane Doe Design', publisher: { '@id': ids.person } }, ids),
|
|
1000
1029
|
buildWebPage(
|
|
@@ -1055,13 +1084,18 @@ const projectPieces = [
|
|
|
1055
1084
|
A docs site for a software project or API.
|
|
1056
1085
|
|
|
1057
1086
|
```ts
|
|
1087
|
+
import type { Organization } from 'schema-dts';
|
|
1088
|
+
|
|
1058
1089
|
const ids = makeIds({ siteUrl: 'https://docs.example.com' });
|
|
1059
1090
|
|
|
1060
1091
|
const pieces = [
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
ids,
|
|
1064
|
-
|
|
1092
|
+
buildPiece<Organization>({
|
|
1093
|
+
'@type': 'Organization',
|
|
1094
|
+
'@id': ids.organization('example'),
|
|
1095
|
+
name: 'Example Inc',
|
|
1096
|
+
url: 'https://example.com/',
|
|
1097
|
+
logo: logoUrl,
|
|
1098
|
+
}),
|
|
1065
1099
|
buildWebSite(
|
|
1066
1100
|
{
|
|
1067
1101
|
url: siteUrl,
|
|
@@ -1109,15 +1143,23 @@ container for `PodcastEpisode`. Include the series as a site-wide entity.
|
|
|
1109
1143
|
**Video podcast (YouTube-based):**
|
|
1110
1144
|
|
|
1111
1145
|
```ts
|
|
1146
|
+
import type { Person, PodcastSeries } from 'schema-dts';
|
|
1147
|
+
|
|
1112
1148
|
const ids = makeIds({ siteUrl: 'https://podcast.example.com' });
|
|
1113
1149
|
const seriesId = `${siteUrl}#podcast-series`;
|
|
1114
1150
|
|
|
1115
1151
|
// Episode page
|
|
1116
1152
|
const pieces = [
|
|
1117
|
-
|
|
1153
|
+
buildPiece<Person>({
|
|
1154
|
+
'@type': 'Person',
|
|
1155
|
+
'@id': ids.person,
|
|
1156
|
+
name: 'Host Name',
|
|
1157
|
+
url: aboutUrl,
|
|
1158
|
+
image: { '@id': ids.personImage },
|
|
1159
|
+
}),
|
|
1118
1160
|
buildImageObject({ id: ids.personImage, url: hostPhotoUrl, width: 400, height: 400 }, ids),
|
|
1119
1161
|
buildWebSite({ url: siteUrl, name: 'My Podcast', publisher: { '@id': ids.person } }, ids),
|
|
1120
|
-
|
|
1162
|
+
buildPiece<PodcastSeries>({
|
|
1121
1163
|
'@type': 'PodcastSeries',
|
|
1122
1164
|
'@id': seriesId,
|
|
1123
1165
|
name: 'My Podcast',
|
|
@@ -1170,12 +1212,14 @@ const pieces = [
|
|
|
1170
1212
|
Use `PodcastEpisode` linked to the `PodcastSeries`:
|
|
1171
1213
|
|
|
1172
1214
|
```ts
|
|
1215
|
+
import type { PodcastEpisode } from 'schema-dts';
|
|
1216
|
+
|
|
1173
1217
|
const seriesId = `${siteUrl}#podcast-series`;
|
|
1174
1218
|
|
|
1175
1219
|
const pieces = [
|
|
1176
1220
|
// ...site-wide entities including PodcastSeries...
|
|
1177
1221
|
buildWebPage({ url, name: episodeTitle, isPartOf: { '@id': ids.website }, datePublished }, ids),
|
|
1178
|
-
|
|
1222
|
+
buildPiece<PodcastEpisode>({
|
|
1179
1223
|
'@type': 'PodcastEpisode',
|
|
1180
1224
|
'@id': `${url}#episode`,
|
|
1181
1225
|
name: episodeTitle,
|
|
@@ -1225,10 +1269,12 @@ const pieces = [
|
|
|
1225
1269
|
### Vacation rental / accommodation
|
|
1226
1270
|
|
|
1227
1271
|
```ts
|
|
1272
|
+
import type { Person, VacationRental } from 'schema-dts';
|
|
1273
|
+
|
|
1228
1274
|
const ids = makeIds({ siteUrl: 'https://myhouse.example.com' });
|
|
1229
1275
|
|
|
1230
1276
|
const pieces = [
|
|
1231
|
-
|
|
1277
|
+
buildPiece<Person>({ '@type': 'Person', '@id': ids.person, name: 'Owner Name', url: siteUrl }),
|
|
1232
1278
|
buildWebSite({ url: siteUrl, name: 'Villa Example', publisher: { '@id': ids.person } }, ids),
|
|
1233
1279
|
buildWebPage(
|
|
1234
1280
|
{
|
|
@@ -1238,7 +1284,7 @@ const pieces = [
|
|
|
1238
1284
|
},
|
|
1239
1285
|
ids,
|
|
1240
1286
|
),
|
|
1241
|
-
|
|
1287
|
+
buildPiece<VacationRental>({
|
|
1242
1288
|
'@type': 'VacationRental',
|
|
1243
1289
|
'@id': `${siteUrl}#rental`,
|
|
1244
1290
|
name: 'Villa Example',
|
|
@@ -1289,6 +1335,8 @@ const pieces = [
|
|
|
1289
1335
|
### Recipe site
|
|
1290
1336
|
|
|
1291
1337
|
```ts
|
|
1338
|
+
import type { Recipe } from 'schema-dts';
|
|
1339
|
+
|
|
1292
1340
|
const ids = makeIds({ siteUrl: 'https://recipes.example.com' });
|
|
1293
1341
|
|
|
1294
1342
|
const pieces = [
|
|
@@ -1314,14 +1362,11 @@ const pieces = [
|
|
|
1314
1362
|
},
|
|
1315
1363
|
ids,
|
|
1316
1364
|
),
|
|
1317
|
-
|
|
1365
|
+
buildPiece<Recipe>({
|
|
1318
1366
|
'@type': 'Recipe',
|
|
1319
1367
|
'@id': `${url}#recipe`,
|
|
1320
1368
|
name: recipeName,
|
|
1321
|
-
description: recipeDescription,
|
|
1322
|
-
image: recipeImageUrl,
|
|
1323
1369
|
author: { '@id': ids.person },
|
|
1324
|
-
datePublished: publishDate.toISOString(),
|
|
1325
1370
|
prepTime: 'PT15M',
|
|
1326
1371
|
cookTime: 'PT45M',
|
|
1327
1372
|
totalTime: 'PT1H',
|
|
@@ -1344,6 +1389,9 @@ const pieces = [
|
|
|
1344
1389
|
{ '@type': 'HowToStep', text: 'Mix egg yolks with pecorino.' },
|
|
1345
1390
|
{ '@type': 'HowToStep', text: 'Combine and serve immediately.' },
|
|
1346
1391
|
],
|
|
1392
|
+
description: recipeDescription,
|
|
1393
|
+
image: recipeImageUrl,
|
|
1394
|
+
datePublished: publishDate.toISOString(),
|
|
1347
1395
|
}),
|
|
1348
1396
|
];
|
|
1349
1397
|
```
|
|
@@ -1353,7 +1401,9 @@ const pieces = [
|
|
|
1353
1401
|
### Event page
|
|
1354
1402
|
|
|
1355
1403
|
```ts
|
|
1356
|
-
|
|
1404
|
+
import type { Event } from 'schema-dts';
|
|
1405
|
+
|
|
1406
|
+
buildPiece<Event>({
|
|
1357
1407
|
'@type': 'Event',
|
|
1358
1408
|
'@id': `${url}#event`,
|
|
1359
1409
|
name: 'JavaScript Conference 2026',
|
|
@@ -1389,8 +1439,16 @@ buildCustomPiece({
|
|
|
1389
1439
|
### SaaS / software product landing page
|
|
1390
1440
|
|
|
1391
1441
|
```ts
|
|
1442
|
+
import type { Organization, SoftwareApplication } from 'schema-dts';
|
|
1443
|
+
|
|
1392
1444
|
const pieces = [
|
|
1393
|
-
|
|
1445
|
+
buildPiece<Organization>({
|
|
1446
|
+
'@type': 'Organization',
|
|
1447
|
+
'@id': ids.organization('myapp'),
|
|
1448
|
+
name: 'MyApp Inc',
|
|
1449
|
+
url: siteUrl,
|
|
1450
|
+
logo: logoUrl,
|
|
1451
|
+
}),
|
|
1394
1452
|
buildWebSite(
|
|
1395
1453
|
{ url: siteUrl, name: 'MyApp', publisher: { '@id': ids.organization('myapp') } },
|
|
1396
1454
|
ids,
|
|
@@ -1403,7 +1461,7 @@ const pieces = [
|
|
|
1403
1461
|
},
|
|
1404
1462
|
ids,
|
|
1405
1463
|
),
|
|
1406
|
-
|
|
1464
|
+
buildPiece<SoftwareApplication>({
|
|
1407
1465
|
'@type': 'SoftwareApplication',
|
|
1408
1466
|
'@id': `${siteUrl}#app`,
|
|
1409
1467
|
name: 'MyApp',
|
|
@@ -1443,13 +1501,15 @@ const pieces = [
|
|
|
1443
1501
|
Combine `WebPage` with a `FAQPage` custom piece:
|
|
1444
1502
|
|
|
1445
1503
|
```ts
|
|
1504
|
+
import type { FAQPage } from 'schema-dts';
|
|
1505
|
+
|
|
1446
1506
|
const pieces = [
|
|
1447
1507
|
// ...site-wide entities...
|
|
1448
1508
|
buildWebPage(
|
|
1449
1509
|
{ url, name: 'Frequently Asked Questions', isPartOf: { '@id': ids.website } },
|
|
1450
1510
|
ids,
|
|
1451
1511
|
),
|
|
1452
|
-
|
|
1512
|
+
buildPiece<FAQPage>({
|
|
1453
1513
|
'@type': 'FAQPage',
|
|
1454
1514
|
'@id': `${url}#faq`,
|
|
1455
1515
|
mainEntity: [
|
|
@@ -1479,7 +1539,9 @@ const pieces = [
|
|
|
1479
1539
|
### Course / educational content
|
|
1480
1540
|
|
|
1481
1541
|
```ts
|
|
1482
|
-
|
|
1542
|
+
import type { Course } from 'schema-dts';
|
|
1543
|
+
|
|
1544
|
+
buildPiece<Course>({
|
|
1483
1545
|
'@type': 'Course',
|
|
1484
1546
|
'@id': `${url}#course`,
|
|
1485
1547
|
name: 'Introduction to TypeScript',
|
|
@@ -1534,17 +1596,19 @@ policies. It can be applied to `Organization`, `Person`, or `CreativeWork`
|
|
|
1534
1596
|
search engines and AI agents about your content's credibility.
|
|
1535
1597
|
|
|
1536
1598
|
```ts
|
|
1599
|
+
import type { Person, Blog, Organization } from 'schema-dts';
|
|
1600
|
+
|
|
1537
1601
|
// On a Person entity (personal blog)
|
|
1538
|
-
|
|
1602
|
+
buildPiece<Person>({
|
|
1603
|
+
'@type': 'Person',
|
|
1604
|
+
'@id': ids.person,
|
|
1539
1605
|
name: 'Jane Doe',
|
|
1540
1606
|
url: aboutUrl,
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
},
|
|
1544
|
-
}, ids),
|
|
1607
|
+
publishingPrinciples: `${siteUrl}/editorial-policy/`,
|
|
1608
|
+
}),
|
|
1545
1609
|
|
|
1546
1610
|
// On a Blog entity
|
|
1547
|
-
|
|
1611
|
+
buildPiece<Blog>({
|
|
1548
1612
|
'@type': 'Blog',
|
|
1549
1613
|
'@id': blogId,
|
|
1550
1614
|
name: 'My Blog',
|
|
@@ -1554,13 +1618,12 @@ buildCustomPiece({
|
|
|
1554
1618
|
}),
|
|
1555
1619
|
|
|
1556
1620
|
// On an Organization (news site, company blog)
|
|
1557
|
-
|
|
1558
|
-
|
|
1621
|
+
buildPiece<Organization>({
|
|
1622
|
+
'@type': 'Organization',
|
|
1623
|
+
'@id': ids.organization('newsroom'),
|
|
1559
1624
|
name: 'The Daily Example',
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
},
|
|
1563
|
-
}, ids),
|
|
1625
|
+
publishingPrinciples: `${siteUrl}/ethics/`,
|
|
1626
|
+
}),
|
|
1564
1627
|
```
|
|
1565
1628
|
|
|
1566
1629
|
### Specialized policy sub-properties
|
|
@@ -1569,21 +1632,22 @@ For news and media organizations, schema.org has more specific sub-properties
|
|
|
1569
1632
|
of `publishingPrinciples`:
|
|
1570
1633
|
|
|
1571
1634
|
```ts
|
|
1572
|
-
|
|
1573
|
-
|
|
1635
|
+
import type { Organization } from 'schema-dts';
|
|
1636
|
+
|
|
1637
|
+
buildPiece<Organization>({
|
|
1638
|
+
'@type': 'Organization',
|
|
1639
|
+
'@id': ids.organization('newsroom'),
|
|
1574
1640
|
name: 'The Daily Example',
|
|
1575
1641
|
url: siteUrl,
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
},
|
|
1586
|
-
}, ids),
|
|
1642
|
+
publishingPrinciples: `${siteUrl}/editorial-policy/`,
|
|
1643
|
+
correctionsPolicy: `${siteUrl}/corrections/`,
|
|
1644
|
+
verificationFactCheckingPolicy: `${siteUrl}/fact-checking/`,
|
|
1645
|
+
actionableFeedbackPolicy: `${siteUrl}/feedback/`,
|
|
1646
|
+
unnamedSourcesPolicy: `${siteUrl}/sources-policy/`,
|
|
1647
|
+
ownershipFundingInfo: `${siteUrl}/about/ownership/`,
|
|
1648
|
+
diversityStaffingReport: `${siteUrl}/diversity-report/`,
|
|
1649
|
+
masthead: `${siteUrl}/team/`,
|
|
1650
|
+
}),
|
|
1587
1651
|
```
|
|
1588
1652
|
|
|
1589
1653
|
### When to use which
|
|
@@ -1621,9 +1685,7 @@ buildWebPage({
|
|
|
1621
1685
|
copyrightNotice: '© 2026 Jane Doe. All rights reserved.',
|
|
1622
1686
|
license: 'https://creativecommons.org/licenses/by/4.0/',
|
|
1623
1687
|
isAccessibleForFree: true,
|
|
1624
|
-
|
|
1625
|
-
creditText: 'Jane Doe / janedoe.com',
|
|
1626
|
-
},
|
|
1688
|
+
creditText: 'Jane Doe / janedoe.com',
|
|
1627
1689
|
}, ids),
|
|
1628
1690
|
```
|
|
1629
1691
|
|
|
@@ -1634,11 +1696,9 @@ buildArticle({
|
|
|
1634
1696
|
url,
|
|
1635
1697
|
headline: title,
|
|
1636
1698
|
// ...other article properties...
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
license: 'https://creativecommons.org/licenses/by-sa/4.0/',
|
|
1641
|
-
},
|
|
1699
|
+
copyrightHolder: { '@id': ids.person },
|
|
1700
|
+
copyrightYear: 2026,
|
|
1701
|
+
license: 'https://creativecommons.org/licenses/by-sa/4.0/',
|
|
1642
1702
|
}, ids, 'BlogPosting'),
|
|
1643
1703
|
```
|
|
1644
1704
|
|
|
@@ -1771,7 +1831,9 @@ potentialAction: {
|
|
|
1771
1831
|
Add to the `Product` or `ProductGroup` entity:
|
|
1772
1832
|
|
|
1773
1833
|
```ts
|
|
1774
|
-
|
|
1834
|
+
import type { Product } from 'schema-dts';
|
|
1835
|
+
|
|
1836
|
+
buildPiece<Product>({
|
|
1775
1837
|
'@type': 'Product',
|
|
1776
1838
|
'@id': `${url}#product`,
|
|
1777
1839
|
name: productName,
|
|
@@ -1824,7 +1886,9 @@ potentialAction: {
|
|
|
1824
1886
|
Add to the `VacationRental`, `Product`, or `Car` entity:
|
|
1825
1887
|
|
|
1826
1888
|
```ts
|
|
1827
|
-
|
|
1889
|
+
import type { VacationRental } from 'schema-dts';
|
|
1890
|
+
|
|
1891
|
+
buildPiece<VacationRental>({
|
|
1828
1892
|
'@type': 'VacationRental',
|
|
1829
1893
|
'@id': `${siteUrl}#rental`,
|
|
1830
1894
|
name: 'Villa Example',
|
|
@@ -1933,29 +1997,29 @@ Many entities benefit from multiple actions. A WebSite typically has a
|
|
|
1933
1997
|
`SearchAction`; the entities within it have trade actions:
|
|
1934
1998
|
|
|
1935
1999
|
```ts
|
|
2000
|
+
import type { Product } from 'schema-dts';
|
|
2001
|
+
|
|
1936
2002
|
// WebSite: how to search
|
|
1937
2003
|
buildWebSite({
|
|
1938
2004
|
url: siteUrl,
|
|
1939
2005
|
name: 'My Shop',
|
|
1940
2006
|
publisher: { '@id': ids.organization('shop') },
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
'
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
valueName: 'search_term_string',
|
|
1952
|
-
},
|
|
2007
|
+
potentialAction: {
|
|
2008
|
+
'@type': 'SearchAction',
|
|
2009
|
+
target: {
|
|
2010
|
+
'@type': 'EntryPoint',
|
|
2011
|
+
urlTemplate: `${siteUrl}search?q={search_term_string}`,
|
|
2012
|
+
},
|
|
2013
|
+
'query-input': {
|
|
2014
|
+
'@type': 'PropertyValueSpecification',
|
|
2015
|
+
valueRequired: true,
|
|
2016
|
+
valueName: 'search_term_string',
|
|
1953
2017
|
},
|
|
1954
2018
|
},
|
|
1955
2019
|
}, ids),
|
|
1956
2020
|
|
|
1957
2021
|
// Product: how to buy
|
|
1958
|
-
|
|
2022
|
+
buildPiece<Product>({
|
|
1959
2023
|
'@type': 'Product',
|
|
1960
2024
|
'@id': `${url}#product`,
|
|
1961
2025
|
name: productName,
|
|
@@ -1994,7 +2058,7 @@ An entity can have multiple `@type` values. This is useful when an entity
|
|
|
1994
2058
|
legitimately belongs to more than one type:
|
|
1995
2059
|
|
|
1996
2060
|
```ts
|
|
1997
|
-
|
|
2061
|
+
buildPiece({
|
|
1998
2062
|
'@type': ['Organization', 'Brand'],
|
|
1999
2063
|
'@id': ids.organization('acme'),
|
|
2000
2064
|
name: 'Acme',
|
|
@@ -2015,18 +2079,15 @@ Common multi-type combinations:
|
|
|
2015
2079
|
- `['WebPage', 'ItemPage']` — Product detail pages
|
|
2016
2080
|
- `['WebPage', 'FAQPage']` — FAQ pages (alternative to separate FAQPage entity)
|
|
2017
2081
|
|
|
2018
|
-
**Note:**
|
|
2019
|
-
subtype as the third argument and add additional types via `extra`:
|
|
2082
|
+
**Note:** With `buildPiece`, pass the `@type` array directly:
|
|
2020
2083
|
|
|
2021
2084
|
```ts
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
ids,
|
|
2029
|
-
);
|
|
2085
|
+
buildPiece({
|
|
2086
|
+
'@type': ['Organization', 'Brand'],
|
|
2087
|
+
'@id': ids.organization('acme'),
|
|
2088
|
+
name: 'Acme',
|
|
2089
|
+
url: 'https://acme.com/',
|
|
2090
|
+
});
|
|
2030
2091
|
```
|
|
2031
2092
|
|
|
2032
2093
|
---
|
|
@@ -2037,49 +2098,47 @@ For established businesses, a richer Organization entity improves knowledge
|
|
|
2037
2098
|
graph representation. Here's the full pattern:
|
|
2038
2099
|
|
|
2039
2100
|
```ts
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
numberOfEmployees: 45,
|
|
2062
|
-
slogan: 'Tools for the modern web',
|
|
2063
|
-
parentOrganization: {
|
|
2064
|
-
'@type': 'Organization',
|
|
2065
|
-
name: 'Parent Holdings Inc',
|
|
2066
|
-
url: 'https://parent.com/',
|
|
2067
|
-
},
|
|
2068
|
-
memberOf: {
|
|
2069
|
-
'@type': 'Organization',
|
|
2070
|
-
name: 'World Wide Web Consortium (W3C)',
|
|
2071
|
-
url: 'https://w3.org/',
|
|
2072
|
-
},
|
|
2073
|
-
address: {
|
|
2074
|
-
'@type': 'PostalAddress',
|
|
2075
|
-
streetAddress: '123 Tech Lane',
|
|
2076
|
-
addressLocality: 'Amsterdam',
|
|
2077
|
-
addressCountry: 'NL',
|
|
2078
|
-
},
|
|
2079
|
-
},
|
|
2101
|
+
import type { Organization } from 'schema-dts';
|
|
2102
|
+
|
|
2103
|
+
buildPiece<Organization>({
|
|
2104
|
+
'@type': 'Organization',
|
|
2105
|
+
'@id': ids.organization('acme'),
|
|
2106
|
+
name: 'Acme Corp',
|
|
2107
|
+
url: 'https://acme.com/',
|
|
2108
|
+
logo: 'https://acme.com/logo.png',
|
|
2109
|
+
description: 'We build developer tools.',
|
|
2110
|
+
sameAs: [
|
|
2111
|
+
'https://twitter.com/acme',
|
|
2112
|
+
'https://linkedin.com/company/acme',
|
|
2113
|
+
'https://github.com/acme',
|
|
2114
|
+
'https://en.wikipedia.org/wiki/Acme_Corp',
|
|
2115
|
+
],
|
|
2116
|
+
legalName: 'Acme Corp B.V.',
|
|
2117
|
+
foundingDate: '2015-03-01',
|
|
2118
|
+
founder: {
|
|
2119
|
+
'@type': 'Person',
|
|
2120
|
+
name: 'Jane Doe',
|
|
2121
|
+
sameAs: 'https://en.wikipedia.org/wiki/Jane_Doe',
|
|
2080
2122
|
},
|
|
2081
|
-
|
|
2082
|
-
|
|
2123
|
+
numberOfEmployees: 45,
|
|
2124
|
+
slogan: 'Tools for the modern web',
|
|
2125
|
+
parentOrganization: {
|
|
2126
|
+
'@type': 'Organization',
|
|
2127
|
+
name: 'Parent Holdings Inc',
|
|
2128
|
+
url: 'https://parent.com/',
|
|
2129
|
+
},
|
|
2130
|
+
memberOf: {
|
|
2131
|
+
'@type': 'Organization',
|
|
2132
|
+
name: 'World Wide Web Consortium (W3C)',
|
|
2133
|
+
url: 'https://w3.org/',
|
|
2134
|
+
},
|
|
2135
|
+
address: {
|
|
2136
|
+
'@type': 'PostalAddress',
|
|
2137
|
+
streetAddress: '123 Tech Lane',
|
|
2138
|
+
addressLocality: 'Amsterdam',
|
|
2139
|
+
addressCountry: 'NL',
|
|
2140
|
+
},
|
|
2141
|
+
});
|
|
2083
2142
|
```
|
|
2084
2143
|
|
|
2085
2144
|
Include as much as is factually accurate. Don't fabricate data. Properties like
|
|
@@ -2094,63 +2153,57 @@ For personal sites, a detailed Person entity establishes identity and
|
|
|
2094
2153
|
credibility. jonoalderson.com uses 80+ entities. Here's the extended pattern:
|
|
2095
2154
|
|
|
2096
2155
|
```ts
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
roleName: 'Advisor',
|
|
2127
|
-
startDate: '2024-06',
|
|
2128
|
-
worksFor: { '@id': ids.organization('startup') },
|
|
2129
|
-
},
|
|
2130
|
-
],
|
|
2131
|
-
spouse: {
|
|
2132
|
-
'@type': 'Person',
|
|
2133
|
-
'@id': `${siteUrl}/#/schema.org/Person/john`,
|
|
2134
|
-
name: 'John Doe',
|
|
2156
|
+
import type { Person } from 'schema-dts';
|
|
2157
|
+
|
|
2158
|
+
buildPiece<Person>({
|
|
2159
|
+
'@type': 'Person',
|
|
2160
|
+
'@id': ids.person,
|
|
2161
|
+
name: 'Jane Doe',
|
|
2162
|
+
familyName: 'Doe',
|
|
2163
|
+
birthDate: '1990-01-15',
|
|
2164
|
+
gender: 'female',
|
|
2165
|
+
nationality: { '@id': ids.country('US') },
|
|
2166
|
+
description: 'Software engineer and technical writer.',
|
|
2167
|
+
jobTitle: 'Lead Engineer',
|
|
2168
|
+
knowsLanguage: ['en', 'es', 'pt'],
|
|
2169
|
+
url: 'https://janedoe.com/about/',
|
|
2170
|
+
image: { '@id': ids.personImage },
|
|
2171
|
+
sameAs: [
|
|
2172
|
+
'https://twitter.com/janedoe',
|
|
2173
|
+
'https://github.com/janedoe',
|
|
2174
|
+
'https://linkedin.com/in/janedoe',
|
|
2175
|
+
'https://bsky.app/profile/janedoe.com',
|
|
2176
|
+
'https://mastodon.social/@janedoe',
|
|
2177
|
+
'https://en.wikipedia.org/wiki/Jane_Doe',
|
|
2178
|
+
],
|
|
2179
|
+
worksFor: [
|
|
2180
|
+
{
|
|
2181
|
+
'@type': 'EmployeeRole',
|
|
2182
|
+
roleName: 'Lead Engineer',
|
|
2183
|
+
startDate: '2022-01',
|
|
2184
|
+
worksFor: { '@id': ids.organization('acme') },
|
|
2135
2185
|
},
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
'Web Performance',
|
|
2142
|
-
],
|
|
2143
|
-
honorificPrefix: 'Dr.',
|
|
2144
|
-
alumniOf: {
|
|
2145
|
-
'@type': 'EducationalOrganization',
|
|
2146
|
-
name: 'MIT',
|
|
2147
|
-
url: 'https://mit.edu/',
|
|
2148
|
-
},
|
|
2149
|
-
award: ['Best Developer Blog 2025', 'Open Source Contributor of the Year 2024'],
|
|
2186
|
+
{
|
|
2187
|
+
'@type': 'EmployeeRole',
|
|
2188
|
+
roleName: 'Advisor',
|
|
2189
|
+
startDate: '2024-06',
|
|
2190
|
+
worksFor: { '@id': ids.organization('startup') },
|
|
2150
2191
|
},
|
|
2192
|
+
],
|
|
2193
|
+
spouse: {
|
|
2194
|
+
'@type': 'Person',
|
|
2195
|
+
'@id': `${siteUrl}/#/schema.org/Person/john`,
|
|
2196
|
+
name: 'John Doe',
|
|
2151
2197
|
},
|
|
2152
|
-
|
|
2153
|
-
|
|
2198
|
+
knowsAbout: ['TypeScript', 'Schema.org', 'Search Engine Optimization', 'Web Performance'],
|
|
2199
|
+
honorificPrefix: 'Dr.',
|
|
2200
|
+
alumniOf: {
|
|
2201
|
+
'@type': 'EducationalOrganization',
|
|
2202
|
+
name: 'MIT',
|
|
2203
|
+
url: 'https://mit.edu/',
|
|
2204
|
+
},
|
|
2205
|
+
award: ['Best Developer Blog 2025', 'Open Source Contributor of the Year 2024'],
|
|
2206
|
+
});
|
|
2154
2207
|
```
|
|
2155
2208
|
|
|
2156
2209
|
**Practical advice:**
|
|
@@ -2533,13 +2586,14 @@ import {
|
|
|
2533
2586
|
makeIds,
|
|
2534
2587
|
assembleGraph,
|
|
2535
2588
|
buildWebSite,
|
|
2536
|
-
|
|
2589
|
+
buildPiece,
|
|
2537
2590
|
buildWebPage,
|
|
2538
2591
|
buildArticle,
|
|
2539
2592
|
buildBreadcrumbList,
|
|
2540
2593
|
buildImageObject,
|
|
2541
2594
|
buildSiteNavigationElement,
|
|
2542
2595
|
} from '@jdevalk/seo-graph-core';
|
|
2596
|
+
import type { Person } from 'schema-dts';
|
|
2543
2597
|
|
|
2544
2598
|
const SITE_URL = 'https://example.com';
|
|
2545
2599
|
export const ids = makeIds({ siteUrl: SITE_URL, personUrl: `${SITE_URL}/about/` });
|
|
@@ -2551,15 +2605,14 @@ function siteWideEntities() {
|
|
|
2551
2605
|
{ url: `${SITE_URL}/`, name: 'My Blog', publisher: { '@id': ids.person } },
|
|
2552
2606
|
ids,
|
|
2553
2607
|
),
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
),
|
|
2608
|
+
buildPiece<Person>({
|
|
2609
|
+
'@type': 'Person',
|
|
2610
|
+
'@id': ids.person,
|
|
2611
|
+
name: 'Jane Doe',
|
|
2612
|
+
url: `${SITE_URL}/about/`,
|
|
2613
|
+
image: { '@id': ids.personImage },
|
|
2614
|
+
sameAs: ['...'],
|
|
2615
|
+
}),
|
|
2563
2616
|
buildImageObject(
|
|
2564
2617
|
{ id: ids.personImage, url: `${SITE_URL}/jane.jpg`, width: 400, height: 400 },
|
|
2565
2618
|
ids,
|
|
@@ -2795,70 +2848,76 @@ export const GET = createSchemaMap({
|
|
|
2795
2848
|
When a person works for several companies, create an organization for each:
|
|
2796
2849
|
|
|
2797
2850
|
```ts
|
|
2851
|
+
import type { Organization, Person } from 'schema-dts';
|
|
2852
|
+
|
|
2798
2853
|
const orgs = [
|
|
2799
2854
|
{ slug: 'acme', name: 'Acme Corp', url: 'https://acme.com/' },
|
|
2800
2855
|
{ slug: 'side-project', name: 'Side Project Inc', url: 'https://sideproject.com/' },
|
|
2801
2856
|
];
|
|
2802
2857
|
|
|
2803
|
-
const orgPieces = orgs.map((org) =>
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
name:
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
'@type': 'EmployeeRole',
|
|
2811
|
-
roleName: 'CEO',
|
|
2812
|
-
startDate: '2020',
|
|
2813
|
-
worksFor: { '@id': ids.organization('acme') },
|
|
2814
|
-
},
|
|
2815
|
-
{
|
|
2816
|
-
'@type': 'EmployeeRole',
|
|
2817
|
-
roleName: 'Advisor',
|
|
2818
|
-
startDate: '2023',
|
|
2819
|
-
worksFor: { '@id': ids.organization('side-project') },
|
|
2820
|
-
},
|
|
2821
|
-
],
|
|
2822
|
-
},
|
|
2823
|
-
ids,
|
|
2858
|
+
const orgPieces = orgs.map((org) =>
|
|
2859
|
+
buildPiece<Organization>({
|
|
2860
|
+
'@type': 'Organization',
|
|
2861
|
+
'@id': ids.organization(org.slug),
|
|
2862
|
+
name: org.name,
|
|
2863
|
+
url: org.url,
|
|
2864
|
+
}),
|
|
2824
2865
|
);
|
|
2866
|
+
|
|
2867
|
+
const personPiece = buildPiece<Person>({
|
|
2868
|
+
'@type': 'Person',
|
|
2869
|
+
'@id': ids.person,
|
|
2870
|
+
name: 'Jane Doe',
|
|
2871
|
+
worksFor: [
|
|
2872
|
+
{
|
|
2873
|
+
'@type': 'EmployeeRole',
|
|
2874
|
+
roleName: 'CEO',
|
|
2875
|
+
startDate: '2020',
|
|
2876
|
+
worksFor: { '@id': ids.organization('acme') },
|
|
2877
|
+
},
|
|
2878
|
+
{
|
|
2879
|
+
'@type': 'EmployeeRole',
|
|
2880
|
+
roleName: 'Advisor',
|
|
2881
|
+
startDate: '2023',
|
|
2882
|
+
worksFor: { '@id': ids.organization('side-project') },
|
|
2883
|
+
},
|
|
2884
|
+
],
|
|
2885
|
+
});
|
|
2825
2886
|
```
|
|
2826
2887
|
|
|
2827
2888
|
### Organization subtypes
|
|
2828
2889
|
|
|
2829
|
-
Use
|
|
2890
|
+
Use the schema.org subtype directly as the `buildPiece` generic for full type safety:
|
|
2830
2891
|
|
|
2831
2892
|
```ts
|
|
2832
|
-
import type { Dentist, Hotel
|
|
2893
|
+
import type { Dentist, Hotel } from 'schema-dts';
|
|
2833
2894
|
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
ids,
|
|
2837
|
-
'
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
},
|
|
2849
|
-
ids,
|
|
2850
|
-
'Hotel',
|
|
2851
|
-
);
|
|
2895
|
+
buildPiece<Dentist>({
|
|
2896
|
+
'@type': 'Dentist',
|
|
2897
|
+
'@id': ids.organization('clinic'),
|
|
2898
|
+
name: 'Smile Dental',
|
|
2899
|
+
medicalSpecialty: 'Dentistry',
|
|
2900
|
+
});
|
|
2901
|
+
buildPiece<Hotel>({
|
|
2902
|
+
'@type': 'Hotel',
|
|
2903
|
+
'@id': ids.organization('hotel'),
|
|
2904
|
+
name: 'Grand Hotel',
|
|
2905
|
+
starRating: { '@type': 'Rating', ratingValue: 4 },
|
|
2906
|
+
checkinTime: '15:00',
|
|
2907
|
+
checkoutTime: '11:00',
|
|
2908
|
+
});
|
|
2852
2909
|
```
|
|
2853
2910
|
|
|
2854
2911
|
### Multi-author blogs
|
|
2855
2912
|
|
|
2856
|
-
When different posts have different authors, use `
|
|
2857
|
-
|
|
2913
|
+
When different posts have different authors, use `buildPiece<Person>` with a
|
|
2914
|
+
custom `@id` for each author (reserving `ids.person` for the site-wide person):
|
|
2858
2915
|
|
|
2859
2916
|
```ts
|
|
2917
|
+
import type { Person } from 'schema-dts';
|
|
2918
|
+
|
|
2860
2919
|
const authorId = `${siteUrl}/authors/${authorSlug}/#person`;
|
|
2861
|
-
const authorPiece =
|
|
2920
|
+
const authorPiece = buildPiece<Person>({
|
|
2862
2921
|
'@type': 'Person',
|
|
2863
2922
|
'@id': authorId,
|
|
2864
2923
|
name: authorName,
|