@rathnasgala2/template 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -59
- package/contracts/render-policy.jcs +1 -1
- package/contracts/theme-styling-contract.jcs +1 -1
- package/package.json +1 -1
- package/src/core/index.js +180 -50
- package/src/core/internal/appearance/base-layer.js +711 -137
- package/src/core/internal/appearance/bootstrap-script.js +570 -83
- package/src/core/internal/appearance/contract.js +6 -5
- package/src/core/internal/appearance/controller-markup.js +46 -55
- package/src/core/internal/appearance/minify-script.js +69 -0
- package/src/core/internal/appearance/styling-contract.js +199 -17
- package/src/core/internal/chrome.js +199 -0
- package/src/core/internal/components.js +379 -0
- package/src/core/internal/eleventy-render.js +13 -4
- package/src/core/internal/feeds.js +74 -6
- package/src/core/internal/icons.js +52 -0
- package/src/core/internal/llms.js +252 -0
- package/src/core/internal/media/pipeline.js +32 -17
- package/src/core/internal/messages.js +104 -0
- package/src/core/internal/page-kinds.js +918 -290
- package/src/core/internal/render-policy-content.js +2 -2
- package/src/core/internal/robots.js +78 -0
- package/src/core/internal/route-labels.js +31 -0
- package/src/core/internal/seo.js +53 -0
- package/src/core/internal/skeleton.js +54 -129
- package/src/core/internal/structured-data.js +247 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema.org structured data (JSON-LD) for every generated page. One
|
|
3
|
+
* `<script type="application/ld+json">` carrying one `@graph` per page:
|
|
4
|
+
*
|
|
5
|
+
* - home: `WebSite` + `Blog` (no `SearchAction`: the site search is a
|
|
6
|
+
* client-side dialog over a static index, there is no search-results URL
|
|
7
|
+
* a crawler could call, and an invented one would be a false claim);
|
|
8
|
+
* - article: `BlogPosting` + `BreadcrumbList`;
|
|
9
|
+
* - tag / series / archive / paginated index: `CollectionPage` +
|
|
10
|
+
* `BreadcrumbList`;
|
|
11
|
+
* - author: `ProfilePage` whose `mainEntity` is the `Person`;
|
|
12
|
+
* - authored page / publication profile: `WebPage` (+ `BreadcrumbList`).
|
|
13
|
+
*
|
|
14
|
+
* The block is data, never executed: a browser does not run a script whose
|
|
15
|
+
* `type` is not a JavaScript type, and the page CSP (`script-src 'self'`)
|
|
16
|
+
* does not govern it. The "exactly one executable script per page" rule
|
|
17
|
+
* therefore stays true: the one executable script is the `<script src>`; the
|
|
18
|
+
* data block is distinguished by its `type`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @type {number} Google's recommended maximum `headline` length. */
|
|
22
|
+
const HEADLINE_MAX = 110;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Serialize a JSON-LD document for safe embedding inside a `<script>`
|
|
26
|
+
* element: `<`, `>`, `&`, U+2028 and U+2029 become `\uXXXX` escapes, so the
|
|
27
|
+
* text can never close the element or start a comment, and stays valid
|
|
28
|
+
* JSON.
|
|
29
|
+
*
|
|
30
|
+
* @param {unknown} document the JSON-LD value
|
|
31
|
+
* @returns {string} compact, embed-safe JSON
|
|
32
|
+
*/
|
|
33
|
+
export function serializeJsonLd(document) {
|
|
34
|
+
return JSON.stringify(document).replace(
|
|
35
|
+
/[<>&\u2028\u2029]/g,
|
|
36
|
+
(character) =>
|
|
37
|
+
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {{url: string, width?: number, height?: number}} ImageInfo
|
|
43
|
+
* @typedef {{label: string, route?: string}} CrumbStep
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {ImageInfo | undefined} image
|
|
48
|
+
* @returns {Record<string, unknown> | undefined}
|
|
49
|
+
*/
|
|
50
|
+
function imageObject(image) {
|
|
51
|
+
return image
|
|
52
|
+
? {
|
|
53
|
+
'@type': 'ImageObject',
|
|
54
|
+
url: image.url,
|
|
55
|
+
width: image.width,
|
|
56
|
+
height: image.height,
|
|
57
|
+
}
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} text
|
|
63
|
+
* @param {number} max
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function bound(text, max) {
|
|
67
|
+
const characters = Array.from(text);
|
|
68
|
+
return characters.length > max
|
|
69
|
+
? `${characters
|
|
70
|
+
.slice(0, max - 1)
|
|
71
|
+
.join('')
|
|
72
|
+
.trimEnd()}…`
|
|
73
|
+
: text;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Bind the JSON-LD builders to one build.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} context
|
|
80
|
+
* @param {{title: string, description: string, defaultLanguage: string}} context.publication
|
|
81
|
+
* @param {(route: string) => string} context.absolute the absolute URL of a
|
|
82
|
+
* `basePath`-joined route
|
|
83
|
+
* @param {ImageInfo | undefined} context.brandMark the appearance brand mark
|
|
84
|
+
* @returns {{
|
|
85
|
+
* serialize: (graph: readonly unknown[]) => string,
|
|
86
|
+
* home: (input: {posts: readonly {url: string, headline: string, datePublished: string}[]}) => unknown[],
|
|
87
|
+
* article: (input: ArticleInput) => unknown[],
|
|
88
|
+
* collection: (input: {name: string, description?: string, url: string, language: string, trail: readonly CrumbStep[], image?: ImageInfo}) => unknown[],
|
|
89
|
+
* profile: (input: {name: string, description?: string, url: string, language: string, image?: ImageInfo, sameAs: readonly string[]}) => unknown[],
|
|
90
|
+
* webPage: (input: {name: string, description?: string, url: string, language: string, trail: readonly CrumbStep[], dateModified?: string}) => unknown[],
|
|
91
|
+
* }}
|
|
92
|
+
*/
|
|
93
|
+
export function createStructuredData({ publication, absolute, brandMark }) {
|
|
94
|
+
const origin = absolute('/');
|
|
95
|
+
const organization = {
|
|
96
|
+
'@type': 'Organization',
|
|
97
|
+
'@id': `${origin}#organization`,
|
|
98
|
+
name: publication.title,
|
|
99
|
+
url: origin,
|
|
100
|
+
logo: imageObject(brandMark),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {readonly CrumbStep[]} trail
|
|
105
|
+
* @returns {Record<string, unknown>}
|
|
106
|
+
*/
|
|
107
|
+
const breadcrumbs = (trail) => ({
|
|
108
|
+
'@type': 'BreadcrumbList',
|
|
109
|
+
itemListElement: trail.map((step, index) => ({
|
|
110
|
+
'@type': 'ListItem',
|
|
111
|
+
position: index + 1,
|
|
112
|
+
name: step.label,
|
|
113
|
+
item: step.route ? absolute(step.route) : undefined,
|
|
114
|
+
})),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
serialize: (graph) =>
|
|
119
|
+
serializeJsonLd({ '@context': 'https://schema.org', '@graph': graph }),
|
|
120
|
+
|
|
121
|
+
home: ({ posts }) => [
|
|
122
|
+
{
|
|
123
|
+
'@type': 'WebSite',
|
|
124
|
+
'@id': `${origin}#website`,
|
|
125
|
+
url: origin,
|
|
126
|
+
name: publication.title,
|
|
127
|
+
description: publication.description,
|
|
128
|
+
inLanguage: publication.defaultLanguage,
|
|
129
|
+
publisher: { '@id': organization['@id'] },
|
|
130
|
+
// No SearchAction: this site has no search-results URL (see module
|
|
131
|
+
// documentation).
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
'@type': 'Blog',
|
|
135
|
+
'@id': `${origin}#blog`,
|
|
136
|
+
url: origin,
|
|
137
|
+
name: publication.title,
|
|
138
|
+
description: publication.description,
|
|
139
|
+
inLanguage: publication.defaultLanguage,
|
|
140
|
+
publisher: { '@id': organization['@id'] },
|
|
141
|
+
blogPost: posts.map((post) => ({
|
|
142
|
+
'@type': 'BlogPosting',
|
|
143
|
+
headline: bound(post.headline, HEADLINE_MAX),
|
|
144
|
+
url: post.url,
|
|
145
|
+
datePublished: post.datePublished,
|
|
146
|
+
})),
|
|
147
|
+
},
|
|
148
|
+
organization,
|
|
149
|
+
],
|
|
150
|
+
|
|
151
|
+
article: (input) => {
|
|
152
|
+
const authors = input.authors.map((author) => ({
|
|
153
|
+
'@type': 'Person',
|
|
154
|
+
name: author.name,
|
|
155
|
+
url: author.url,
|
|
156
|
+
sameAs: author.sameAs.length > 0 ? [...author.sameAs] : undefined,
|
|
157
|
+
}));
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
'@type': 'BlogPosting',
|
|
161
|
+
'@id': `${input.url}#article`,
|
|
162
|
+
mainEntityOfPage: { '@type': 'WebPage', '@id': input.url },
|
|
163
|
+
url: input.url,
|
|
164
|
+
headline: bound(input.headline, HEADLINE_MAX),
|
|
165
|
+
description: input.description,
|
|
166
|
+
image: imageObject(input.image),
|
|
167
|
+
datePublished: input.datePublished,
|
|
168
|
+
dateModified: input.dateModified,
|
|
169
|
+
inLanguage: input.language,
|
|
170
|
+
author: authors.length === 1 ? authors[0] : authors,
|
|
171
|
+
publisher: organization,
|
|
172
|
+
keywords: input.tags.length > 0 ? input.tags.join(', ') : undefined,
|
|
173
|
+
isPartOf: input.series
|
|
174
|
+
? {
|
|
175
|
+
'@type': 'CreativeWorkSeries',
|
|
176
|
+
name: input.series.name,
|
|
177
|
+
url: input.series.url,
|
|
178
|
+
}
|
|
179
|
+
: undefined,
|
|
180
|
+
},
|
|
181
|
+
breadcrumbs(input.trail),
|
|
182
|
+
];
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
collection: ({ name, description, url, language, trail, image }) => [
|
|
186
|
+
{
|
|
187
|
+
'@type': 'CollectionPage',
|
|
188
|
+
'@id': `${url}#page`,
|
|
189
|
+
url,
|
|
190
|
+
name,
|
|
191
|
+
description,
|
|
192
|
+
inLanguage: language,
|
|
193
|
+
primaryImageOfPage: imageObject(image),
|
|
194
|
+
isPartOf: { '@id': `${origin}#website` },
|
|
195
|
+
},
|
|
196
|
+
breadcrumbs(trail),
|
|
197
|
+
],
|
|
198
|
+
|
|
199
|
+
profile: ({ name, description, url, language, image, sameAs }) => [
|
|
200
|
+
{
|
|
201
|
+
'@type': 'ProfilePage',
|
|
202
|
+
'@id': `${url}#page`,
|
|
203
|
+
url,
|
|
204
|
+
name,
|
|
205
|
+
inLanguage: language,
|
|
206
|
+
mainEntity: {
|
|
207
|
+
'@type': 'Person',
|
|
208
|
+
'@id': `${url}#person`,
|
|
209
|
+
name,
|
|
210
|
+
url,
|
|
211
|
+
description,
|
|
212
|
+
image: imageObject(image),
|
|
213
|
+
sameAs: sameAs.length > 0 ? [...sameAs] : undefined,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
|
|
218
|
+
webPage: ({ name, description, url, language, trail, dateModified }) => [
|
|
219
|
+
{
|
|
220
|
+
'@type': 'WebPage',
|
|
221
|
+
'@id': `${url}#page`,
|
|
222
|
+
url,
|
|
223
|
+
name,
|
|
224
|
+
description,
|
|
225
|
+
inLanguage: language,
|
|
226
|
+
dateModified,
|
|
227
|
+
isPartOf: { '@id': `${origin}#website` },
|
|
228
|
+
},
|
|
229
|
+
...(trail.length > 0 ? [breadcrumbs(trail)] : []),
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @typedef {object} ArticleInput
|
|
236
|
+
* @property {string} url the article's absolute URL
|
|
237
|
+
* @property {string} headline
|
|
238
|
+
* @property {string} [description]
|
|
239
|
+
* @property {ImageInfo} [image]
|
|
240
|
+
* @property {string} datePublished
|
|
241
|
+
* @property {string} dateModified
|
|
242
|
+
* @property {string} language
|
|
243
|
+
* @property {readonly {name: string, url: string, sameAs: readonly string[]}[]} authors
|
|
244
|
+
* @property {readonly string[]} tags
|
|
245
|
+
* @property {{name: string, url: string}} [series]
|
|
246
|
+
* @property {readonly CrumbStep[]} trail
|
|
247
|
+
*/
|