@jdevalk/astro-seo-graph 0.3.1 → 0.4.1
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 +525 -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,167 @@ 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`.
|
|
514
|
+
|
|
515
|
+
### Overriding `@id`
|
|
516
|
+
|
|
517
|
+
Every dedicated builder computes an `@id` from the `IdFactory` (e.g.
|
|
518
|
+
`ids.website`, `ids.article(url)`). You can override it by passing `'@id'`
|
|
519
|
+
directly — the explicit value wins:
|
|
520
|
+
|
|
521
|
+
```ts
|
|
522
|
+
buildBreadcrumbList(
|
|
523
|
+
{
|
|
524
|
+
url,
|
|
525
|
+
items: [
|
|
526
|
+
{ name: 'Home', url: siteUrl },
|
|
527
|
+
{ name: 'Blog', url: blogUrl },
|
|
528
|
+
],
|
|
529
|
+
'@id': `${blogUrl}#breadcrumb`, // overrides ids.breadcrumb(url)
|
|
530
|
+
},
|
|
531
|
+
ids,
|
|
532
|
+
);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
This works on all builders: `buildWebSite`, `buildWebPage`, `buildArticle`,
|
|
536
|
+
`buildBreadcrumbList`, `buildImageObject`, `buildVideoObject`, and
|
|
537
|
+
`buildSiteNavigationElement`.
|
|
505
538
|
|
|
506
539
|
### assembleGraph
|
|
507
540
|
|
|
@@ -523,6 +556,16 @@ const graph = assembleGraph([
|
|
|
523
556
|
**Always call this last.** It handles deduplication: if multiple pages produce
|
|
524
557
|
the same `WebSite` or `Person` entity (same `@id`), the first occurrence wins.
|
|
525
558
|
|
|
559
|
+
**Dangling reference validation:** Pass `warnOnDanglingReferences: true` to
|
|
560
|
+
validate that every `{ '@id': '...' }` reference in the graph resolves to an
|
|
561
|
+
actual entity. This helps catch broken links — for example, a `WebSite`
|
|
562
|
+
referencing a `Person` that was never included in the pieces array.
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
const graph = assembleGraph(pieces, { warnOnDanglingReferences: true });
|
|
566
|
+
// Warns: [seo-graph] Dangling reference in WebSite: { "@id": "..." } does not match any entity in the graph.
|
|
567
|
+
```
|
|
568
|
+
|
|
526
569
|
### deduplicateByGraphId
|
|
527
570
|
|
|
528
571
|
The dedup engine on its own, for custom assembly workflows.
|
|
@@ -549,19 +592,21 @@ about page.
|
|
|
549
592
|
**For every page** (site-wide entities):
|
|
550
593
|
|
|
551
594
|
- `buildWebSite` — publisher points to Person
|
|
552
|
-
- `
|
|
595
|
+
- `buildPiece<Person>` — the blog author
|
|
553
596
|
- `buildImageObject` — person's profile photo (use `id: ids.personImage`)
|
|
554
|
-
- `
|
|
597
|
+
- `buildPiece<Blog>` — a `Blog` entity representing the blog as a publication
|
|
555
598
|
|
|
556
599
|
The `Blog` entity is a `CreativeWork` that represents the blog as a whole,
|
|
557
600
|
separate from the `WebSite`. Individual `BlogPosting` entries reference the
|
|
558
601
|
Blog via `isPartOf`. This is the pattern used by jonoalderson.com.
|
|
559
602
|
|
|
560
603
|
```ts
|
|
604
|
+
import type { Blog } from 'schema-dts';
|
|
605
|
+
|
|
561
606
|
const blogId = `${siteUrl}/blog/#blog`;
|
|
562
607
|
|
|
563
608
|
// Include on every page as a site-wide entity
|
|
564
|
-
|
|
609
|
+
buildPiece<Blog>({
|
|
565
610
|
'@type': 'Blog',
|
|
566
611
|
'@id': blogId,
|
|
567
612
|
name: 'My Blog',
|
|
@@ -577,13 +622,15 @@ buildCustomPiece({
|
|
|
577
622
|
Use `BlogPosting` instead of `Article` and link it to the Blog:
|
|
578
623
|
|
|
579
624
|
```ts
|
|
625
|
+
import type { Person, Blog } from 'schema-dts';
|
|
626
|
+
|
|
580
627
|
const blogId = `${siteUrl}/blog/#blog`;
|
|
581
628
|
|
|
582
629
|
const pieces = [
|
|
583
630
|
buildWebSite({ url: siteUrl, name: 'My Blog', publisher: { '@id': ids.person } }, ids),
|
|
584
|
-
|
|
631
|
+
buildPiece<Person>({ '@type': 'Person', '@id': ids.person, name: 'Jane Doe', url: aboutUrl, image: { '@id': ids.personImage }, sameAs: [...] }),
|
|
585
632
|
buildImageObject({ id: ids.personImage, url: profilePhotoUrl, width: 400, height: 400 }, ids),
|
|
586
|
-
|
|
633
|
+
buildPiece<Blog>({
|
|
587
634
|
'@type': 'Blog',
|
|
588
635
|
'@id': blogId,
|
|
589
636
|
name: 'My Blog',
|
|
@@ -599,11 +646,10 @@ const pieces = [
|
|
|
599
646
|
dateModified,
|
|
600
647
|
author: { '@id': ids.person },
|
|
601
648
|
publisher: { '@id': ids.person },
|
|
602
|
-
isPartOf: { '@id': ids.webPage(url) },
|
|
649
|
+
isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }],
|
|
603
650
|
image: { '@id': ids.primaryImage(url) },
|
|
604
651
|
articleSection: category,
|
|
605
652
|
wordCount,
|
|
606
|
-
extra: { isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }] },
|
|
607
653
|
}, ids, 'BlogPosting'),
|
|
608
654
|
buildBreadcrumbList({ url, items: [{ name: 'Home', url: siteUrl }, { name: 'Blog', url: blogUrl }, { name: title, url }] }, ids),
|
|
609
655
|
buildImageObject({ pageUrl: url, url: featureImageUrl, width: 1200, height: 630 }, ids),
|
|
@@ -611,9 +657,9 @@ const pieces = [
|
|
|
611
657
|
const graph = assembleGraph(pieces);
|
|
612
658
|
```
|
|
613
659
|
|
|
614
|
-
**Note:** The `
|
|
615
|
-
|
|
616
|
-
|
|
660
|
+
**Note:** The `isPartOf` array links the posting to both the `WebPage` and the
|
|
661
|
+
`Blog`. If you don't need the `Blog` link, just use
|
|
662
|
+
`isPartOf: { '@id': ids.webPage(url) }` directly.
|
|
617
663
|
|
|
618
664
|
**Blog listing** (`/blog/`):
|
|
619
665
|
|
|
@@ -715,14 +761,16 @@ A multi-author blog owned by a company.
|
|
|
715
761
|
entities.
|
|
716
762
|
|
|
717
763
|
```ts
|
|
764
|
+
import type { Organization, Blog, Person } from 'schema-dts';
|
|
765
|
+
|
|
718
766
|
const ids = makeIds({ siteUrl: 'https://acme.com' });
|
|
719
767
|
|
|
720
768
|
// Site-wide
|
|
721
769
|
const blogId = 'https://acme.com/blog/#blog';
|
|
722
770
|
const siteEntities = [
|
|
723
|
-
|
|
771
|
+
buildPiece<Organization>({ '@type': 'Organization', '@id': ids.organization('acme'), name: 'Acme Corp', url: 'https://acme.com/', logo: logoUrl, sameAs: [...] }),
|
|
724
772
|
buildWebSite({ url: 'https://acme.com/', name: 'Acme Blog', publisher: { '@id': ids.organization('acme') } }, ids),
|
|
725
|
-
|
|
773
|
+
buildPiece<Blog>({
|
|
726
774
|
'@type': 'Blog',
|
|
727
775
|
'@id': blogId,
|
|
728
776
|
name: 'The Acme Blog',
|
|
@@ -735,7 +783,7 @@ const siteEntities = [
|
|
|
735
783
|
const authorId = 'https://acme.com/team/jane/#person';
|
|
736
784
|
const postPieces = [
|
|
737
785
|
...siteEntities,
|
|
738
|
-
|
|
786
|
+
buildPiece<Person>({ '@type': 'Person', '@id': authorId, name: 'Jane Doe', url: 'https://acme.com/team/jane/' }),
|
|
739
787
|
buildWebPage({ url, name: title, isPartOf: { '@id': ids.website }, datePublished }, ids),
|
|
740
788
|
buildArticle({
|
|
741
789
|
url,
|
|
@@ -744,8 +792,7 @@ const postPieces = [
|
|
|
744
792
|
datePublished,
|
|
745
793
|
author: { '@id': authorId },
|
|
746
794
|
publisher: { '@id': ids.organization('acme') },
|
|
747
|
-
isPartOf: { '@id': ids.webPage(url) },
|
|
748
|
-
extra: { isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }] },
|
|
795
|
+
isPartOf: [{ '@id': ids.webPage(url) }, { '@id': blogId }],
|
|
749
796
|
}, ids, 'BlogPosting'),
|
|
750
797
|
buildBreadcrumbList({ url, items: [{ name: 'Home', url: siteUrl }, { name: 'Blog', url: blogUrl }, { name: title, url }] }, ids),
|
|
751
798
|
];
|
|
@@ -755,16 +802,23 @@ const postPieces = [
|
|
|
755
802
|
|
|
756
803
|
### E-commerce / product page
|
|
757
804
|
|
|
758
|
-
Use `
|
|
759
|
-
builder because product schemas vary wildly.
|
|
805
|
+
Use `buildPiece<Product>` for `Product` and `buildPiece<ProductGroup>` for `ProductGroup` entities.
|
|
760
806
|
|
|
761
807
|
**Simple product (single variant):**
|
|
762
808
|
|
|
763
809
|
```ts
|
|
810
|
+
import type { Organization, Product } from 'schema-dts';
|
|
811
|
+
|
|
764
812
|
const ids = makeIds({ siteUrl: 'https://shop.example.com' });
|
|
765
813
|
|
|
766
814
|
const pieces = [
|
|
767
|
-
|
|
815
|
+
buildPiece<Organization>({
|
|
816
|
+
'@type': 'Organization',
|
|
817
|
+
'@id': ids.organization('shop'),
|
|
818
|
+
name: 'Example Shop',
|
|
819
|
+
url: siteUrl,
|
|
820
|
+
logo: logoUrl,
|
|
821
|
+
}),
|
|
768
822
|
buildWebSite(
|
|
769
823
|
{ url: siteUrl, name: 'Example Shop', publisher: { '@id': ids.organization('shop') } },
|
|
770
824
|
ids,
|
|
@@ -789,13 +843,12 @@ const pieces = [
|
|
|
789
843
|
},
|
|
790
844
|
ids,
|
|
791
845
|
),
|
|
792
|
-
|
|
846
|
+
buildPiece<Product>({
|
|
793
847
|
'@type': 'Product',
|
|
794
848
|
'@id': `${url}#product`,
|
|
795
849
|
name: productName,
|
|
796
850
|
description: productDescription,
|
|
797
|
-
|
|
798
|
-
brand: { '@type': 'Brand', name: 'Nike' },
|
|
851
|
+
brand: 'Nike',
|
|
799
852
|
sku: 'ABC123',
|
|
800
853
|
offers: {
|
|
801
854
|
'@type': 'Offer',
|
|
@@ -818,6 +871,7 @@ const pieces = [
|
|
|
818
871
|
},
|
|
819
872
|
seller: { '@id': ids.organization('shop') },
|
|
820
873
|
},
|
|
874
|
+
image: productImageUrl,
|
|
821
875
|
}),
|
|
822
876
|
];
|
|
823
877
|
```
|
|
@@ -828,6 +882,8 @@ When a product has multiple variants (e.g. sizes, colors), use `ProductGroup`
|
|
|
828
882
|
as the parent and individual `Product` entities for each variant:
|
|
829
883
|
|
|
830
884
|
```ts
|
|
885
|
+
import type { Product, ProductGroup } from 'schema-dts';
|
|
886
|
+
|
|
831
887
|
const variants = [
|
|
832
888
|
{
|
|
833
889
|
sku: 'SHOE-BLK-10',
|
|
@@ -857,25 +913,23 @@ const variants = [
|
|
|
857
913
|
|
|
858
914
|
const pieces = [
|
|
859
915
|
// ...site-wide + WebPage + BreadcrumbList...
|
|
860
|
-
|
|
916
|
+
buildPiece<ProductGroup>({
|
|
861
917
|
'@type': 'ProductGroup',
|
|
862
|
-
'@id': `${url}#product
|
|
918
|
+
'@id': `${url}#product`,
|
|
863
919
|
name: 'Running Shoe',
|
|
864
920
|
description: productDescription,
|
|
865
|
-
brand:
|
|
921
|
+
brand: 'Nike',
|
|
922
|
+
url,
|
|
866
923
|
productGroupID: 'running-shoe',
|
|
867
924
|
variesBy: ['https://schema.org/color', 'https://schema.org/size'],
|
|
868
|
-
hasVariant: variants.map((v) => ({ '@id': `${url}#
|
|
925
|
+
hasVariant: variants.map((v) => ({ '@id': `${url}#product-${v.sku}` })),
|
|
869
926
|
}),
|
|
870
927
|
...variants.map((v) =>
|
|
871
|
-
|
|
928
|
+
buildPiece<Product>({
|
|
872
929
|
'@type': 'Product',
|
|
873
|
-
'@id': `${url}#
|
|
930
|
+
'@id': `${url}#product-${v.sku}`,
|
|
874
931
|
name: v.name,
|
|
875
932
|
sku: v.sku,
|
|
876
|
-
color: v.color,
|
|
877
|
-
size: v.size,
|
|
878
|
-
image: [productImageUrl],
|
|
879
933
|
offers: {
|
|
880
934
|
'@type': 'Offer',
|
|
881
935
|
price: v.price,
|
|
@@ -896,6 +950,9 @@ const pieces = [
|
|
|
896
950
|
shippingDestination: { '@type': 'DefinedRegion', addressCountry: 'US' },
|
|
897
951
|
},
|
|
898
952
|
},
|
|
953
|
+
color: v.color,
|
|
954
|
+
size: v.size,
|
|
955
|
+
image: [productImageUrl],
|
|
899
956
|
}),
|
|
900
957
|
),
|
|
901
958
|
];
|
|
@@ -913,48 +970,43 @@ import type { Restaurant } from 'schema-dts';
|
|
|
913
970
|
const ids = makeIds({ siteUrl: 'https://chezexample.com' });
|
|
914
971
|
|
|
915
972
|
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
|
-
},
|
|
973
|
+
buildPiece<Restaurant>({
|
|
974
|
+
'@type': 'Restaurant',
|
|
975
|
+
'@id': ids.organization('chez-example'),
|
|
976
|
+
name: 'Chez Example',
|
|
977
|
+
url: 'https://chezexample.com/',
|
|
978
|
+
logo: logoUrl,
|
|
979
|
+
sameAs: ['https://instagram.com/chezexample'],
|
|
980
|
+
address: {
|
|
981
|
+
'@type': 'PostalAddress',
|
|
982
|
+
streetAddress: '123 Rue de la Paix',
|
|
983
|
+
addressLocality: 'Paris',
|
|
984
|
+
postalCode: '75002',
|
|
985
|
+
addressCountry: 'FR',
|
|
954
986
|
},
|
|
955
|
-
|
|
956
|
-
'
|
|
957
|
-
|
|
987
|
+
telephone: '+33-1-23-45-67-89',
|
|
988
|
+
priceRange: '$$$',
|
|
989
|
+
servesCuisine: 'French',
|
|
990
|
+
geo: {
|
|
991
|
+
'@type': 'GeoCoordinates',
|
|
992
|
+
latitude: 48.8698,
|
|
993
|
+
longitude: 2.3311,
|
|
994
|
+
},
|
|
995
|
+
openingHoursSpecification: [
|
|
996
|
+
{
|
|
997
|
+
'@type': 'OpeningHoursSpecification',
|
|
998
|
+
dayOfWeek: ['Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
999
|
+
opens: '12:00',
|
|
1000
|
+
closes: '14:30',
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
'@type': 'OpeningHoursSpecification',
|
|
1004
|
+
dayOfWeek: ['Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
1005
|
+
opens: '19:00',
|
|
1006
|
+
closes: '22:30',
|
|
1007
|
+
},
|
|
1008
|
+
],
|
|
1009
|
+
}),
|
|
958
1010
|
buildWebSite(
|
|
959
1011
|
{
|
|
960
1012
|
url: siteUrl,
|
|
@@ -981,20 +1033,21 @@ const pieces = [
|
|
|
981
1033
|
A freelancer or agency showcasing work.
|
|
982
1034
|
|
|
983
1035
|
```ts
|
|
1036
|
+
import type { Person } from 'schema-dts';
|
|
1037
|
+
|
|
984
1038
|
const ids = makeIds({ siteUrl: 'https://janedoe.design' });
|
|
985
1039
|
|
|
986
1040
|
// Homepage — CollectionPage showcasing work
|
|
987
1041
|
const pieces = [
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
),
|
|
1042
|
+
buildPiece<Person>({
|
|
1043
|
+
'@type': 'Person',
|
|
1044
|
+
'@id': ids.person,
|
|
1045
|
+
name: 'Jane Doe',
|
|
1046
|
+
jobTitle: 'Product Designer',
|
|
1047
|
+
url: siteUrl,
|
|
1048
|
+
image: { '@id': ids.personImage },
|
|
1049
|
+
sameAs: [dribbble, linkedin],
|
|
1050
|
+
}),
|
|
998
1051
|
buildImageObject({ id: ids.personImage, url: headshot, width: 400, height: 400 }, ids),
|
|
999
1052
|
buildWebSite({ url: siteUrl, name: 'Jane Doe Design', publisher: { '@id': ids.person } }, ids),
|
|
1000
1053
|
buildWebPage(
|
|
@@ -1055,13 +1108,18 @@ const projectPieces = [
|
|
|
1055
1108
|
A docs site for a software project or API.
|
|
1056
1109
|
|
|
1057
1110
|
```ts
|
|
1111
|
+
import type { Organization } from 'schema-dts';
|
|
1112
|
+
|
|
1058
1113
|
const ids = makeIds({ siteUrl: 'https://docs.example.com' });
|
|
1059
1114
|
|
|
1060
1115
|
const pieces = [
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
ids,
|
|
1064
|
-
|
|
1116
|
+
buildPiece<Organization>({
|
|
1117
|
+
'@type': 'Organization',
|
|
1118
|
+
'@id': ids.organization('example'),
|
|
1119
|
+
name: 'Example Inc',
|
|
1120
|
+
url: 'https://example.com/',
|
|
1121
|
+
logo: logoUrl,
|
|
1122
|
+
}),
|
|
1065
1123
|
buildWebSite(
|
|
1066
1124
|
{
|
|
1067
1125
|
url: siteUrl,
|
|
@@ -1109,15 +1167,23 @@ container for `PodcastEpisode`. Include the series as a site-wide entity.
|
|
|
1109
1167
|
**Video podcast (YouTube-based):**
|
|
1110
1168
|
|
|
1111
1169
|
```ts
|
|
1170
|
+
import type { Person, PodcastSeries } from 'schema-dts';
|
|
1171
|
+
|
|
1112
1172
|
const ids = makeIds({ siteUrl: 'https://podcast.example.com' });
|
|
1113
1173
|
const seriesId = `${siteUrl}#podcast-series`;
|
|
1114
1174
|
|
|
1115
1175
|
// Episode page
|
|
1116
1176
|
const pieces = [
|
|
1117
|
-
|
|
1177
|
+
buildPiece<Person>({
|
|
1178
|
+
'@type': 'Person',
|
|
1179
|
+
'@id': ids.person,
|
|
1180
|
+
name: 'Host Name',
|
|
1181
|
+
url: aboutUrl,
|
|
1182
|
+
image: { '@id': ids.personImage },
|
|
1183
|
+
}),
|
|
1118
1184
|
buildImageObject({ id: ids.personImage, url: hostPhotoUrl, width: 400, height: 400 }, ids),
|
|
1119
1185
|
buildWebSite({ url: siteUrl, name: 'My Podcast', publisher: { '@id': ids.person } }, ids),
|
|
1120
|
-
|
|
1186
|
+
buildPiece<PodcastSeries>({
|
|
1121
1187
|
'@type': 'PodcastSeries',
|
|
1122
1188
|
'@id': seriesId,
|
|
1123
1189
|
name: 'My Podcast',
|
|
@@ -1170,12 +1236,14 @@ const pieces = [
|
|
|
1170
1236
|
Use `PodcastEpisode` linked to the `PodcastSeries`:
|
|
1171
1237
|
|
|
1172
1238
|
```ts
|
|
1239
|
+
import type { PodcastEpisode } from 'schema-dts';
|
|
1240
|
+
|
|
1173
1241
|
const seriesId = `${siteUrl}#podcast-series`;
|
|
1174
1242
|
|
|
1175
1243
|
const pieces = [
|
|
1176
1244
|
// ...site-wide entities including PodcastSeries...
|
|
1177
1245
|
buildWebPage({ url, name: episodeTitle, isPartOf: { '@id': ids.website }, datePublished }, ids),
|
|
1178
|
-
|
|
1246
|
+
buildPiece<PodcastEpisode>({
|
|
1179
1247
|
'@type': 'PodcastEpisode',
|
|
1180
1248
|
'@id': `${url}#episode`,
|
|
1181
1249
|
name: episodeTitle,
|
|
@@ -1225,10 +1293,12 @@ const pieces = [
|
|
|
1225
1293
|
### Vacation rental / accommodation
|
|
1226
1294
|
|
|
1227
1295
|
```ts
|
|
1296
|
+
import type { Person, VacationRental } from 'schema-dts';
|
|
1297
|
+
|
|
1228
1298
|
const ids = makeIds({ siteUrl: 'https://myhouse.example.com' });
|
|
1229
1299
|
|
|
1230
1300
|
const pieces = [
|
|
1231
|
-
|
|
1301
|
+
buildPiece<Person>({ '@type': 'Person', '@id': ids.person, name: 'Owner Name', url: siteUrl }),
|
|
1232
1302
|
buildWebSite({ url: siteUrl, name: 'Villa Example', publisher: { '@id': ids.person } }, ids),
|
|
1233
1303
|
buildWebPage(
|
|
1234
1304
|
{
|
|
@@ -1238,7 +1308,7 @@ const pieces = [
|
|
|
1238
1308
|
},
|
|
1239
1309
|
ids,
|
|
1240
1310
|
),
|
|
1241
|
-
|
|
1311
|
+
buildPiece<VacationRental>({
|
|
1242
1312
|
'@type': 'VacationRental',
|
|
1243
1313
|
'@id': `${siteUrl}#rental`,
|
|
1244
1314
|
name: 'Villa Example',
|
|
@@ -1289,6 +1359,8 @@ const pieces = [
|
|
|
1289
1359
|
### Recipe site
|
|
1290
1360
|
|
|
1291
1361
|
```ts
|
|
1362
|
+
import type { Recipe } from 'schema-dts';
|
|
1363
|
+
|
|
1292
1364
|
const ids = makeIds({ siteUrl: 'https://recipes.example.com' });
|
|
1293
1365
|
|
|
1294
1366
|
const pieces = [
|
|
@@ -1314,14 +1386,11 @@ const pieces = [
|
|
|
1314
1386
|
},
|
|
1315
1387
|
ids,
|
|
1316
1388
|
),
|
|
1317
|
-
|
|
1389
|
+
buildPiece<Recipe>({
|
|
1318
1390
|
'@type': 'Recipe',
|
|
1319
1391
|
'@id': `${url}#recipe`,
|
|
1320
1392
|
name: recipeName,
|
|
1321
|
-
description: recipeDescription,
|
|
1322
|
-
image: recipeImageUrl,
|
|
1323
1393
|
author: { '@id': ids.person },
|
|
1324
|
-
datePublished: publishDate.toISOString(),
|
|
1325
1394
|
prepTime: 'PT15M',
|
|
1326
1395
|
cookTime: 'PT45M',
|
|
1327
1396
|
totalTime: 'PT1H',
|
|
@@ -1344,6 +1413,9 @@ const pieces = [
|
|
|
1344
1413
|
{ '@type': 'HowToStep', text: 'Mix egg yolks with pecorino.' },
|
|
1345
1414
|
{ '@type': 'HowToStep', text: 'Combine and serve immediately.' },
|
|
1346
1415
|
],
|
|
1416
|
+
description: recipeDescription,
|
|
1417
|
+
image: recipeImageUrl,
|
|
1418
|
+
datePublished: publishDate.toISOString(),
|
|
1347
1419
|
}),
|
|
1348
1420
|
];
|
|
1349
1421
|
```
|
|
@@ -1353,7 +1425,9 @@ const pieces = [
|
|
|
1353
1425
|
### Event page
|
|
1354
1426
|
|
|
1355
1427
|
```ts
|
|
1356
|
-
|
|
1428
|
+
import type { Event } from 'schema-dts';
|
|
1429
|
+
|
|
1430
|
+
buildPiece<Event>({
|
|
1357
1431
|
'@type': 'Event',
|
|
1358
1432
|
'@id': `${url}#event`,
|
|
1359
1433
|
name: 'JavaScript Conference 2026',
|
|
@@ -1389,8 +1463,16 @@ buildCustomPiece({
|
|
|
1389
1463
|
### SaaS / software product landing page
|
|
1390
1464
|
|
|
1391
1465
|
```ts
|
|
1466
|
+
import type { Organization, SoftwareApplication } from 'schema-dts';
|
|
1467
|
+
|
|
1392
1468
|
const pieces = [
|
|
1393
|
-
|
|
1469
|
+
buildPiece<Organization>({
|
|
1470
|
+
'@type': 'Organization',
|
|
1471
|
+
'@id': ids.organization('myapp'),
|
|
1472
|
+
name: 'MyApp Inc',
|
|
1473
|
+
url: siteUrl,
|
|
1474
|
+
logo: logoUrl,
|
|
1475
|
+
}),
|
|
1394
1476
|
buildWebSite(
|
|
1395
1477
|
{ url: siteUrl, name: 'MyApp', publisher: { '@id': ids.organization('myapp') } },
|
|
1396
1478
|
ids,
|
|
@@ -1403,7 +1485,7 @@ const pieces = [
|
|
|
1403
1485
|
},
|
|
1404
1486
|
ids,
|
|
1405
1487
|
),
|
|
1406
|
-
|
|
1488
|
+
buildPiece<SoftwareApplication>({
|
|
1407
1489
|
'@type': 'SoftwareApplication',
|
|
1408
1490
|
'@id': `${siteUrl}#app`,
|
|
1409
1491
|
name: 'MyApp',
|
|
@@ -1443,13 +1525,15 @@ const pieces = [
|
|
|
1443
1525
|
Combine `WebPage` with a `FAQPage` custom piece:
|
|
1444
1526
|
|
|
1445
1527
|
```ts
|
|
1528
|
+
import type { FAQPage } from 'schema-dts';
|
|
1529
|
+
|
|
1446
1530
|
const pieces = [
|
|
1447
1531
|
// ...site-wide entities...
|
|
1448
1532
|
buildWebPage(
|
|
1449
1533
|
{ url, name: 'Frequently Asked Questions', isPartOf: { '@id': ids.website } },
|
|
1450
1534
|
ids,
|
|
1451
1535
|
),
|
|
1452
|
-
|
|
1536
|
+
buildPiece<FAQPage>({
|
|
1453
1537
|
'@type': 'FAQPage',
|
|
1454
1538
|
'@id': `${url}#faq`,
|
|
1455
1539
|
mainEntity: [
|
|
@@ -1479,7 +1563,9 @@ const pieces = [
|
|
|
1479
1563
|
### Course / educational content
|
|
1480
1564
|
|
|
1481
1565
|
```ts
|
|
1482
|
-
|
|
1566
|
+
import type { Course } from 'schema-dts';
|
|
1567
|
+
|
|
1568
|
+
buildPiece<Course>({
|
|
1483
1569
|
'@type': 'Course',
|
|
1484
1570
|
'@id': `${url}#course`,
|
|
1485
1571
|
name: 'Introduction to TypeScript',
|
|
@@ -1534,17 +1620,19 @@ policies. It can be applied to `Organization`, `Person`, or `CreativeWork`
|
|
|
1534
1620
|
search engines and AI agents about your content's credibility.
|
|
1535
1621
|
|
|
1536
1622
|
```ts
|
|
1623
|
+
import type { Person, Blog, Organization } from 'schema-dts';
|
|
1624
|
+
|
|
1537
1625
|
// On a Person entity (personal blog)
|
|
1538
|
-
|
|
1626
|
+
buildPiece<Person>({
|
|
1627
|
+
'@type': 'Person',
|
|
1628
|
+
'@id': ids.person,
|
|
1539
1629
|
name: 'Jane Doe',
|
|
1540
1630
|
url: aboutUrl,
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
},
|
|
1544
|
-
}, ids),
|
|
1631
|
+
publishingPrinciples: `${siteUrl}/editorial-policy/`,
|
|
1632
|
+
}),
|
|
1545
1633
|
|
|
1546
1634
|
// On a Blog entity
|
|
1547
|
-
|
|
1635
|
+
buildPiece<Blog>({
|
|
1548
1636
|
'@type': 'Blog',
|
|
1549
1637
|
'@id': blogId,
|
|
1550
1638
|
name: 'My Blog',
|
|
@@ -1554,13 +1642,12 @@ buildCustomPiece({
|
|
|
1554
1642
|
}),
|
|
1555
1643
|
|
|
1556
1644
|
// On an Organization (news site, company blog)
|
|
1557
|
-
|
|
1558
|
-
|
|
1645
|
+
buildPiece<Organization>({
|
|
1646
|
+
'@type': 'Organization',
|
|
1647
|
+
'@id': ids.organization('newsroom'),
|
|
1559
1648
|
name: 'The Daily Example',
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
},
|
|
1563
|
-
}, ids),
|
|
1649
|
+
publishingPrinciples: `${siteUrl}/ethics/`,
|
|
1650
|
+
}),
|
|
1564
1651
|
```
|
|
1565
1652
|
|
|
1566
1653
|
### Specialized policy sub-properties
|
|
@@ -1569,21 +1656,22 @@ For news and media organizations, schema.org has more specific sub-properties
|
|
|
1569
1656
|
of `publishingPrinciples`:
|
|
1570
1657
|
|
|
1571
1658
|
```ts
|
|
1572
|
-
|
|
1573
|
-
|
|
1659
|
+
import type { Organization } from 'schema-dts';
|
|
1660
|
+
|
|
1661
|
+
buildPiece<Organization>({
|
|
1662
|
+
'@type': 'Organization',
|
|
1663
|
+
'@id': ids.organization('newsroom'),
|
|
1574
1664
|
name: 'The Daily Example',
|
|
1575
1665
|
url: siteUrl,
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
},
|
|
1586
|
-
}, ids),
|
|
1666
|
+
publishingPrinciples: `${siteUrl}/editorial-policy/`,
|
|
1667
|
+
correctionsPolicy: `${siteUrl}/corrections/`,
|
|
1668
|
+
verificationFactCheckingPolicy: `${siteUrl}/fact-checking/`,
|
|
1669
|
+
actionableFeedbackPolicy: `${siteUrl}/feedback/`,
|
|
1670
|
+
unnamedSourcesPolicy: `${siteUrl}/sources-policy/`,
|
|
1671
|
+
ownershipFundingInfo: `${siteUrl}/about/ownership/`,
|
|
1672
|
+
diversityStaffingReport: `${siteUrl}/diversity-report/`,
|
|
1673
|
+
masthead: `${siteUrl}/team/`,
|
|
1674
|
+
}),
|
|
1587
1675
|
```
|
|
1588
1676
|
|
|
1589
1677
|
### When to use which
|
|
@@ -1621,9 +1709,7 @@ buildWebPage({
|
|
|
1621
1709
|
copyrightNotice: '© 2026 Jane Doe. All rights reserved.',
|
|
1622
1710
|
license: 'https://creativecommons.org/licenses/by/4.0/',
|
|
1623
1711
|
isAccessibleForFree: true,
|
|
1624
|
-
|
|
1625
|
-
creditText: 'Jane Doe / janedoe.com',
|
|
1626
|
-
},
|
|
1712
|
+
creditText: 'Jane Doe / janedoe.com',
|
|
1627
1713
|
}, ids),
|
|
1628
1714
|
```
|
|
1629
1715
|
|
|
@@ -1634,11 +1720,9 @@ buildArticle({
|
|
|
1634
1720
|
url,
|
|
1635
1721
|
headline: title,
|
|
1636
1722
|
// ...other article properties...
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
license: 'https://creativecommons.org/licenses/by-sa/4.0/',
|
|
1641
|
-
},
|
|
1723
|
+
copyrightHolder: { '@id': ids.person },
|
|
1724
|
+
copyrightYear: 2026,
|
|
1725
|
+
license: 'https://creativecommons.org/licenses/by-sa/4.0/',
|
|
1642
1726
|
}, ids, 'BlogPosting'),
|
|
1643
1727
|
```
|
|
1644
1728
|
|
|
@@ -1771,7 +1855,9 @@ potentialAction: {
|
|
|
1771
1855
|
Add to the `Product` or `ProductGroup` entity:
|
|
1772
1856
|
|
|
1773
1857
|
```ts
|
|
1774
|
-
|
|
1858
|
+
import type { Product } from 'schema-dts';
|
|
1859
|
+
|
|
1860
|
+
buildPiece<Product>({
|
|
1775
1861
|
'@type': 'Product',
|
|
1776
1862
|
'@id': `${url}#product`,
|
|
1777
1863
|
name: productName,
|
|
@@ -1824,7 +1910,9 @@ potentialAction: {
|
|
|
1824
1910
|
Add to the `VacationRental`, `Product`, or `Car` entity:
|
|
1825
1911
|
|
|
1826
1912
|
```ts
|
|
1827
|
-
|
|
1913
|
+
import type { VacationRental } from 'schema-dts';
|
|
1914
|
+
|
|
1915
|
+
buildPiece<VacationRental>({
|
|
1828
1916
|
'@type': 'VacationRental',
|
|
1829
1917
|
'@id': `${siteUrl}#rental`,
|
|
1830
1918
|
name: 'Villa Example',
|
|
@@ -1933,29 +2021,29 @@ Many entities benefit from multiple actions. A WebSite typically has a
|
|
|
1933
2021
|
`SearchAction`; the entities within it have trade actions:
|
|
1934
2022
|
|
|
1935
2023
|
```ts
|
|
2024
|
+
import type { Product } from 'schema-dts';
|
|
2025
|
+
|
|
1936
2026
|
// WebSite: how to search
|
|
1937
2027
|
buildWebSite({
|
|
1938
2028
|
url: siteUrl,
|
|
1939
2029
|
name: 'My Shop',
|
|
1940
2030
|
publisher: { '@id': ids.organization('shop') },
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
'
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
valueName: 'search_term_string',
|
|
1952
|
-
},
|
|
2031
|
+
potentialAction: {
|
|
2032
|
+
'@type': 'SearchAction',
|
|
2033
|
+
target: {
|
|
2034
|
+
'@type': 'EntryPoint',
|
|
2035
|
+
urlTemplate: `${siteUrl}search?q={search_term_string}`,
|
|
2036
|
+
},
|
|
2037
|
+
'query-input': {
|
|
2038
|
+
'@type': 'PropertyValueSpecification',
|
|
2039
|
+
valueRequired: true,
|
|
2040
|
+
valueName: 'search_term_string',
|
|
1953
2041
|
},
|
|
1954
2042
|
},
|
|
1955
2043
|
}, ids),
|
|
1956
2044
|
|
|
1957
2045
|
// Product: how to buy
|
|
1958
|
-
|
|
2046
|
+
buildPiece<Product>({
|
|
1959
2047
|
'@type': 'Product',
|
|
1960
2048
|
'@id': `${url}#product`,
|
|
1961
2049
|
name: productName,
|
|
@@ -1994,7 +2082,7 @@ An entity can have multiple `@type` values. This is useful when an entity
|
|
|
1994
2082
|
legitimately belongs to more than one type:
|
|
1995
2083
|
|
|
1996
2084
|
```ts
|
|
1997
|
-
|
|
2085
|
+
buildPiece({
|
|
1998
2086
|
'@type': ['Organization', 'Brand'],
|
|
1999
2087
|
'@id': ids.organization('acme'),
|
|
2000
2088
|
name: 'Acme',
|
|
@@ -2015,18 +2103,15 @@ Common multi-type combinations:
|
|
|
2015
2103
|
- `['WebPage', 'ItemPage']` — Product detail pages
|
|
2016
2104
|
- `['WebPage', 'FAQPage']` — FAQ pages (alternative to separate FAQPage entity)
|
|
2017
2105
|
|
|
2018
|
-
**Note:**
|
|
2019
|
-
subtype as the third argument and add additional types via `extra`:
|
|
2106
|
+
**Note:** With `buildPiece`, pass the `@type` array directly:
|
|
2020
2107
|
|
|
2021
2108
|
```ts
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
ids,
|
|
2029
|
-
);
|
|
2109
|
+
buildPiece({
|
|
2110
|
+
'@type': ['Organization', 'Brand'],
|
|
2111
|
+
'@id': ids.organization('acme'),
|
|
2112
|
+
name: 'Acme',
|
|
2113
|
+
url: 'https://acme.com/',
|
|
2114
|
+
});
|
|
2030
2115
|
```
|
|
2031
2116
|
|
|
2032
2117
|
---
|
|
@@ -2037,49 +2122,47 @@ For established businesses, a richer Organization entity improves knowledge
|
|
|
2037
2122
|
graph representation. Here's the full pattern:
|
|
2038
2123
|
|
|
2039
2124
|
```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
|
-
},
|
|
2125
|
+
import type { Organization } from 'schema-dts';
|
|
2126
|
+
|
|
2127
|
+
buildPiece<Organization>({
|
|
2128
|
+
'@type': 'Organization',
|
|
2129
|
+
'@id': ids.organization('acme'),
|
|
2130
|
+
name: 'Acme Corp',
|
|
2131
|
+
url: 'https://acme.com/',
|
|
2132
|
+
logo: 'https://acme.com/logo.png',
|
|
2133
|
+
description: 'We build developer tools.',
|
|
2134
|
+
sameAs: [
|
|
2135
|
+
'https://twitter.com/acme',
|
|
2136
|
+
'https://linkedin.com/company/acme',
|
|
2137
|
+
'https://github.com/acme',
|
|
2138
|
+
'https://en.wikipedia.org/wiki/Acme_Corp',
|
|
2139
|
+
],
|
|
2140
|
+
legalName: 'Acme Corp B.V.',
|
|
2141
|
+
foundingDate: '2015-03-01',
|
|
2142
|
+
founder: {
|
|
2143
|
+
'@type': 'Person',
|
|
2144
|
+
name: 'Jane Doe',
|
|
2145
|
+
sameAs: 'https://en.wikipedia.org/wiki/Jane_Doe',
|
|
2080
2146
|
},
|
|
2081
|
-
|
|
2082
|
-
|
|
2147
|
+
numberOfEmployees: 45,
|
|
2148
|
+
slogan: 'Tools for the modern web',
|
|
2149
|
+
parentOrganization: {
|
|
2150
|
+
'@type': 'Organization',
|
|
2151
|
+
name: 'Parent Holdings Inc',
|
|
2152
|
+
url: 'https://parent.com/',
|
|
2153
|
+
},
|
|
2154
|
+
memberOf: {
|
|
2155
|
+
'@type': 'Organization',
|
|
2156
|
+
name: 'World Wide Web Consortium (W3C)',
|
|
2157
|
+
url: 'https://w3.org/',
|
|
2158
|
+
},
|
|
2159
|
+
address: {
|
|
2160
|
+
'@type': 'PostalAddress',
|
|
2161
|
+
streetAddress: '123 Tech Lane',
|
|
2162
|
+
addressLocality: 'Amsterdam',
|
|
2163
|
+
addressCountry: 'NL',
|
|
2164
|
+
},
|
|
2165
|
+
});
|
|
2083
2166
|
```
|
|
2084
2167
|
|
|
2085
2168
|
Include as much as is factually accurate. Don't fabricate data. Properties like
|
|
@@ -2094,63 +2177,57 @@ For personal sites, a detailed Person entity establishes identity and
|
|
|
2094
2177
|
credibility. jonoalderson.com uses 80+ entities. Here's the extended pattern:
|
|
2095
2178
|
|
|
2096
2179
|
```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',
|
|
2180
|
+
import type { Person } from 'schema-dts';
|
|
2181
|
+
|
|
2182
|
+
buildPiece<Person>({
|
|
2183
|
+
'@type': 'Person',
|
|
2184
|
+
'@id': ids.person,
|
|
2185
|
+
name: 'Jane Doe',
|
|
2186
|
+
familyName: 'Doe',
|
|
2187
|
+
birthDate: '1990-01-15',
|
|
2188
|
+
gender: 'female',
|
|
2189
|
+
nationality: { '@id': ids.country('US') },
|
|
2190
|
+
description: 'Software engineer and technical writer.',
|
|
2191
|
+
jobTitle: 'Lead Engineer',
|
|
2192
|
+
knowsLanguage: ['en', 'es', 'pt'],
|
|
2193
|
+
url: 'https://janedoe.com/about/',
|
|
2194
|
+
image: { '@id': ids.personImage },
|
|
2195
|
+
sameAs: [
|
|
2196
|
+
'https://twitter.com/janedoe',
|
|
2197
|
+
'https://github.com/janedoe',
|
|
2198
|
+
'https://linkedin.com/in/janedoe',
|
|
2199
|
+
'https://bsky.app/profile/janedoe.com',
|
|
2200
|
+
'https://mastodon.social/@janedoe',
|
|
2201
|
+
'https://en.wikipedia.org/wiki/Jane_Doe',
|
|
2202
|
+
],
|
|
2203
|
+
worksFor: [
|
|
2204
|
+
{
|
|
2205
|
+
'@type': 'EmployeeRole',
|
|
2206
|
+
roleName: 'Lead Engineer',
|
|
2207
|
+
startDate: '2022-01',
|
|
2208
|
+
worksFor: { '@id': ids.organization('acme') },
|
|
2135
2209
|
},
|
|
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'],
|
|
2210
|
+
{
|
|
2211
|
+
'@type': 'EmployeeRole',
|
|
2212
|
+
roleName: 'Advisor',
|
|
2213
|
+
startDate: '2024-06',
|
|
2214
|
+
worksFor: { '@id': ids.organization('startup') },
|
|
2150
2215
|
},
|
|
2216
|
+
],
|
|
2217
|
+
spouse: {
|
|
2218
|
+
'@type': 'Person',
|
|
2219
|
+
'@id': `${siteUrl}/#/schema.org/Person/john`,
|
|
2220
|
+
name: 'John Doe',
|
|
2151
2221
|
},
|
|
2152
|
-
|
|
2153
|
-
|
|
2222
|
+
knowsAbout: ['TypeScript', 'Schema.org', 'Search Engine Optimization', 'Web Performance'],
|
|
2223
|
+
honorificPrefix: 'Dr.',
|
|
2224
|
+
alumniOf: {
|
|
2225
|
+
'@type': 'EducationalOrganization',
|
|
2226
|
+
name: 'MIT',
|
|
2227
|
+
url: 'https://mit.edu/',
|
|
2228
|
+
},
|
|
2229
|
+
award: ['Best Developer Blog 2025', 'Open Source Contributor of the Year 2024'],
|
|
2230
|
+
});
|
|
2154
2231
|
```
|
|
2155
2232
|
|
|
2156
2233
|
**Practical advice:**
|
|
@@ -2533,13 +2610,14 @@ import {
|
|
|
2533
2610
|
makeIds,
|
|
2534
2611
|
assembleGraph,
|
|
2535
2612
|
buildWebSite,
|
|
2536
|
-
|
|
2613
|
+
buildPiece,
|
|
2537
2614
|
buildWebPage,
|
|
2538
2615
|
buildArticle,
|
|
2539
2616
|
buildBreadcrumbList,
|
|
2540
2617
|
buildImageObject,
|
|
2541
2618
|
buildSiteNavigationElement,
|
|
2542
2619
|
} from '@jdevalk/seo-graph-core';
|
|
2620
|
+
import type { Person } from 'schema-dts';
|
|
2543
2621
|
|
|
2544
2622
|
const SITE_URL = 'https://example.com';
|
|
2545
2623
|
export const ids = makeIds({ siteUrl: SITE_URL, personUrl: `${SITE_URL}/about/` });
|
|
@@ -2551,15 +2629,14 @@ function siteWideEntities() {
|
|
|
2551
2629
|
{ url: `${SITE_URL}/`, name: 'My Blog', publisher: { '@id': ids.person } },
|
|
2552
2630
|
ids,
|
|
2553
2631
|
),
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
),
|
|
2632
|
+
buildPiece<Person>({
|
|
2633
|
+
'@type': 'Person',
|
|
2634
|
+
'@id': ids.person,
|
|
2635
|
+
name: 'Jane Doe',
|
|
2636
|
+
url: `${SITE_URL}/about/`,
|
|
2637
|
+
image: { '@id': ids.personImage },
|
|
2638
|
+
sameAs: ['...'],
|
|
2639
|
+
}),
|
|
2563
2640
|
buildImageObject(
|
|
2564
2641
|
{ id: ids.personImage, url: `${SITE_URL}/jane.jpg`, width: 400, height: 400 },
|
|
2565
2642
|
ids,
|
|
@@ -2795,70 +2872,76 @@ export const GET = createSchemaMap({
|
|
|
2795
2872
|
When a person works for several companies, create an organization for each:
|
|
2796
2873
|
|
|
2797
2874
|
```ts
|
|
2875
|
+
import type { Organization, Person } from 'schema-dts';
|
|
2876
|
+
|
|
2798
2877
|
const orgs = [
|
|
2799
2878
|
{ slug: 'acme', name: 'Acme Corp', url: 'https://acme.com/' },
|
|
2800
2879
|
{ slug: 'side-project', name: 'Side Project Inc', url: 'https://sideproject.com/' },
|
|
2801
2880
|
];
|
|
2802
2881
|
|
|
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,
|
|
2882
|
+
const orgPieces = orgs.map((org) =>
|
|
2883
|
+
buildPiece<Organization>({
|
|
2884
|
+
'@type': 'Organization',
|
|
2885
|
+
'@id': ids.organization(org.slug),
|
|
2886
|
+
name: org.name,
|
|
2887
|
+
url: org.url,
|
|
2888
|
+
}),
|
|
2824
2889
|
);
|
|
2890
|
+
|
|
2891
|
+
const personPiece = buildPiece<Person>({
|
|
2892
|
+
'@type': 'Person',
|
|
2893
|
+
'@id': ids.person,
|
|
2894
|
+
name: 'Jane Doe',
|
|
2895
|
+
worksFor: [
|
|
2896
|
+
{
|
|
2897
|
+
'@type': 'EmployeeRole',
|
|
2898
|
+
roleName: 'CEO',
|
|
2899
|
+
startDate: '2020',
|
|
2900
|
+
worksFor: { '@id': ids.organization('acme') },
|
|
2901
|
+
},
|
|
2902
|
+
{
|
|
2903
|
+
'@type': 'EmployeeRole',
|
|
2904
|
+
roleName: 'Advisor',
|
|
2905
|
+
startDate: '2023',
|
|
2906
|
+
worksFor: { '@id': ids.organization('side-project') },
|
|
2907
|
+
},
|
|
2908
|
+
],
|
|
2909
|
+
});
|
|
2825
2910
|
```
|
|
2826
2911
|
|
|
2827
2912
|
### Organization subtypes
|
|
2828
2913
|
|
|
2829
|
-
Use
|
|
2914
|
+
Use the schema.org subtype directly as the `buildPiece` generic for full type safety:
|
|
2830
2915
|
|
|
2831
2916
|
```ts
|
|
2832
|
-
import type { Dentist, Hotel
|
|
2917
|
+
import type { Dentist, Hotel } from 'schema-dts';
|
|
2833
2918
|
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
ids,
|
|
2837
|
-
'
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
},
|
|
2849
|
-
ids,
|
|
2850
|
-
'Hotel',
|
|
2851
|
-
);
|
|
2919
|
+
buildPiece<Dentist>({
|
|
2920
|
+
'@type': 'Dentist',
|
|
2921
|
+
'@id': ids.organization('clinic'),
|
|
2922
|
+
name: 'Smile Dental',
|
|
2923
|
+
medicalSpecialty: 'Dentistry',
|
|
2924
|
+
});
|
|
2925
|
+
buildPiece<Hotel>({
|
|
2926
|
+
'@type': 'Hotel',
|
|
2927
|
+
'@id': ids.organization('hotel'),
|
|
2928
|
+
name: 'Grand Hotel',
|
|
2929
|
+
starRating: { '@type': 'Rating', ratingValue: 4 },
|
|
2930
|
+
checkinTime: '15:00',
|
|
2931
|
+
checkoutTime: '11:00',
|
|
2932
|
+
});
|
|
2852
2933
|
```
|
|
2853
2934
|
|
|
2854
2935
|
### Multi-author blogs
|
|
2855
2936
|
|
|
2856
|
-
When different posts have different authors, use `
|
|
2857
|
-
|
|
2937
|
+
When different posts have different authors, use `buildPiece<Person>` with a
|
|
2938
|
+
custom `@id` for each author (reserving `ids.person` for the site-wide person):
|
|
2858
2939
|
|
|
2859
2940
|
```ts
|
|
2941
|
+
import type { Person } from 'schema-dts';
|
|
2942
|
+
|
|
2860
2943
|
const authorId = `${siteUrl}/authors/${authorSlug}/#person`;
|
|
2861
|
-
const authorPiece =
|
|
2944
|
+
const authorPiece = buildPiece<Person>({
|
|
2862
2945
|
'@type': 'Person',
|
|
2863
2946
|
'@id': authorId,
|
|
2864
2947
|
name: authorName,
|