@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagilight-starlight",
3
- "version": "0.2.4",
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
- "@astrojs/rss": "^4.0.19",
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 rss from '@astrojs/rss';
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
- ? { pubDate: data.lastUpdated }
17
+ ? { date: data.lastUpdated }
19
18
  : {}),
20
- link: new URL(`${slug ? `${slug}/` : ''}`, base).toString(),
19
+ link: slug ? `${slug}/` : '/',
21
20
  };
22
21
  });
23
22
 
24
- return rss({
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
- import type {
2
- ZodDate,
3
- ZodObject,
4
- ZodOptional,
5
- ZodString,
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
- import { z } from 'astro/zod';
2
- import { isValidSeoImageReference } from './seo-utils.mjs';
3
-
4
- const optionalText = z.string().trim().min(1).optional();
5
- const optionalImage = z.string()
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
- const article = {
222
- '@context': 'https://schema.org',
223
- '@type': 'Article',
224
- headline: data.title,
229
+ structuredData.push(buildArticleStructuredData({
225
230
  url: canonicalUrl,
226
- mainEntityOfPage: { '@type': 'WebPage', '@id': canonicalUrl },
227
- };
228
- const description = pageSeo.description ?? data.description;
229
- if (description) article.description = description;
230
- const published = validDate(pageSeo.publishedDate ?? data.publishedDate);
231
- const modified = validDate(data.lastUpdated);
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
- const org = {
272
- '@context': 'https://schema.org',
273
- '@type': 'Organization',
274
- name: organization.name,
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
- }