@indaco/sveo 1.0.0 → 1.0.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.
@@ -0,0 +1,89 @@
1
+ # PageMetaTags
2
+
3
+ Easily add metadata (title, canonical url, description, keywords) to your pages as well as attach [OpenGraph] and [TwitterCard] to let pages become rich objects.
4
+
5
+ ## Usage
6
+
7
+ ### Article Example
8
+
9
+ ```html
10
+ <script lang="ts">
11
+ import type { SEOWebPage } from '@indaco/sveo/types';
12
+ import { OpenGraphType, TwitterCardType } from '@indaco/sveo/types';
13
+ import { PageMetaTags } from '@indaco/sveo/metadata';
14
+
15
+ const samplePage: SEOWebPage = {
16
+ url: 'https://example.com/posts/getting-started',
17
+ title: 'Getting Started Article',
18
+ description: 'This is the description for the Getting Started Article',
19
+ author: 'Your Name',
20
+ keywords: ['sveltekit', 'components', 'tests', 'vitest'],
21
+ opengraph: {
22
+ type: OpenGraphType.Article,
23
+ article: {
24
+ published_time: '23-01-2022',
25
+ modified_time: '24-01-2022'
26
+ }
27
+ },
28
+ twitter: {
29
+ type: TwitterCardType.Large,
30
+ site: '@username'
31
+ }
32
+ };
33
+ <script>
34
+
35
+ <PageMetaTags data={samplePage} />
36
+ ```
37
+
38
+ ### Music Album Example
39
+
40
+ ```html
41
+ <script>
42
+ import type { SEOWebPageMetadata } from '@indaco/sveo/types';
43
+ import { OpenGraphType } from '@indaco/sveo/types';
44
+ import { PageMetaTags } from '@indaco/sveo/metadata';
45
+
46
+ const sampleMusicAlbum: SEOWebPageMetadata = {
47
+ url: 'https://www.dgmlive.com/',
48
+ title: 'In the Court of the Crimson King',
49
+ description: 'Description for the album',
50
+ keywords: 'progressive rock, jazz rock, rock',
51
+ opengraph: {
52
+ type: OpenGraphType.MusicAlbum,
53
+ album: {
54
+ url: 'https://open.spotify.com/album/6tVg2Wl9hVKMpHYcAl2V2M?si=dJtzXM7ATvmLOn9NfdDnbg',
55
+ musicians: [
56
+ {
57
+ url: 'https://open.spotify.com/artist/7M1FPw29m5FbicYzS2xdpi?si=w9MGJ88-S3O7tiG5IheXAw'
58
+ }
59
+ ],
60
+ songs: [
61
+ {
62
+ url: 'https://open.spotify.com/track/5L7VBYoosmkmiiDlzumdCe?si=aa49699b95604f8d'
63
+ },
64
+ {
65
+ url: 'https://open.spotify.com/track/4QbpagjMCqSECj6IimTL2n?si=65e9458fe8454eea'
66
+ }
67
+ ],
68
+ release_date: new Date('10-10-1969')
69
+ }
70
+ }
71
+ };
72
+ <script>
73
+
74
+ <PageMetaTags data={sampleMusicAlbum} />
75
+ ```
76
+
77
+ ## Properties
78
+
79
+ ### PageMetags
80
+
81
+ | Prop | Type | Required | Description |
82
+ | :----- | :----------: | :------: | :----------------------------------------- |
83
+ | `data` | [SEOWebPage] | yes | The SEO object containing Page metadata |
84
+
85
+ <!-- Resource Links -->
86
+
87
+ [SEOWebPage]: https://github.com/indaco/sveo/blob/913f83920f7f76183fc7d6ea58eebbceeb82f452/src/lib/types.ts#L34-L43
88
+ [OpenGraph]: https://ogp.me/
89
+ [TwitterCard]: https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards
@@ -21,6 +21,13 @@ let { data } = $props();
21
21
  <meta property="og:description" content={data.description} />
22
22
  {/if}
23
23
 
24
+ {#if data.opengraph.locale}
25
+ <meta property="og:locale" content={data.opengraph.locale} />
26
+ {/if}
27
+ {#if data.opengraph.site_name}
28
+ <meta property="og:site_name" content={data.opengraph.site_name} />
29
+ {/if}
30
+
24
31
  {#if data.image?.url}
25
32
  <meta property="og:image" content={data.image.url} />
26
33
  {#if data.image.alt}
@@ -0,0 +1,58 @@
1
+ # OpenGraph
2
+
3
+ The `OpenGraph` component provides a comprehensive and type-safe way to generate Open Graph `<meta>` tags for your Svelte or SvelteKit application.
4
+
5
+ It is the **only exported component** for Open Graph, and it **automatically handles conditional rendering** of all supported subtypes (e.g. article, book, video, music) internally.
6
+
7
+ ## Supported Open Graph Types
8
+
9
+ The component supports official Open Graph object types, including:
10
+
11
+ | Type | Description |
12
+ | :----------------------------- | :---------------------------------------- |
13
+ | `website` | Basic OG meta (title, description, image) |
14
+ | `article` | Adds `article:*` tags like author, tags |
15
+ | `book` | ISBN, release date, and tags |
16
+ | `profile` | Personal metadata |
17
+ | `business.business` | Contact information |
18
+ | `product` | Product-specific metadata |
19
+ | `music.song` | Song metadata |
20
+ | `music.album` | Album-level metadata |
21
+ | `music.playlist` | Playlist metadata |
22
+ | `music.radio_station` | Radio metadata |
23
+ | `video.movie` | Movie metadata |
24
+ | `video.episode` | Episode metadata (also includes movie) |
25
+ | `video.tv_show`, `video.other` | Handled like movie |
26
+
27
+ ## Usage Example (Article)
28
+
29
+ To use the `OpenGraph` component, pass a typed `SEOWebPage` object with an `opengraph` field and `type`.
30
+
31
+ ```svelte
32
+ <script lang="ts">
33
+ import { OpenGraphType, TwitterCardType } from '@indaco/sveo/types';
34
+ import type { SEOWebPage } from '@indaco/sveo/types';
35
+
36
+ const sampleArticle: SEOWebPage = {
37
+ url: 'https://example.com/posts/getting-started',
38
+ title: 'Getting Started Article',
39
+ description: 'This is the description for the Getting Started Article',
40
+ author: 'Mirco Veltri',
41
+ keywords: ['sveltekit, components, tests, vitest'],
42
+ opengraph: {
43
+ type: OpenGraphType.Article,
44
+ article: {
45
+ tags: ['sveltekit'],
46
+ published_time: '23-01-2022',
47
+ modified_time: '24-01-2022'
48
+ }
49
+ },
50
+ twitter: {
51
+ type: TwitterCardType.Large,
52
+ site: '@indaco'
53
+ }
54
+ };
55
+ </script>
56
+
57
+ <OpenGraph data={sampleArticle} />
58
+ ```
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">import { toISODateString } from '../../../utils.js';
2
2
  let { data } = $props();
3
- const times = [
3
+ const times = $derived([
4
4
  ['published_time', data.opengraph?.article?.published_time],
5
5
  ['modified_time', data.opengraph?.article?.modified_time],
6
6
  ['expiration_time', data.opengraph?.article?.expiration_time]
7
- ];
7
+ ]);
8
8
  </script>
9
9
 
10
10
  {#each times as [key, value] (key)}
@@ -20,7 +20,7 @@ export {};
20
20
  {#if data.opengraph?.business?.postal_code}
21
21
  <meta
22
22
  property="business:contact_data:postal_code"
23
- content={data.opengraph.business.postal_code.toString()}
23
+ content={data.opengraph.business.postal_code}
24
24
  />
25
25
  {/if}
26
26
 
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">import { toISODateString } from '../../../utils.js';
2
- import VideoMovie from './video-movie.svelte';
3
2
  let { data } = $props();
4
3
  </script>
5
4
 
@@ -42,6 +41,6 @@ let { data } = $props();
42
41
  {/each}
43
42
  {/if}
44
43
 
45
- {#if data.opengraph?.episode?.series}
46
- <VideoMovie {data} />
44
+ {#if data.opengraph?.episode?.series?.url}
45
+ <meta property="video:series" content={data.opengraph.episode.series.url} />
47
46
  {/if}
@@ -36,7 +36,7 @@ let { data } = $props();
36
36
  {/if}
37
37
 
38
38
  {#if Array.isArray(data.opengraph?.movie?.tags)}
39
- {@const _tags = data.opengraph?.movie?.tags}
39
+ {@const _tags = data.opengraph.movie.tags}
40
40
  {#each _tags as tag (tag)}
41
41
  <meta property="video:tag" content={tag} />
42
42
  {/each}
@@ -0,0 +1,81 @@
1
+ # TwitterCard
2
+
3
+ Generate [Twitter Card](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards) meta tags using a single, type-safe Svelte component.
4
+
5
+ It supports all official Twitter Card types:
6
+
7
+ - `summary`
8
+ - `summary_large_image`
9
+ - `player`
10
+ - `app`
11
+
12
+ The component automatically renders conditional tags based on the selected card type and the fields present in `data.twitter`.
13
+
14
+ ## Usage
15
+
16
+ ```svelte
17
+ <script lang="ts">
18
+ import TwitterCard from '@indaco/sveo/metadata/twittercard/TwitterCard.svelte';
19
+ import type { SEOWebPage } from '@indaco/sveo/types';
20
+
21
+ const seo: SEOWebPage = {
22
+ title: 'Your Page Title',
23
+ description: 'Page description for Twitter card.',
24
+ image: {
25
+ url: 'https://example.com/og-image.jpg',
26
+ alt: 'An image for Twitter'
27
+ },
28
+ twitter: {
29
+ type: 'summary_large_image',
30
+ site: '@yourhandle'
31
+ }
32
+ };
33
+ </script>
34
+
35
+ <TwitterCard data={seo} />
36
+ ```
37
+
38
+ Renders the following tags (based on your data):
39
+
40
+ ```html
41
+ <meta property="twitter:card" content="summary_large_image" />
42
+ <meta property="twitter:title" content="Your Page Title" />
43
+ <meta property="twitter:site" content="@yourhandle" />
44
+ <meta property="twitter:description" content="Page description for Twitter card." />
45
+ <meta property="twitter:image" content="https://example.com/og-image.jpg" />
46
+ <meta property="twitter:image:alt" content="An image for Twitter" />
47
+ ```
48
+
49
+ ### Example (Player Card)
50
+
51
+ ```svelte
52
+ <TwitterCard
53
+ data={{
54
+ title: 'Watch this video!',
55
+ description: 'Best video ever.',
56
+ image: {
57
+ url: 'https://example.com/video-thumbnail.jpg',
58
+ alt: 'Video thumbnail'
59
+ },
60
+ twitter: {
61
+ type: 'player',
62
+ site: '@myapp',
63
+ player: {
64
+ url: 'https://example.com/embed/video',
65
+ width: 1280,
66
+ height: 720
67
+ }
68
+ }
69
+ }}
70
+ />
71
+ ```
72
+
73
+ ## Props
74
+
75
+ | Prop | Type | Required | Description |
76
+ | :----- | :----------: | :------: | :----------------------------------------- |
77
+ | `data` | [SEOWebPage] | yes | The SEO object containing Twitter metadata |
78
+
79
+ <!-- Resource Links -->
80
+
81
+ [SEOWebPage]: https://github.com/indaco/sveo/blob/913f83920f7f76183fc7d6ea58eebbceeb82f452/src/lib/types.ts#L34-L43
@@ -4,34 +4,34 @@ let { data } = $props();
4
4
 
5
5
  {#if data.twitter}
6
6
  {#if data.twitter.type}
7
- <meta property="twitter:card" content={data.twitter.type} />
8
- <meta property="twitter:title" content={data.title} />
7
+ <meta name="twitter:card" content={data.twitter.type} />
8
+ <meta name="twitter:title" content={data.title} />
9
9
  {#if data.twitter.site}
10
- <meta property="twitter:site" content={data.twitter.site} />
10
+ <meta name="twitter:site" content={data.twitter.site} />
11
11
  {/if}
12
12
  {#if data.description}
13
- <meta property="twitter:description" content={data.description} />
13
+ <meta name="twitter:description" content={data.description} />
14
14
  {/if}
15
15
  {#if data.image?.url}
16
- <meta property="twitter:image" content={data.image.url} />
16
+ <meta name="twitter:image" content={data.image.url} />
17
17
  {#if data.image.alt}
18
- <meta property="twitter:image:alt" content={data.image.alt} />
18
+ <meta name="twitter:image:alt" content={data.image.alt} />
19
19
  {/if}
20
20
  {/if}
21
21
 
22
22
  {#if data.twitter.type === TwitterCardType.Player && data.twitter.player}
23
- <meta property="twitter:player" content={data.twitter.player.url} />
24
- <meta property="twitter:player:width" content={data.twitter.player.width.toString()} />
25
- <meta property="twitter:player:height" content={data.twitter.player.height.toString()} />
23
+ <meta name="twitter:player" content={data.twitter.player.url} />
24
+ <meta name="twitter:player:width" content={data.twitter.player.width.toString()} />
25
+ <meta name="twitter:player:height" content={data.twitter.player.height.toString()} />
26
26
  {/if}
27
27
 
28
28
  {#if data.twitter.type === TwitterCardType.App && data.twitter.app}
29
29
  {#if data.twitter.app.country}
30
- <meta property="twitter:app:country" content={data.twitter.app.country} />
30
+ <meta name="twitter:app:country" content={data.twitter.app.country} />
31
31
  {/if}
32
- <meta property="twitter:app:id:iphone" content={data.twitter.app.idIPhone} />
33
- <meta property="twitter:app:id:ipad" content={data.twitter.app.idIPad} />
34
- <meta property="twitter:app:id:googleplay" content={data.twitter.app.idGooglePlay} />
32
+ <meta name="twitter:app:id:iphone" content={data.twitter.app.idIPhone} />
33
+ <meta name="twitter:app:id:ipad" content={data.twitter.app.idIPad} />
34
+ <meta name="twitter:app:id:googleplay" content={data.twitter.app.idGooglePlay} />
35
35
  {/if}
36
36
  {/if}
37
37
  {/if}
@@ -1,28 +1,30 @@
1
1
  <script lang="ts">import { serializeJSONLdSchema, pathSegments } from '../../../utils.js';
2
2
  let { url } = $props();
3
- const baseURL = new URL(url).origin;
4
- const segments = pathSegments(url);
5
- const itemListElement = [
6
- {
7
- '@type': 'ListItem',
8
- position: 1,
9
- name: 'Home',
10
- url: baseURL
11
- },
12
- ...segments.map((segment, index) => {
13
- return {
3
+ const schemaOrgBreadcrumbList = $derived.by(() => {
4
+ const baseURL = new URL(url).origin;
5
+ const segments = pathSegments(url);
6
+ const itemListElement = [
7
+ {
14
8
  '@type': 'ListItem',
15
- position: index + 2,
16
- name: segment,
17
- url: `${baseURL}/${segments.slice(0, index + 1).join('/')}`
18
- };
19
- })
20
- ];
21
- const schemaOrgBreadcrumbList = {
22
- '@context': 'https://schema.org',
23
- '@type': 'BreadcrumbList',
24
- itemListElement
25
- };
9
+ position: 1,
10
+ name: 'Home',
11
+ url: baseURL
12
+ },
13
+ ...segments.map((segment, index) => {
14
+ return {
15
+ '@type': 'ListItem',
16
+ position: index + 2,
17
+ name: segment,
18
+ url: `${baseURL}/${segments.slice(0, index + 1).join('/')}`
19
+ };
20
+ })
21
+ ];
22
+ return {
23
+ '@context': 'https://schema.org',
24
+ '@type': 'BreadcrumbList',
25
+ itemListElement
26
+ };
27
+ });
26
28
  </script>
27
29
 
28
30
  <svelte:head>
@@ -0,0 +1,66 @@
1
+ # JsonLdBreadcrumbs
2
+
3
+ The `JsonLdBreadcrumbs` component adds a [BreadcrumbList] to the page header as [structured data]. This helps search engines understand the page's position within the site hierarchy and enhances rich results in search listings.
4
+
5
+ A `BreadcrumbList` is an [ItemList] consisting of a chain of linked web pages, typically ending with the current page.
6
+
7
+ ## Usage
8
+
9
+ ```html
10
+ <script>
11
+ import { JsonLdBreadcrumbs } from '@indaco/sveo/schemaorg';
12
+
13
+ <JsonLdBreadcrumbs url="https://example.com/blog/welcome" />
14
+ </script>
15
+ ```
16
+
17
+ ### Output
18
+
19
+ ```html
20
+ <script type="application/ld+json">{
21
+ "@context": "https://schema.org",
22
+ "@type": "BreadcrumbList",
23
+ "itemListElement": [
24
+ {
25
+ "@type": "ListItem",
26
+ "position": 1,
27
+ "name": "Home",
28
+ "url": "https://example.com"
29
+ },
30
+ {
31
+ "@type": "ListItem",
32
+ "position": 2,
33
+ "name": "blog",
34
+ "url": "https://example.com/blog"
35
+ },
36
+ {
37
+ "@type": "ListItem",
38
+ "position": 3,
39
+ "name": "welcome",
40
+ "url": "https://example.com/blog/welcome"
41
+ }
42
+ ]
43
+ }</script>
44
+
45
+ ```
46
+
47
+ ### With SvelteKit
48
+
49
+ ```html
50
+ <script>
51
+ import { page } from '$app/stores';
52
+ import { JsonLdBreadcrumbs } from '@indaco/sveo/schemaorg';
53
+
54
+ <JsonLdBreadcrumbs url={$page.url.href} />
55
+ </script>
56
+ ```
57
+
58
+ ## Properties
59
+
60
+ | Property | Type | Required | Description |
61
+ | :-------- | :------: | :------: | :------------------------------- |
62
+ | url | `string` | yes | Absolute URL of the current page |
63
+
64
+ [structured data]: https://developers.google.com/search/docs/appearance/structured-data/breadcrumb
65
+ [BreadcrumbList]: https://schema.org/BreadcrumbList
66
+ [ItemList]: https://schema.org/ItemList
@@ -1,20 +1,15 @@
1
1
  <script lang="ts">import { serializeJSONLdSchema } from '../../../utils.js';
2
2
  let { baseURL, data } = $props();
3
- function makeSiteNavigationElementList(data) {
4
- return Array.isArray(data)
5
- ? data.map((elem) => ({
6
- '@type': 'SiteNavigationElement',
7
- position: elem.weight,
8
- name: elem.name,
9
- url: elem.external ? elem.url : `${baseURL}${elem.url}`
10
- }))
11
- : [];
12
- }
13
- const schemaOrgSiteNavigationElement = {
3
+ const schemaOrgSiteNavigationElement = $derived.by(() => ({
14
4
  '@context': 'https://schema.org',
15
5
  '@type': 'ItemList',
16
- itemListElement: makeSiteNavigationElementList(data)
17
- };
6
+ itemListElement: data.map((elem) => ({
7
+ '@type': 'SiteNavigationElement',
8
+ position: elem.weight,
9
+ name: elem.name,
10
+ url: elem.external ? elem.url : `${baseURL}${elem.url}`
11
+ }))
12
+ }));
18
13
  </script>
19
14
 
20
15
  <svelte:head>
@@ -0,0 +1,64 @@
1
+ # JsonLdSiteNavigationElement
2
+
3
+ The `JsonLdSiteNavigationElement` component injects a [SiteNavigationElement] from Schema.org into the page as a `<script type="application/ld+json">` tag, describing the structure of the site's main navigation menu for search engines.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <script lang="ts">
9
+ import type { SEOMenuItem } from '@indaco/sveo/types.js';
10
+ import { JsonLdSiteNavigationElement } from '@indaco/sveo/schemaorg';
11
+
12
+ const menu: Array<SEOMenuItem> = [
13
+ {
14
+ identifier: 'home',
15
+ name: 'Home',
16
+ url: '/',
17
+ weight: 1
18
+ },
19
+ {
20
+ identifier: 'about',
21
+ name: 'About',
22
+ url: '/about',
23
+ weight: 2
24
+ }
25
+ ];
26
+
27
+ <JsonLdSiteNavigationElement baseURL="https://example.com" data={menu} />
28
+ </script>
29
+ ```
30
+
31
+ ### Output
32
+
33
+ ```html
34
+ <script type="application/ld+json">{
35
+ "@context": "https://schema.org",
36
+ "@type": "ItemList",
37
+ "itemListElement": [
38
+ {
39
+ "@type": "SiteNavigationElement",
40
+ "position": 1,
41
+ "name": "home",
42
+ "url": "https://example.com/"
43
+ },
44
+ {
45
+ "@type": "SiteNavigationElement",
46
+ "position": 2,
47
+ "name": "about",
48
+ "url": "https://example.com/about"
49
+ }
50
+ ]
51
+ }</script>
52
+ ```
53
+
54
+ ## Properties
55
+
56
+ | Property | Type | Required | Description |
57
+ | :------- | :---------------: | :------: | :------------------------------------------- |
58
+ | baseURL | `string` | yes | Base URL to prefix relative paths |
59
+ | data | [SEOMenuItem]`[]` | yes | Array of menu items to include in the schema |
60
+
61
+ <!-- Resource Links -->
62
+
63
+ [SEOMenuItem]: https://github.com/indaco/sveo/blob/06de4d7c79a27f0474981cce3ebc2cf922484b09/src/lib/types.ts#L20-L27
64
+ [SiteNavigationElement]: https://schema.org/SiteNavigationElement
@@ -1,15 +1,18 @@
1
1
  <script lang="ts">import { serializeJSONLdSchema } from '../../../utils.js';
2
2
  let { data } = $props();
3
- const schemaOrgWebPage = $state({
4
- '@context': 'https://schema.org',
5
- '@type': 'WebPage',
6
- name: data.title,
7
- description: data.description || ''
3
+ const schemaOrgWebPage = $derived.by(() => {
4
+ const page = {
5
+ '@context': 'https://schema.org',
6
+ '@type': 'WebPage',
7
+ name: data.title,
8
+ description: data.description || ''
9
+ };
10
+ if (data.author)
11
+ page.author = data.author;
12
+ if (data.keywords?.length)
13
+ page.keywords = data.keywords;
14
+ return page;
8
15
  });
9
- if (data.author)
10
- schemaOrgWebPage.author = data.author;
11
- if (data.keywords?.length)
12
- schemaOrgWebPage.keywords = data.keywords;
13
16
  </script>
14
17
 
15
18
  <svelte:head>
@@ -0,0 +1,51 @@
1
+ # JsonLdWebPage
2
+
3
+ The `JsonLdWebPage` component adds a [WebPage] Schema.org type to the page as structured data via a `<script type="application/ld+json">` tag. This helps search engines understand the page's content and metadata such as title, description, and keywords.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <script lang="ts">
9
+ import type { SEOWebPage } from '@indaco/sveo/types.js';
10
+ import { JsonLdWebPage } from '@indaco/sveo/schemaorg';
11
+
12
+ const pageData: SEOWebPage = {
13
+ url: website.baseURL,
14
+ title: 'Home Page',
15
+ description: 'This is the description for the Home Page',
16
+ keywords: 'sveltekit, components, tests, vitest',
17
+ opengraph: {
18
+ type: OpenGraphType.Website
19
+ },
20
+ twitter: {
21
+ type: TwitterCardType.Summary
22
+ }
23
+ }
24
+
25
+ <JsonLdWebPage data={pageData} />
26
+ </script>
27
+ ```
28
+
29
+ Output
30
+
31
+ ```html
32
+ <script type="application/ld+json">{
33
+ "@context": "https://schema.org",
34
+ "@type": "WebPage",
35
+ "name": "Home Page",
36
+ "description": "This is the description for the Home Page",
37
+ "keywords": "sveltekit, components, tests, jest"
38
+ }
39
+ </script>
40
+ ```
41
+
42
+ ## Properties
43
+
44
+ | Property | Type | Required | Description |
45
+ | :------- | :----------: | :------: | :-------------------------------------- |
46
+ | data | [SEOWebPage] | yes | Web page metadata to convert to JSON-LD |
47
+
48
+ <!-- Resource Links -->
49
+
50
+ [SEOWebPage]: https://github.com/indaco/sveo/blob/913f83920f7f76183fc7d6ea58eebbceeb82f452/src/lib/types.ts#L34-L43
51
+ [WebPage]: https://schema.org/WebPage