@hagicode/hagilight-starlight 0.2.4 → 0.2.5-dev.43.1.d86178d
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/package.json +2 -3
- package/rss-renderer.mjs +7 -7
- package/seo-schema.d.ts +5 -21
- package/seo-schema.mjs +5 -24
- package/seo-utils.mjs +27 -171
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight-starlight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5-dev.43.1.d86178d",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Starlight customizations for HagiCode documentation",
|
|
6
6
|
"repository": {
|
|
@@ -57,8 +57,7 @@
|
|
|
57
57
|
"assets/*.png"
|
|
58
58
|
],
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@
|
|
61
|
-
"@hagicode/hagilight": "0.2.4"
|
|
60
|
+
"@hagicode/hagilight": "0.2.5-dev.43.1.d86178d"
|
|
62
61
|
},
|
|
63
62
|
"peerDependencies": {
|
|
64
63
|
"@astrojs/starlight": "^0.42.4",
|
package/rss-renderer.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { generateRssFeed as generateCoreRssFeed } from '@hagicode/hagilight/rss';
|
|
2
2
|
import { selectRssEntries } from './rss-utils.mjs';
|
|
3
3
|
|
|
4
4
|
export function generateRssFeed({ site, baseUrl, filename, locales, options, entries }) {
|
|
@@ -8,24 +8,24 @@ export function generateRssFeed({ site, baseUrl, filename, locales, options, ent
|
|
|
8
8
|
throw new Error(`Hagilight RSS has no configured locale for "${filename}".`);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const base = new URL(baseUrl, site);
|
|
12
11
|
const items = selectRssEntries(entries, { filename, locales, options }).map(({ id, data }) => {
|
|
13
12
|
const slug = id.replace(/(?:^|\/)index$/u, '');
|
|
14
13
|
return {
|
|
15
14
|
title: data.title,
|
|
16
15
|
description: data.description,
|
|
17
16
|
...(data.lastUpdated instanceof Date && !Number.isNaN(data.lastUpdated.getTime())
|
|
18
|
-
? {
|
|
17
|
+
? { date: data.lastUpdated }
|
|
19
18
|
: {}),
|
|
20
|
-
link:
|
|
19
|
+
link: slug ? `${slug}/` : '/',
|
|
21
20
|
};
|
|
22
21
|
});
|
|
23
22
|
|
|
24
|
-
return
|
|
23
|
+
return generateCoreRssFeed({
|
|
24
|
+
site,
|
|
25
|
+
baseUrl,
|
|
26
|
+
language: locale?.lang ?? 'en',
|
|
25
27
|
title: 'Documentation',
|
|
26
28
|
description: 'Recently updated documentation pages',
|
|
27
|
-
site: base,
|
|
28
|
-
customData: `<language>${locale?.lang ?? 'en'}</language>`,
|
|
29
29
|
items,
|
|
30
30
|
});
|
|
31
31
|
}
|
package/seo-schema.d.ts
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from "astro/zod";
|
|
7
|
-
|
|
8
|
-
export const seoSchema: ZodObject<{
|
|
9
|
-
seo: ZodOptional<
|
|
10
|
-
ZodObject<{
|
|
11
|
-
title: ZodOptional<ZodString>;
|
|
12
|
-
description: ZodOptional<ZodString>;
|
|
13
|
-
image: ZodOptional<ZodString>;
|
|
14
|
-
author: ZodOptional<ZodString>;
|
|
15
|
-
publishedDate: ZodOptional<ZodDate>;
|
|
16
|
-
}>
|
|
17
|
-
>;
|
|
18
|
-
}>;
|
|
19
|
-
|
|
20
|
-
export { seoSchema as seoFrontmatterSchema };
|
|
21
|
-
export { seoSchema as default };
|
|
1
|
+
export {
|
|
2
|
+
seoSchema,
|
|
3
|
+
seoSchema as seoFrontmatterSchema,
|
|
4
|
+
seoSchema as default,
|
|
5
|
+
} from "@hagicode/hagilight/seo-schema";
|
package/seo-schema.mjs
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.trim()
|
|
7
|
-
.min(1)
|
|
8
|
-
.refine(isValidSeoImageReference, {
|
|
9
|
-
message: 'Use an absolute HTTP(S) URL or a site-root path beginning with "/".',
|
|
10
|
-
})
|
|
11
|
-
.optional();
|
|
12
|
-
|
|
13
|
-
export const seoSchema = z.object({
|
|
14
|
-
seo: z.object({
|
|
15
|
-
title: optionalText,
|
|
16
|
-
description: optionalText,
|
|
17
|
-
image: optionalImage,
|
|
18
|
-
author: optionalText,
|
|
19
|
-
publishedDate: z.coerce.date().optional(),
|
|
20
|
-
}).optional(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export { seoSchema as seoFrontmatterSchema };
|
|
24
|
-
export default seoSchema;
|
|
1
|
+
export {
|
|
2
|
+
seoSchema,
|
|
3
|
+
seoSchema as seoFrontmatterSchema,
|
|
4
|
+
seoSchema as default,
|
|
5
|
+
} from '@hagicode/hagilight/seo-schema';
|
package/seo-utils.mjs
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { isBlogEntry } from './rss-utils.mjs';
|
|
2
|
+
import {
|
|
3
|
+
buildArticleStructuredData,
|
|
4
|
+
buildBreadcrumbStructuredData,
|
|
5
|
+
buildOrganizationStructuredData,
|
|
6
|
+
} from '@hagicode/hagilight/seo-utils';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
composeSeoHead,
|
|
10
|
+
extractSeoDescription,
|
|
11
|
+
isValidSeoImageReference,
|
|
12
|
+
resolveSeoImageUrl,
|
|
13
|
+
resolveSeoMetadata,
|
|
14
|
+
serializeJsonLd,
|
|
15
|
+
} from '@hagicode/hagilight/seo-utils';
|
|
2
16
|
|
|
3
17
|
const normalizeSlug = (slug) => slug
|
|
4
18
|
.replace(/^\/+|\/+$/gu, '')
|
|
@@ -190,12 +204,6 @@ export function buildDocsPageUrl(route, slug, {
|
|
|
190
204
|
return new URL(`${base}${pagePath}`, site).href;
|
|
191
205
|
}
|
|
192
206
|
|
|
193
|
-
function validDate(value) {
|
|
194
|
-
if (value === undefined || value === null || value === false) return undefined;
|
|
195
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
196
|
-
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
207
|
export function buildStructuredData({
|
|
200
208
|
entry,
|
|
201
209
|
entryId,
|
|
@@ -218,24 +226,14 @@ export function buildStructuredData({
|
|
|
218
226
|
const data = entry.data ?? {};
|
|
219
227
|
const structuredData = [];
|
|
220
228
|
if (isBlogEntry(page.slug)) {
|
|
221
|
-
|
|
222
|
-
'@context': 'https://schema.org',
|
|
223
|
-
'@type': 'Article',
|
|
224
|
-
headline: data.title,
|
|
229
|
+
structuredData.push(buildArticleStructuredData({
|
|
225
230
|
url: canonicalUrl,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (published) article.datePublished = published;
|
|
233
|
-
if (modified) article.dateModified = modified;
|
|
234
|
-
const author = pageSeo.author ?? data.author;
|
|
235
|
-
if (typeof author === 'string' && author.trim()) {
|
|
236
|
-
article.author = { '@type': 'Person', name: author.trim() };
|
|
237
|
-
}
|
|
238
|
-
structuredData.push(article);
|
|
231
|
+
title: data.title,
|
|
232
|
+
description: pageSeo.description ?? data.description,
|
|
233
|
+
author: pageSeo.author ?? data.author,
|
|
234
|
+
publishedDate: pageSeo.publishedDate ?? data.publishedDate,
|
|
235
|
+
modifiedDate: data.lastUpdated,
|
|
236
|
+
}));
|
|
239
237
|
}
|
|
240
238
|
|
|
241
239
|
const slugParts = page.slug.split('/').filter(Boolean);
|
|
@@ -252,159 +250,17 @@ export function buildStructuredData({
|
|
|
252
250
|
format,
|
|
253
251
|
trailingSlash,
|
|
254
252
|
});
|
|
255
|
-
crumbs.push({
|
|
256
|
-
'@type': 'ListItem',
|
|
257
|
-
position: crumbs.length + 1,
|
|
258
|
-
name: ancestor.data.title,
|
|
259
|
-
item: url,
|
|
260
|
-
});
|
|
253
|
+
crumbs.push({ name: ancestor.data.title, url });
|
|
261
254
|
}
|
|
262
255
|
if (crumbs.length > 0) {
|
|
263
|
-
structuredData.push(
|
|
264
|
-
'@context': 'https://schema.org',
|
|
265
|
-
'@type': 'BreadcrumbList',
|
|
266
|
-
itemListElement: crumbs,
|
|
267
|
-
});
|
|
256
|
+
structuredData.push(buildBreadcrumbStructuredData(crumbs));
|
|
268
257
|
}
|
|
269
258
|
|
|
270
259
|
if (organization && page.route === defaultLocale && page.slug === '') {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
url: organization.url,
|
|
276
|
-
};
|
|
277
|
-
if (organization.logo) org.logo = organization.logo;
|
|
278
|
-
if (siteTitle) org.alternateName = siteTitle;
|
|
279
|
-
structuredData.push(org);
|
|
260
|
+
structuredData.push(buildOrganizationStructuredData({
|
|
261
|
+
...organization,
|
|
262
|
+
alternateName: siteTitle,
|
|
263
|
+
}));
|
|
280
264
|
}
|
|
281
265
|
return structuredData;
|
|
282
266
|
}
|
|
283
|
-
|
|
284
|
-
export function serializeJsonLd(value) {
|
|
285
|
-
return JSON.stringify(value).replace(/</gu, '\\u003c');
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function metaKey(entry) {
|
|
289
|
-
if (entry?.tag !== 'meta') return undefined;
|
|
290
|
-
const attrs = entry.attrs ?? {};
|
|
291
|
-
if (typeof attrs.property === 'string') return `property:${attrs.property.toLowerCase()}`;
|
|
292
|
-
if (typeof attrs.name === 'string') return `name:${attrs.name.toLowerCase()}`;
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function existingMetaValue(head, key) {
|
|
297
|
-
return head.find((entry) => metaKey(entry) === key)?.attrs?.content;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function explicitMetaValue(heads, key) {
|
|
301
|
-
return heads.flatMap((head) => (Array.isArray(head) ? head : []))
|
|
302
|
-
.find((entry) => metaKey(entry) === key)?.attrs?.content;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export function extractSeoDescription(body) {
|
|
306
|
-
if (typeof body !== 'string' || !body.trim()) return undefined;
|
|
307
|
-
const text = body
|
|
308
|
-
.replace(/^```[\s\S]*?^```/gmu, ' ')
|
|
309
|
-
.replace(/^import\s.+;?\s*$/gmu, ' ')
|
|
310
|
-
.replace(/!\[([^\]]*)\]\([^)]*\)/gu, '$1')
|
|
311
|
-
.replace(/\[([^\]]+)\]\([^)]*\)/gu, '$1')
|
|
312
|
-
.replace(/<[^>]*>/gu, ' ')
|
|
313
|
-
.replace(/`([^`]+)`/gu, '$1')
|
|
314
|
-
.replace(/^\s{0,3}(?:#{1,6}\s+|>\s*|[-*+]\s+|\d+\.\s+)/gmu, '')
|
|
315
|
-
.replace(/[*_~]/gu, '')
|
|
316
|
-
.replace(/\s+/gu, ' ')
|
|
317
|
-
.trim();
|
|
318
|
-
if (!text) return undefined;
|
|
319
|
-
|
|
320
|
-
const characters = Array.from(text);
|
|
321
|
-
if (characters.length <= 160) return text;
|
|
322
|
-
let end = 159;
|
|
323
|
-
const boundary = characters.slice(0, end).lastIndexOf(' ');
|
|
324
|
-
if (boundary > 120) end = boundary;
|
|
325
|
-
return `${characters.slice(0, end).join('').trimEnd()}…`;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function resolveSeoMetadata({
|
|
329
|
-
entry,
|
|
330
|
-
head,
|
|
331
|
-
pageSeo = {},
|
|
332
|
-
siteSeo = {},
|
|
333
|
-
site,
|
|
334
|
-
basePath = '/',
|
|
335
|
-
explicitHead = [],
|
|
336
|
-
body,
|
|
337
|
-
}) {
|
|
338
|
-
const title = pageSeo.title
|
|
339
|
-
?? siteSeo.title
|
|
340
|
-
?? entry.data.title
|
|
341
|
-
?? existingMetaValue(head, 'property:og:title');
|
|
342
|
-
const description = pageSeo.description
|
|
343
|
-
?? entry.data.description
|
|
344
|
-
?? explicitMetaValue(explicitHead, 'name:description')
|
|
345
|
-
?? explicitMetaValue(explicitHead, 'property:og:description')
|
|
346
|
-
?? extractSeoDescription(body)
|
|
347
|
-
?? existingMetaValue(head, 'name:description')
|
|
348
|
-
?? existingMetaValue(head, 'property:og:description')
|
|
349
|
-
?? siteSeo.description;
|
|
350
|
-
const image = pageSeo.image ?? siteSeo.image;
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
title,
|
|
354
|
-
description,
|
|
355
|
-
image: image === undefined ? undefined : resolveSeoImageUrl(image, { site, basePath }),
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
export function isValidSeoImageReference(value) {
|
|
360
|
-
if (typeof value !== 'string' || value.trim() !== value || value.length === 0) return false;
|
|
361
|
-
if (value.startsWith('/')) return !value.startsWith('//');
|
|
362
|
-
try {
|
|
363
|
-
const url = new URL(value);
|
|
364
|
-
return (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password;
|
|
365
|
-
} catch {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
export function resolveSeoImageUrl(value, { site, basePath = '/' }) {
|
|
371
|
-
if (!isValidSeoImageReference(value)) {
|
|
372
|
-
throw new TypeError('Hagilight SEO image must be an absolute HTTP(S) URL or a site-root path.');
|
|
373
|
-
}
|
|
374
|
-
if (/^https?:\/\//iu.test(value)) return new URL(value).href;
|
|
375
|
-
const base = `${normalizeBasePath(basePath)}/`;
|
|
376
|
-
return new URL(`${base}${value.replace(/^\/+/u, '')}`, site).href;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function explicitMetaKeys(heads) {
|
|
380
|
-
return new Set(heads.flatMap((head) => (Array.isArray(head) ? head : [])
|
|
381
|
-
.map(metaKey)
|
|
382
|
-
.filter(Boolean)));
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export function composeSeoHead(head, metadata, explicitHead = []) {
|
|
386
|
-
const explicit = explicitMetaKeys(explicitHead);
|
|
387
|
-
const values = [
|
|
388
|
-
['property:og:title', metadata.title],
|
|
389
|
-
['property:og:description', metadata.description],
|
|
390
|
-
['property:og:image', metadata.image],
|
|
391
|
-
['name:twitter:title', metadata.title],
|
|
392
|
-
['name:twitter:description', metadata.description],
|
|
393
|
-
['name:twitter:image', metadata.image],
|
|
394
|
-
];
|
|
395
|
-
const generatedKeys = new Set(values.map(([key]) => key));
|
|
396
|
-
const result = head.filter((entry) => {
|
|
397
|
-
const key = metaKey(entry);
|
|
398
|
-
return !key || explicit.has(key) || !generatedKeys.has(key);
|
|
399
|
-
});
|
|
400
|
-
const present = new Set(result.map(metaKey).filter(Boolean));
|
|
401
|
-
|
|
402
|
-
for (const [key, content] of values) {
|
|
403
|
-
if (content === undefined || explicit.has(key) || present.has(key)) continue;
|
|
404
|
-
const separator = key.indexOf(':');
|
|
405
|
-
const attribute = key.slice(0, separator);
|
|
406
|
-
const value = key.slice(separator + 1);
|
|
407
|
-
result.push({ tag: 'meta', attrs: { [attribute]: value, content } });
|
|
408
|
-
}
|
|
409
|
-
return result;
|
|
410
|
-
}
|