@dineway-ai/plugin-seo-graph 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +227 -0
- package/package.json +49 -0
- package/src/admin-redirects.tsx +317 -0
- package/src/admin.tsx +529 -0
- package/src/canonical.ts +46 -0
- package/src/descriptions.ts +17 -0
- package/src/fuzzy.ts +112 -0
- package/src/hreflang.ts +103 -0
- package/src/index.ts +98 -0
- package/src/indexnow.ts +139 -0
- package/src/llms.ts +151 -0
- package/src/metadata.ts +93 -0
- package/src/opengraph.ts +327 -0
- package/src/robots.ts +29 -0
- package/src/schema/article.ts +70 -0
- package/src/schema/breadcrumb.ts +158 -0
- package/src/schema/endpoints.ts +69 -0
- package/src/schema/index.ts +175 -0
- package/src/schema/organization.ts +133 -0
- package/src/schema/person.ts +54 -0
- package/src/schema/webpage.ts +84 -0
- package/src/schema/website.ts +52 -0
- package/src/settings.ts +330 -0
- package/src/terms.ts +33 -0
- package/src/titles.ts +59 -0
- package/src/urls.ts +72 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assembleGraph,
|
|
3
|
+
buildBreadcrumbList,
|
|
4
|
+
buildImageObject,
|
|
5
|
+
buildPiece,
|
|
6
|
+
buildSiteNavigationElement,
|
|
7
|
+
makeIds,
|
|
8
|
+
type GraphEntity,
|
|
9
|
+
} from "@jdevalk/seo-graph-core";
|
|
10
|
+
import type { PublicPageContext } from "dineway";
|
|
11
|
+
import type { Blog } from "schema-dts";
|
|
12
|
+
|
|
13
|
+
import type { SeoSettings } from "../settings.js";
|
|
14
|
+
import { buildArticle } from "./article.js";
|
|
15
|
+
import { buildBreadcrumbs } from "./breadcrumb.js";
|
|
16
|
+
import { buildSiteEntity, getSiteEntityId } from "./organization.js";
|
|
17
|
+
import { buildAuthorPerson } from "./person.js";
|
|
18
|
+
import { buildWebPage } from "./webpage.js";
|
|
19
|
+
import { buildWebSite } from "./website.js";
|
|
20
|
+
|
|
21
|
+
const TRAILING_SLASH_RE = /\/$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the complete JSON-LD schema graph for a page.
|
|
25
|
+
* Outputs a @graph array with distinct, linked nodes.
|
|
26
|
+
*
|
|
27
|
+
* Individual piece builders delegate to `@jdevalk/seo-graph-core`'s
|
|
28
|
+
* typed builders (`buildWebSite`, `buildWebPage`, `buildArticle`,
|
|
29
|
+
* `buildPiece<Organization>`, `buildPiece<Person>`) with thin
|
|
30
|
+
* Dineway-specific wrappers that map Dineway's page/settings model
|
|
31
|
+
* onto core's input types. This keeps both Dineway and joost.blog on
|
|
32
|
+
* the same code path for graph construction, not just the ID scheme.
|
|
33
|
+
*/
|
|
34
|
+
export function buildSchemaGraph(
|
|
35
|
+
page: PublicPageContext,
|
|
36
|
+
settings: SeoSettings,
|
|
37
|
+
siteUrl: string,
|
|
38
|
+
siteName: string,
|
|
39
|
+
canonical: string | null,
|
|
40
|
+
ogTitle: string,
|
|
41
|
+
description: string | null,
|
|
42
|
+
locale: string,
|
|
43
|
+
keywords?: string[],
|
|
44
|
+
articleSection?: string,
|
|
45
|
+
): Record<string, unknown> | null {
|
|
46
|
+
// No schema for 404 pages
|
|
47
|
+
if (page.path === "/404") return null;
|
|
48
|
+
|
|
49
|
+
const ids = makeIds({
|
|
50
|
+
siteUrl,
|
|
51
|
+
personUrl: settings.personUrl || undefined,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Compute breadcrumbs early so the WebPage node can carry a
|
|
55
|
+
// back-reference (`breadcrumb: { "@id": ... }`) when present.
|
|
56
|
+
const crumbs = buildBreadcrumbs(page, settings, siteUrl);
|
|
57
|
+
const hasBreadcrumbs = crumbs !== null && crumbs.length > 1;
|
|
58
|
+
const pageUrl = canonical || page.url;
|
|
59
|
+
const siteEntityId = getSiteEntityId(settings, ids);
|
|
60
|
+
|
|
61
|
+
// Derive Blog @id when blog settings are configured.
|
|
62
|
+
const blogId = settings.blogUrl
|
|
63
|
+
? `${settings.blogUrl.replace(TRAILING_SLASH_RE, "")}/#blog`
|
|
64
|
+
: null;
|
|
65
|
+
|
|
66
|
+
const pieces: GraphEntity[] = [];
|
|
67
|
+
|
|
68
|
+
// 1. Site entity (Organization or Person) - always present
|
|
69
|
+
pieces.push(buildSiteEntity(settings, siteUrl, siteName, locale, ids) as GraphEntity);
|
|
70
|
+
|
|
71
|
+
// 2. WebSite - always present
|
|
72
|
+
const hasNavigation = settings.navigationItems.length > 0;
|
|
73
|
+
pieces.push(
|
|
74
|
+
buildWebSite(
|
|
75
|
+
settings,
|
|
76
|
+
siteUrl,
|
|
77
|
+
siteName,
|
|
78
|
+
settings.defaultDescription || null,
|
|
79
|
+
locale,
|
|
80
|
+
ids,
|
|
81
|
+
hasNavigation,
|
|
82
|
+
) as GraphEntity,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// 3. SiteNavigationElement - when navigation items are configured
|
|
86
|
+
if (hasNavigation) {
|
|
87
|
+
pieces.push(
|
|
88
|
+
buildSiteNavigationElement(
|
|
89
|
+
{
|
|
90
|
+
name: "Main navigation",
|
|
91
|
+
isPartOf: { "@id": ids.website },
|
|
92
|
+
items: settings.navigationItems,
|
|
93
|
+
},
|
|
94
|
+
ids,
|
|
95
|
+
) as GraphEntity,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Blog entity - when blog URL is configured
|
|
100
|
+
if (blogId && settings.blogUrl) {
|
|
101
|
+
pieces.push(
|
|
102
|
+
buildPiece<Blog>({
|
|
103
|
+
"@type": "Blog",
|
|
104
|
+
"@id": blogId,
|
|
105
|
+
name: settings.blogName || "Blog",
|
|
106
|
+
url: settings.blogUrl,
|
|
107
|
+
publisher: { "@id": siteEntityId },
|
|
108
|
+
inLanguage: locale,
|
|
109
|
+
}) as GraphEntity,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 5. WebPage - always present
|
|
114
|
+
pieces.push(
|
|
115
|
+
buildWebPage(
|
|
116
|
+
page,
|
|
117
|
+
settings,
|
|
118
|
+
canonical,
|
|
119
|
+
ogTitle,
|
|
120
|
+
description,
|
|
121
|
+
locale,
|
|
122
|
+
ids,
|
|
123
|
+
hasBreadcrumbs,
|
|
124
|
+
siteEntityId,
|
|
125
|
+
) as GraphEntity,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// 6. BreadcrumbList - when derived/ruled with >1 crumb
|
|
129
|
+
if (hasBreadcrumbs) {
|
|
130
|
+
pieces.push(buildBreadcrumbList({ url: pageUrl, items: crumbs }, ids) as GraphEntity);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 7. Primary ImageObject - when the page has an image
|
|
134
|
+
if (page.image) {
|
|
135
|
+
pieces.push(
|
|
136
|
+
buildImageObject(
|
|
137
|
+
{
|
|
138
|
+
pageUrl,
|
|
139
|
+
url: page.image,
|
|
140
|
+
width: 1200,
|
|
141
|
+
height: 675,
|
|
142
|
+
inLanguage: locale,
|
|
143
|
+
},
|
|
144
|
+
ids,
|
|
145
|
+
) as GraphEntity,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 8. Article + Author Person - for content pages with article meta
|
|
150
|
+
if (page.kind === "content" && page.articleMeta?.publishedTime) {
|
|
151
|
+
const article = buildArticle(
|
|
152
|
+
page,
|
|
153
|
+
settings,
|
|
154
|
+
siteName,
|
|
155
|
+
canonical,
|
|
156
|
+
ogTitle,
|
|
157
|
+
description,
|
|
158
|
+
locale,
|
|
159
|
+
ids,
|
|
160
|
+
blogId,
|
|
161
|
+
keywords,
|
|
162
|
+
articleSection,
|
|
163
|
+
);
|
|
164
|
+
if (article) pieces.push(article as GraphEntity);
|
|
165
|
+
|
|
166
|
+
// For Organization sites the author Person is a distinct entity
|
|
167
|
+
// from the site entity, so emit it separately. For Person sites
|
|
168
|
+
// the site entity already is this Person — assembleGraph's
|
|
169
|
+
// first-wins dedupe keeps the richer site-entity version.
|
|
170
|
+
const author = buildAuthorPerson(settings, siteName, ids);
|
|
171
|
+
if (author) pieces.push(author as GraphEntity);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return assembleGraph(pieces, { warnOnDanglingReferences: true });
|
|
175
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { buildPiece } from "@jdevalk/seo-graph-core";
|
|
2
|
+
import type { IdFactory } from "@jdevalk/seo-graph-core";
|
|
3
|
+
import type { Organization, Person } from "schema-dts";
|
|
4
|
+
|
|
5
|
+
import type { SeoSettings } from "../settings.js";
|
|
6
|
+
|
|
7
|
+
const TRAILING_SLASH_RE = /\/$/;
|
|
8
|
+
const NON_ALPHANUMERIC_RE = /[^a-z0-9]+/g;
|
|
9
|
+
const EDGE_DASH_RE = /(^-|-$)/g;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build the Organization or Person node representing the site entity.
|
|
13
|
+
* Every page includes an Organization or Person node.
|
|
14
|
+
*/
|
|
15
|
+
export function buildSiteEntity(
|
|
16
|
+
settings: SeoSettings,
|
|
17
|
+
siteUrl: string,
|
|
18
|
+
siteName: string,
|
|
19
|
+
locale: string,
|
|
20
|
+
ids: IdFactory,
|
|
21
|
+
): Record<string, unknown> {
|
|
22
|
+
const baseUrl = siteUrl.replace(TRAILING_SLASH_RE, "");
|
|
23
|
+
|
|
24
|
+
if (settings.siteRepresents === "organization") {
|
|
25
|
+
return buildOrganizationEntity(settings, baseUrl, siteName, locale, ids);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return buildPersonEntity(settings, baseUrl, siteName, locale, ids);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildOrganizationEntity(
|
|
32
|
+
settings: SeoSettings,
|
|
33
|
+
baseUrl: string,
|
|
34
|
+
siteName: string,
|
|
35
|
+
locale: string,
|
|
36
|
+
ids: IdFactory,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
const orgLogoId = `${baseUrl}/#/schema.org/ImageObject/logo`;
|
|
39
|
+
|
|
40
|
+
const piece = buildPiece<Organization>({
|
|
41
|
+
"@type": "Organization",
|
|
42
|
+
"@id": ids.organization(orgSlug(settings)),
|
|
43
|
+
name: settings.orgName || siteName,
|
|
44
|
+
url: baseUrl,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (settings.orgLogoUrl) {
|
|
48
|
+
piece.logo = {
|
|
49
|
+
"@type": "ImageObject",
|
|
50
|
+
"@id": orgLogoId,
|
|
51
|
+
url: settings.orgLogoUrl,
|
|
52
|
+
contentUrl: settings.orgLogoUrl,
|
|
53
|
+
inLanguage: locale,
|
|
54
|
+
};
|
|
55
|
+
piece.image = { "@id": orgLogoId };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (settings.socials.length > 0) {
|
|
59
|
+
piece.sameAs = settings.socials;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (settings.publishingPrinciples) {
|
|
63
|
+
piece.publishingPrinciples = settings.publishingPrinciples;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return piece;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildPersonEntity(
|
|
70
|
+
settings: SeoSettings,
|
|
71
|
+
baseUrl: string,
|
|
72
|
+
siteName: string,
|
|
73
|
+
locale: string,
|
|
74
|
+
ids: IdFactory,
|
|
75
|
+
): Record<string, unknown> {
|
|
76
|
+
const name = settings.personName || siteName;
|
|
77
|
+
|
|
78
|
+
const piece = buildPiece<Person>({
|
|
79
|
+
"@type": "Person",
|
|
80
|
+
"@id": ids.person,
|
|
81
|
+
name,
|
|
82
|
+
url: baseUrl,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (settings.personDescription) {
|
|
86
|
+
piece.description = settings.personDescription.slice(0, 250);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (settings.personJobTitle) {
|
|
90
|
+
piece.jobTitle = settings.personJobTitle;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (settings.personImageUrl) {
|
|
94
|
+
piece.image = {
|
|
95
|
+
"@type": "ImageObject",
|
|
96
|
+
"@id": ids.personImage,
|
|
97
|
+
url: settings.personImageUrl,
|
|
98
|
+
contentUrl: settings.personImageUrl,
|
|
99
|
+
inLanguage: locale,
|
|
100
|
+
caption: name,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (settings.socials.length > 0) {
|
|
105
|
+
piece.sameAs = settings.socials;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (settings.publishingPrinciples) {
|
|
109
|
+
piece.publishingPrinciples = settings.publishingPrinciples;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return piece;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get the @id reference for the site entity — either the site-wide
|
|
117
|
+
* Person or an Organization. Used by other pieces (Article, WebSite)
|
|
118
|
+
* that need to reference the publisher.
|
|
119
|
+
*/
|
|
120
|
+
export function getSiteEntityId(settings: SeoSettings, ids: IdFactory): string {
|
|
121
|
+
if (settings.siteRepresents === "organization") {
|
|
122
|
+
return ids.organization(orgSlug(settings));
|
|
123
|
+
}
|
|
124
|
+
return ids.person;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Slugify the organization name for use as the Organization @id slug.
|
|
129
|
+
*/
|
|
130
|
+
function orgSlug(settings: SeoSettings): string {
|
|
131
|
+
const name = settings.orgName || "site";
|
|
132
|
+
return name.toLowerCase().replace(NON_ALPHANUMERIC_RE, "-").replace(EDGE_DASH_RE, "") || "site";
|
|
133
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildPiece } from "@jdevalk/seo-graph-core";
|
|
2
|
+
import type { IdFactory } from "@jdevalk/seo-graph-core";
|
|
3
|
+
import type { Person } from "schema-dts";
|
|
4
|
+
|
|
5
|
+
import type { SeoSettings } from "../settings.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build the author Person schema node. Returned as a separate piece
|
|
9
|
+
* only when the site represents an Organization — for Person sites
|
|
10
|
+
* the site entity already covers this node.
|
|
11
|
+
*
|
|
12
|
+
* Required fields are @type, @id, name. Names like "admin" are
|
|
13
|
+
* rejected per schema.org best practice.
|
|
14
|
+
*/
|
|
15
|
+
export function buildAuthorPerson(
|
|
16
|
+
settings: SeoSettings,
|
|
17
|
+
siteName: string,
|
|
18
|
+
ids: IdFactory,
|
|
19
|
+
): Record<string, unknown> | null {
|
|
20
|
+
const name = settings.personName || siteName;
|
|
21
|
+
|
|
22
|
+
// Per spec: reject "admin" or similar invalid author names
|
|
23
|
+
if (!name || name.toLowerCase() === "admin") return null;
|
|
24
|
+
|
|
25
|
+
const piece = buildPiece<Person>({
|
|
26
|
+
"@type": "Person",
|
|
27
|
+
"@id": ids.person,
|
|
28
|
+
name,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (settings.personDescription) {
|
|
32
|
+
piece.description = settings.personDescription.slice(0, 250);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (settings.personUrl) {
|
|
36
|
+
piece.url = settings.personUrl;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (settings.personImageUrl) {
|
|
40
|
+
piece.image = {
|
|
41
|
+
"@type": "ImageObject",
|
|
42
|
+
"@id": ids.personImage,
|
|
43
|
+
url: settings.personImageUrl,
|
|
44
|
+
contentUrl: settings.personImageUrl,
|
|
45
|
+
caption: name,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (settings.socials.length > 0) {
|
|
50
|
+
piece.sameAs = settings.socials;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return piece;
|
|
54
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { buildWebPage as coreBuildWebPage, type WebPageType } from "@jdevalk/seo-graph-core";
|
|
2
|
+
import type { IdFactory, Reference } from "@jdevalk/seo-graph-core";
|
|
3
|
+
import type { PublicPageContext } from "dineway";
|
|
4
|
+
|
|
5
|
+
import type { SeoSettings } from "../settings.js";
|
|
6
|
+
|
|
7
|
+
const COLLECTION_PAGE_PATHS = new Set(["/", "/posts", "/posts/", "/videos", "/videos/"]);
|
|
8
|
+
|
|
9
|
+
const PROFILE_PAGE_PATHS = new Set(["/about", "/about/"]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Determine the WebPage @type based on the page path.
|
|
13
|
+
*/
|
|
14
|
+
function getWebPageType(page: PublicPageContext): WebPageType {
|
|
15
|
+
const path = page.path || "/";
|
|
16
|
+
|
|
17
|
+
if (PROFILE_PAGE_PATHS.has(path)) return "ProfilePage";
|
|
18
|
+
if (COLLECTION_PAGE_PATHS.has(path)) return "CollectionPage";
|
|
19
|
+
if (path.startsWith("/categories/")) return "CollectionPage";
|
|
20
|
+
if (path.startsWith("/tags/")) return "CollectionPage";
|
|
21
|
+
|
|
22
|
+
return "WebPage";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the WebPage schema node.
|
|
27
|
+
* Every page includes a WebPage node.
|
|
28
|
+
*
|
|
29
|
+
* When `hasBreadcrumbs` is true, a `breadcrumb` back-reference is
|
|
30
|
+
* added pointing at the BreadcrumbList entity. Callers are
|
|
31
|
+
* responsible for emitting the matching BreadcrumbList piece.
|
|
32
|
+
*/
|
|
33
|
+
export function buildWebPage(
|
|
34
|
+
page: PublicPageContext,
|
|
35
|
+
settings: SeoSettings,
|
|
36
|
+
canonical: string | null,
|
|
37
|
+
ogTitle: string,
|
|
38
|
+
description: string | null,
|
|
39
|
+
locale: string,
|
|
40
|
+
ids: IdFactory,
|
|
41
|
+
hasBreadcrumbs: boolean,
|
|
42
|
+
siteEntityId: string,
|
|
43
|
+
): Record<string, unknown> {
|
|
44
|
+
const pageUrl = canonical || page.url;
|
|
45
|
+
const type = getWebPageType(page);
|
|
46
|
+
|
|
47
|
+
// Homepage and ProfilePage carry an `about` reference to the site entity.
|
|
48
|
+
const isAboutPage = type === "ProfilePage" || (page.path || "/") === "/";
|
|
49
|
+
const about: Reference | undefined = isAboutPage ? { "@id": siteEntityId } : undefined;
|
|
50
|
+
|
|
51
|
+
// Primary image reference when the page has an image.
|
|
52
|
+
const primaryImage: Reference | undefined = page.image
|
|
53
|
+
? { "@id": ids.primaryImage(pageUrl) }
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
// Copyright fields from settings.
|
|
57
|
+
const copyrightHolder: Reference | undefined = settings.copyrightYear
|
|
58
|
+
? { "@id": siteEntityId }
|
|
59
|
+
: undefined;
|
|
60
|
+
|
|
61
|
+
return coreBuildWebPage(
|
|
62
|
+
{
|
|
63
|
+
url: pageUrl,
|
|
64
|
+
name: ogTitle,
|
|
65
|
+
isPartOf: { "@id": ids.website },
|
|
66
|
+
inLanguage: locale,
|
|
67
|
+
description: description || undefined,
|
|
68
|
+
breadcrumb: hasBreadcrumbs ? { "@id": ids.breadcrumb(pageUrl) } : undefined,
|
|
69
|
+
datePublished: page.articleMeta?.publishedTime
|
|
70
|
+
? new Date(page.articleMeta.publishedTime)
|
|
71
|
+
: undefined,
|
|
72
|
+
dateModified: page.articleMeta?.modifiedTime
|
|
73
|
+
? new Date(page.articleMeta.modifiedTime)
|
|
74
|
+
: undefined,
|
|
75
|
+
about,
|
|
76
|
+
primaryImage,
|
|
77
|
+
copyrightHolder,
|
|
78
|
+
copyrightYear: settings.copyrightYear || undefined,
|
|
79
|
+
license: settings.licenseUrl || undefined,
|
|
80
|
+
},
|
|
81
|
+
ids,
|
|
82
|
+
type,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { buildWebSite as coreBuildWebSite } from "@jdevalk/seo-graph-core";
|
|
2
|
+
import type { IdFactory } from "@jdevalk/seo-graph-core";
|
|
3
|
+
|
|
4
|
+
import type { SeoSettings } from "../settings.js";
|
|
5
|
+
import { getSiteEntityId } from "./organization.js";
|
|
6
|
+
|
|
7
|
+
const TRAILING_SLASH_RE = /\/$/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the WebSite schema node.
|
|
11
|
+
* Every page includes a WebSite node with SearchAction.
|
|
12
|
+
*/
|
|
13
|
+
export function buildWebSite(
|
|
14
|
+
settings: SeoSettings,
|
|
15
|
+
siteUrl: string,
|
|
16
|
+
siteName: string,
|
|
17
|
+
siteDescription: string | null,
|
|
18
|
+
locale: string,
|
|
19
|
+
ids: IdFactory,
|
|
20
|
+
hasNavigation: boolean,
|
|
21
|
+
): Record<string, unknown> {
|
|
22
|
+
const baseUrl = siteUrl.replace(TRAILING_SLASH_RE, "");
|
|
23
|
+
|
|
24
|
+
const piece = coreBuildWebSite(
|
|
25
|
+
{
|
|
26
|
+
url: `${baseUrl}/`,
|
|
27
|
+
name: siteName,
|
|
28
|
+
publisher: { "@id": getSiteEntityId(settings, ids) },
|
|
29
|
+
inLanguage: locale,
|
|
30
|
+
description: siteDescription || undefined,
|
|
31
|
+
hasPart: hasNavigation ? { "@id": ids.navigation } : undefined,
|
|
32
|
+
},
|
|
33
|
+
ids,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// SearchAction with Google's query-input extension — not in schema-dts
|
|
37
|
+
// types, so set directly on the result.
|
|
38
|
+
piece.potentialAction = {
|
|
39
|
+
"@type": "SearchAction",
|
|
40
|
+
target: {
|
|
41
|
+
"@type": "EntryPoint",
|
|
42
|
+
urlTemplate: `${baseUrl}/search?q={search_term_string}`,
|
|
43
|
+
},
|
|
44
|
+
"query-input": {
|
|
45
|
+
"@type": "PropertyValueSpecification",
|
|
46
|
+
valueRequired: "http://schema.org/True",
|
|
47
|
+
valueName: "search_term_string",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return piece;
|
|
52
|
+
}
|