@rimelight/seo 0.0.5 → 0.0.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.
Files changed (74) hide show
  1. package/dist/assets.d.mts +39 -0
  2. package/dist/assets.mjs +96 -0
  3. package/dist/catalog.d.mts +7 -0
  4. package/dist/catalog.mjs +10 -0
  5. package/dist/index.d.mts +9 -3
  6. package/dist/index.mjs +10 -2
  7. package/dist/integration.d.mts +3 -20
  8. package/dist/integration.mjs +257 -7
  9. package/dist/manifest.d.mts +7 -0
  10. package/dist/manifest.mjs +44 -0
  11. package/dist/og.d.mts +117 -0
  12. package/dist/og.mjs +302 -0
  13. package/dist/routes/api-catalog.d.mts +5 -0
  14. package/dist/routes/api-catalog.mjs +38 -0
  15. package/dist/routes/apple-touch-icon.png.d.mts +5 -0
  16. package/dist/routes/apple-touch-icon.png.mjs +17 -0
  17. package/dist/routes/favicon.ico.d.mts +5 -0
  18. package/dist/routes/favicon.ico.mjs +14 -0
  19. package/dist/routes/favicon.svg.d.mts +5 -0
  20. package/dist/routes/favicon.svg.mjs +12 -0
  21. package/dist/routes/icon-192.png.d.mts +5 -0
  22. package/dist/routes/icon-192.png.mjs +17 -0
  23. package/dist/routes/icon-512.png.d.mts +5 -0
  24. package/dist/routes/icon-512.png.mjs +17 -0
  25. package/dist/routes/icon-maskable-512.png.d.mts +5 -0
  26. package/dist/routes/icon-maskable-512.png.mjs +18 -0
  27. package/dist/routes/llms-full.txt.d.mts +5 -0
  28. package/dist/routes/llms-full.txt.mjs +57 -0
  29. package/dist/routes/llms.txt.d.mts +1 -0
  30. package/dist/routes/llms.txt.mjs +60 -12
  31. package/dist/routes/robots.txt.mjs +32 -4
  32. package/dist/routes/rss.xml.d.mts +4 -0
  33. package/dist/routes/rss.xml.mjs +79 -0
  34. package/dist/routes/security.txt.d.mts +5 -0
  35. package/dist/routes/security.txt.mjs +24 -0
  36. package/dist/routes/site.webmanifest.d.mts +5 -0
  37. package/dist/routes/site.webmanifest.mjs +23 -0
  38. package/dist/routes/sitemap.xml.mjs +18 -4
  39. package/dist/rss.d.mts +8 -0
  40. package/dist/rss.mjs +44 -0
  41. package/dist/security.d.mts +7 -0
  42. package/dist/security.mjs +34 -0
  43. package/dist/types.d.mts +263 -3
  44. package/package.json +38 -3
  45. package/src/assets.test.ts +70 -0
  46. package/src/assets.ts +137 -0
  47. package/src/catalog.test.ts +26 -0
  48. package/src/catalog.ts +11 -0
  49. package/src/components/SEOHead.astro +1 -1
  50. package/src/env.d.ts +51 -3
  51. package/src/index.ts +33 -2
  52. package/src/integration.ts +307 -28
  53. package/src/manifest.test.ts +41 -0
  54. package/src/manifest.ts +59 -0
  55. package/src/og.ts +436 -0
  56. package/src/routes/api-catalog.ts +55 -0
  57. package/src/routes/apple-touch-icon.png.ts +19 -0
  58. package/src/routes/favicon.ico.ts +19 -0
  59. package/src/routes/favicon.svg.ts +17 -0
  60. package/src/routes/icon-192.png.ts +19 -0
  61. package/src/routes/icon-512.png.ts +19 -0
  62. package/src/routes/icon-maskable-512.png.ts +24 -0
  63. package/src/routes/llms-full.txt.ts +77 -0
  64. package/src/routes/llms.txt.ts +84 -13
  65. package/src/routes/robots.txt.ts +47 -4
  66. package/src/routes/rss.xml.ts +129 -0
  67. package/src/routes/security.txt.ts +30 -0
  68. package/src/routes/site.webmanifest.ts +28 -0
  69. package/src/routes/sitemap.xml.ts +32 -5
  70. package/src/rss.test.ts +28 -0
  71. package/src/rss.ts +63 -0
  72. package/src/security.test.ts +46 -0
  73. package/src/security.ts +78 -0
  74. package/src/types.ts +286 -3
package/dist/types.d.mts CHANGED
@@ -115,16 +115,17 @@ export interface SiteConfig {
115
115
  name: string;
116
116
  description: string;
117
117
  url: string;
118
- ogImage: string;
118
+ ogImage?: string;
119
119
  author: string;
120
120
  email: string;
121
121
  branding: {
122
122
  logo: {
123
123
  alt: string;
124
124
  };
125
- favicon: {
125
+ favicon?: {
126
126
  svg: string;
127
127
  };
128
+ icon?: string;
128
129
  colors: {
129
130
  themeColor: string;
130
131
  backgroundColor: string;
@@ -132,7 +133,7 @@ export interface SiteConfig {
132
133
  };
133
134
  seo: {
134
135
  titleTemplate: string;
135
- ogImageFallback: string;
136
+ ogImageFallback?: string;
136
137
  maxDescriptionLength: number;
137
138
  authorHandle?: string;
138
139
  };
@@ -273,5 +274,264 @@ export interface HeadProps {
273
274
  */
274
275
  twitter?: TwitterMeta;
275
276
  }
277
+ export interface RssItem {
278
+ title: string;
279
+ link: string;
280
+ pubDate: Date | string | number;
281
+ description?: string;
282
+ content?: string;
283
+ guid?: string;
284
+ author?: string;
285
+ }
286
+ export interface RssFeedOptions {
287
+ title: string;
288
+ description: string;
289
+ site: string;
290
+ feedUrl?: string;
291
+ language?: string;
292
+ items: RssItem[];
293
+ }
294
+ export interface RssCollectionConfig {
295
+ name: string;
296
+ pathPrefix?: string;
297
+ }
298
+ export interface RssOptions extends Omit<Partial<RssFeedOptions>, "items"> {
299
+ /**
300
+ * Custom RSS route pattern (default: "/rss.xml")
301
+ */
302
+ path?: string;
303
+ /**
304
+ * Custom items loader or static items array
305
+ */
306
+ items?: RssItem[] | (() => Promise<RssItem[]> | RssItem[]);
307
+ /**
308
+ * Collections to include in RSS feed (defaults to ["blog", "posts", "news", "articles"])
309
+ */
310
+ collections?: (string | RssCollectionConfig)[];
311
+ }
312
+ export interface SitemapOptions {
313
+ /**
314
+ * Sitemap route pattern (default: "/sitemap.xml")
315
+ */
316
+ path?: string;
317
+ urls?: SitemapUrl[];
318
+ entries?: () => Promise<SeoEntry[]> | SeoEntry[];
319
+ locales?: LocaleConfig;
320
+ [key: string]: unknown;
321
+ }
322
+ export interface SecurityTxtOptions {
323
+ /**
324
+ * Contact URI or list of contact URIs (e.g. "mailto:security@example.com") - REQUIRED by RFC 9116
325
+ */
326
+ contact: string | string[];
327
+ /**
328
+ * Expiration date/time (ISO 8601 string or Date) - REQUIRED by RFC 9116
329
+ */
330
+ expires?: Date | string;
331
+ /**
332
+ * URI to PGP/GPG public key
333
+ */
334
+ encryption?: string | string[];
335
+ /**
336
+ * URI to security acknowledgments / hall of fame
337
+ */
338
+ acknowledgments?: string | string[];
339
+ /**
340
+ * Comma-separated list of preferred languages (e.g. "en, pt")
341
+ */
342
+ preferredLanguages?: string;
343
+ /**
344
+ * Canonical URI of the security.txt file
345
+ */
346
+ canonical?: string;
347
+ /**
348
+ * URI to security policy
349
+ */
350
+ policy?: string;
351
+ /**
352
+ * URI to security hiring page
353
+ */
354
+ hiring?: string;
355
+ /**
356
+ * URI to Common Security Advisory Framework (CSAF) provider
357
+ */
358
+ csaf?: string;
359
+ }
360
+ export interface SecurityRouteOptions extends Partial<SecurityTxtOptions> {
361
+ /**
362
+ * Route pattern (default: "/.well-known/security.txt")
363
+ */
364
+ path?: string;
365
+ /**
366
+ * Whether to also inject legacy fallback route at "/security.txt" (default: true)
367
+ */
368
+ legacyRoute?: boolean;
369
+ }
370
+ export interface WebManifestIcon {
371
+ src: string;
372
+ sizes: string;
373
+ type: string;
374
+ purpose?: "any" | "maskable" | "monochrome";
375
+ }
376
+ export interface WebManifestShortcut {
377
+ name: string;
378
+ url: string;
379
+ short_name?: string;
380
+ description?: string;
381
+ icons?: WebManifestIcon[];
382
+ }
383
+ export interface WebManifest {
384
+ name: string;
385
+ short_name?: string;
386
+ description?: string;
387
+ start_url: string;
388
+ display: "fullscreen" | "standalone" | "minimal-ui" | "browser";
389
+ background_color: string;
390
+ theme_color: string;
391
+ icons: WebManifestIcon[];
392
+ shortcuts?: WebManifestShortcut[];
393
+ categories?: string[];
394
+ lang?: string;
395
+ dir?: "auto" | "ltr" | "rtl";
396
+ orientation?: "any" | "natural" | "landscape" | "portrait";
397
+ scope?: string;
398
+ [key: string]: unknown;
399
+ }
400
+ export interface WebManifestOptions {
401
+ name: string;
402
+ shortName?: string;
403
+ description?: string;
404
+ startUrl?: string;
405
+ display?: "fullscreen" | "standalone" | "minimal-ui" | "browser";
406
+ backgroundColor?: string;
407
+ themeColor?: string;
408
+ icons?: WebManifestIcon[];
409
+ shortcuts?: WebManifestShortcut[];
410
+ categories?: string[];
411
+ lang?: string;
412
+ dir?: "auto" | "ltr" | "rtl";
413
+ orientation?: "any" | "natural" | "landscape" | "portrait";
414
+ scope?: string;
415
+ }
416
+ export interface ManifestRouteOptions extends Partial<WebManifestOptions> {
417
+ /**
418
+ * Route pattern (default: "/site.webmanifest")
419
+ */
420
+ path?: string;
421
+ }
422
+ export interface ApiCatalogItem {
423
+ rel: string;
424
+ href: string;
425
+ type?: string;
426
+ title?: string;
427
+ [key: string]: unknown;
428
+ }
429
+ export interface ApiCatalog {
430
+ "api-catalog": ApiCatalogItem[];
431
+ }
432
+ export interface ApiCatalogOptions {
433
+ items: ApiCatalogItem[];
434
+ }
435
+ export interface ApiCatalogRouteOptions extends Partial<ApiCatalogOptions> {
436
+ /**
437
+ * Route pattern (default: "/.well-known/api-catalog")
438
+ */
439
+ path?: string;
440
+ }
441
+ export interface RobotsRouteOptions extends Partial<RobotsOptions> {
442
+ /**
443
+ * Route pattern (default: "/robots.txt")
444
+ */
445
+ path?: string;
446
+ }
447
+ export interface LlmsRouteOptions extends Partial<LlmsTxtOptions> {
448
+ /**
449
+ * Route pattern (default: "/llms.txt")
450
+ */
451
+ path?: string;
452
+ }
453
+ export interface LlmsFullRouteOptions extends Partial<LlmsFullTxtOptions> {
454
+ /**
455
+ * Route pattern (default: "/llms-full.txt")
456
+ */
457
+ path?: string;
458
+ }
459
+ export interface AssetRouteOptions {
460
+ /**
461
+ * Path to the master SVG/PNG icon file (relative to project root or absolute). If omitted, will
462
+ * search in src/assets/logos/ or siteConfig.branding.icon.
463
+ */
464
+ icon?: string;
465
+ /**
466
+ * Background color for maskable icons (defaults to siteConfig.branding.colors.backgroundColor or
467
+ * #ffffff)
468
+ */
469
+ backgroundColor?: string;
470
+ /**
471
+ * Whether to inject /favicon.ico (default: true)
472
+ */
473
+ faviconIco?: boolean;
474
+ /**
475
+ * Whether to inject /apple-touch-icon.png (default: true)
476
+ */
477
+ appleTouchIcon?: boolean;
478
+ /**
479
+ * Whether to inject /icon-192.png (default: true)
480
+ */
481
+ icon192?: boolean;
482
+ /**
483
+ * Whether to inject /icon-512.png (default: true)
484
+ */
485
+ icon512?: boolean;
486
+ /**
487
+ * Whether to inject /icon-maskable-512.png (default: true)
488
+ */
489
+ iconMaskable?: boolean;
490
+ /**
491
+ * Whether to generate and emit /og.png branded card (default: true)
492
+ */
493
+ ogImage?: boolean;
494
+ }
495
+ export interface RimelightSeoOptions {
496
+ /**
497
+ * Sitemap configuration or false to disable automatic /sitemap.xml (default: true)
498
+ */
499
+ sitemap?: boolean | SitemapOptions;
500
+ /**
501
+ * Robots.txt configuration or false to disable automatic /robots.txt (default: true)
502
+ */
503
+ robots?: boolean | RobotsRouteOptions;
504
+ /**
505
+ * RSS feed configuration or false to disable automatic /rss.xml (default: true)
506
+ */
507
+ rss?: boolean | RssOptions;
508
+ /**
509
+ * LLMs.txt configuration or false to disable automatic /llms.txt (default: true)
510
+ */
511
+ llms?: boolean | LlmsRouteOptions;
512
+ /**
513
+ * LLMs-full.txt configuration or false to disable automatic /llms-full.txt (default: true)
514
+ */
515
+ llmsFull?: boolean | LlmsFullRouteOptions;
516
+ /**
517
+ * Security.txt configuration or false to disable automatic /.well-known/security.txt (default:
518
+ * true)
519
+ */
520
+ security?: boolean | SecurityRouteOptions;
521
+ /**
522
+ * RFC 9652 API Catalog configuration or false to disable automatic /.well-known/api-catalog
523
+ * (default: true)
524
+ */
525
+ apiCatalog?: boolean | ApiCatalogRouteOptions;
526
+ /**
527
+ * Web App Manifest configuration or false to disable automatic /site.webmanifest (default: true)
528
+ */
529
+ manifest?: boolean | ManifestRouteOptions;
530
+ /**
531
+ * Automatic generation of compliant public image assets (favicon.ico, apple-touch-icon.png, PWA
532
+ * icons) from a single master icon (default: true)
533
+ */
534
+ assets?: boolean | AssetRouteOptions;
535
+ }
276
536
  //#endregion
277
537
  export type { ArticleSchemaOptions, BreadcrumbItem, LlmsFullTxtOptions, LlmsPage, LlmsTxtOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/seo",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "private": false,
5
5
  "description": "Rimelight Entertainment's SEO Package",
6
6
  "homepage": "https://rimelight.com/docs",
@@ -41,6 +41,30 @@
41
41
  "types": "./dist/integration.d.mts",
42
42
  "import": "./dist/integration.mjs"
43
43
  },
44
+ "./rss": {
45
+ "types": "./dist/rss.d.mts",
46
+ "import": "./dist/rss.mjs"
47
+ },
48
+ "./security": {
49
+ "types": "./dist/security.d.mts",
50
+ "import": "./dist/security.mjs"
51
+ },
52
+ "./manifest": {
53
+ "types": "./dist/manifest.d.mts",
54
+ "import": "./dist/manifest.mjs"
55
+ },
56
+ "./catalog": {
57
+ "types": "./dist/catalog.d.mts",
58
+ "import": "./dist/catalog.mjs"
59
+ },
60
+ "./og": {
61
+ "types": "./dist/og.d.mts",
62
+ "import": "./dist/og.mjs"
63
+ },
64
+ "./assets": {
65
+ "types": "./dist/assets.d.mts",
66
+ "import": "./dist/assets.mjs"
67
+ },
44
68
  "./components/*": "./src/components/*",
45
69
  "./routes/*": "./src/routes/*",
46
70
  "./*": "./src/*"
@@ -49,12 +73,23 @@
49
73
  "access": "public"
50
74
  },
51
75
  "devDependencies": {
52
- "@rimelight/config": "0.0.6",
76
+ "@rimelight/config": "0.0.9",
53
77
  "astro": "7.3.2",
78
+ "sharp": "^0.35.4",
54
79
  "typescript": "6.0.3"
55
80
  },
56
81
  "peerDependencies": {
57
- "astro": ">=7.0.0"
82
+ "astro": ">=7.0.0",
83
+ "sharp": ">=0.33.0",
84
+ "takumi-js": ">=2.0.0"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "sharp": {
88
+ "optional": true
89
+ },
90
+ "takumi-js": {
91
+ "optional": true
92
+ }
58
93
  },
59
94
  "engines": {
60
95
  "node": ">=26.8.2"
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from "vite-plus/test"
2
+ import { generatePng, generateMaskablePng, generateIco, generateOgCard } from "./assets.js"
3
+ import sharp from "sharp"
4
+
5
+ const sampleSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
6
+ <circle cx="50" cy="50" r="40" fill="#ff0000" />
7
+ </svg>`
8
+
9
+ describe("Asset Generation", () => {
10
+ it("generates PNG buffer with correct dimensions", async () => {
11
+ const pngBuffer = await generatePng(Buffer.from(sampleSvg), { width: 192, height: 192 })
12
+ expect(pngBuffer).toBeInstanceOf(Buffer)
13
+
14
+ const meta = await sharp(pngBuffer).metadata()
15
+ expect(meta.format).toBe("png")
16
+ expect(meta.width).toBe(192)
17
+ expect(meta.height).toBe(192)
18
+ })
19
+
20
+ it("generates maskable PNG buffer with padding and background", async () => {
21
+ const maskableBuffer = await generateMaskablePng(Buffer.from(sampleSvg), {
22
+ size: 512,
23
+ background: "#123456"
24
+ })
25
+ expect(maskableBuffer).toBeInstanceOf(Buffer)
26
+
27
+ const meta = await sharp(maskableBuffer).metadata()
28
+ expect(meta.format).toBe("png")
29
+ expect(meta.width).toBe(512)
30
+ expect(meta.height).toBe(512)
31
+ })
32
+
33
+ it("generates valid .ico buffer containing PNG data", async () => {
34
+ const icoBuffer = await generateIco(Buffer.from(sampleSvg), 32)
35
+ expect(icoBuffer).toBeInstanceOf(Buffer)
36
+ expect(icoBuffer.length).toBeGreaterThan(22)
37
+
38
+ // Verify ICO header
39
+ expect(icoBuffer.readUInt16LE(0)).toBe(0) // Reserved
40
+ expect(icoBuffer.readUInt16LE(2)).toBe(1) // Type = 1 (ICO)
41
+ expect(icoBuffer.readUInt16LE(4)).toBe(1) // 1 image
42
+
43
+ // Verify directory entry
44
+ expect(icoBuffer.readUInt8(6)).toBe(32) // Width
45
+ expect(icoBuffer.readUInt8(7)).toBe(32) // Height
46
+ expect(icoBuffer.readUInt16LE(10)).toBe(1) // Planes
47
+ expect(icoBuffer.readUInt16LE(12)).toBe(32) // 32 bpp
48
+ expect(icoBuffer.readUInt32LE(18)).toBe(22) // Offset = 22
49
+
50
+ // Verify PNG payload inside ICO
51
+ const pngSlice = icoBuffer.subarray(22)
52
+ const meta = await sharp(pngSlice).metadata()
53
+ expect(meta.format).toBe("png")
54
+ expect(meta.width).toBe(32)
55
+ expect(meta.height).toBe(32)
56
+ })
57
+
58
+ it("generates standard 1200x630 OG card buffer", async () => {
59
+ const ogBuffer = await generateOgCard({
60
+ logoBuffer: Buffer.from(sampleSvg),
61
+ background: "#0a0a0a"
62
+ })
63
+ expect(ogBuffer).toBeInstanceOf(Buffer)
64
+
65
+ const meta = await sharp(ogBuffer).metadata()
66
+ expect(meta.format).toBe("png")
67
+ expect(meta.width).toBe(1200)
68
+ expect(meta.height).toBe(630)
69
+ })
70
+ })
package/src/assets.ts ADDED
@@ -0,0 +1,137 @@
1
+ import sharp from "sharp"
2
+
3
+ export interface GeneratePngOptions {
4
+ width: number
5
+ height: number
6
+ background?: string
7
+ }
8
+
9
+ export interface GenerateMaskableOptions {
10
+ size?: number
11
+ background?: string
12
+ paddingRatio?: number
13
+ }
14
+
15
+ export interface GenerateOgCardOptions {
16
+ width?: number
17
+ height?: number
18
+ background?: string
19
+ logoBuffer?: Buffer | Uint8Array | string | null
20
+ }
21
+
22
+ /**
23
+ * Generate a PNG buffer resized to specific dimensions from an SVG or image buffer.
24
+ */
25
+ export async function generatePng(
26
+ source: Buffer | Uint8Array | string,
27
+ options: { width: number; height: number }
28
+ ): Promise<Buffer> {
29
+ return sharp(source)
30
+ .resize(options.width, options.height, {
31
+ fit: "contain",
32
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
33
+ })
34
+ .png()
35
+ .toBuffer()
36
+ }
37
+
38
+ /**
39
+ * Generate a maskable PWA PNG icon with safe-zone padding and solid background. The standard
40
+ * recommendation is to place the master icon in the inner 80% safe zone.
41
+ */
42
+ export async function generateMaskablePng(
43
+ source: Buffer | Uint8Array | string,
44
+ options: GenerateMaskableOptions = {}
45
+ ): Promise<Buffer> {
46
+ const { size = 512, background = "#ffffff", paddingRatio = 0.15 } = options
47
+ const innerSize = Math.round(size * (1 - paddingRatio * 2))
48
+ const pad = Math.floor((size - innerSize) / 2)
49
+
50
+ const innerPng = await sharp(source)
51
+ .resize(innerSize, innerSize, {
52
+ fit: "contain",
53
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
54
+ })
55
+ .png()
56
+ .toBuffer()
57
+
58
+ return sharp(innerPng)
59
+ .extend({
60
+ top: pad,
61
+ bottom: size - innerSize - pad,
62
+ left: pad,
63
+ right: size - innerSize - pad,
64
+ background
65
+ })
66
+ .png()
67
+ .toBuffer()
68
+ }
69
+
70
+ /**
71
+ * Generate a valid .ico buffer containing a 32x32 PNG image according to the Windows ICO
72
+ * specification.
73
+ */
74
+ export async function generateIco(
75
+ source: Buffer | Uint8Array | string,
76
+ size = 32
77
+ ): Promise<Buffer> {
78
+ const pngBuffer = await generatePng(source, { width: size, height: size })
79
+
80
+ const header = Buffer.alloc(22)
81
+ // Reserved (2 bytes)
82
+ header.writeUInt16LE(0, 0)
83
+ // Image type (1 = ICO) (2 bytes)
84
+ header.writeUInt16LE(1, 2)
85
+ // Number of images (2 bytes)
86
+ header.writeUInt16LE(1, 4)
87
+
88
+ // Directory entry (16 bytes)
89
+ header.writeUInt8(size === 256 ? 0 : size, 6) // Width
90
+ header.writeUInt8(size === 256 ? 0 : size, 7) // Height
91
+ header.writeUInt8(0, 8) // Palette size (0 = no palette)
92
+ header.writeUInt8(0, 9) // Reserved
93
+ header.writeUInt16LE(1, 10) // Color planes
94
+ header.writeUInt16LE(32, 12) // Bits per pixel (32bpp)
95
+ header.writeUInt32LE(pngBuffer.length, 14) // Image size in bytes
96
+ header.writeUInt32LE(22, 18) // Offset of image data (header size = 22)
97
+
98
+ return Buffer.concat([header, pngBuffer])
99
+ }
100
+
101
+ /**
102
+ * Generate a standard 1200x630 branded Open Graph card from a master logo and background color.
103
+ */
104
+ export async function generateOgCard(options: GenerateOgCardOptions = {}): Promise<Buffer> {
105
+ const { width = 1200, height = 630, background = "#0a0a0a", logoBuffer } = options
106
+
107
+ if (logoBuffer) {
108
+ const logoSize = Math.min(Math.round(height * 0.45), 280)
109
+ const logoPng = await generatePng(logoBuffer, { width: logoSize, height: logoSize })
110
+ const topPad = Math.floor((height - logoSize) / 2)
111
+ const bottomPad = height - logoSize - topPad
112
+ const leftPad = Math.floor((width - logoSize) / 2)
113
+ const rightPad = width - logoSize - leftPad
114
+
115
+ return sharp(logoPng)
116
+ .extend({
117
+ top: topPad,
118
+ bottom: bottomPad,
119
+ left: leftPad,
120
+ right: rightPad,
121
+ background
122
+ })
123
+ .png()
124
+ .toBuffer()
125
+ }
126
+
127
+ return sharp({
128
+ create: {
129
+ width,
130
+ height,
131
+ channels: 4,
132
+ background
133
+ }
134
+ })
135
+ .png()
136
+ .toBuffer()
137
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from "vite-plus/test"
2
+ import { buildApiCatalog } from "./catalog.js"
3
+
4
+ describe("buildApiCatalog", () => {
5
+ it("builds linkset api-catalog object", () => {
6
+ const catalog = buildApiCatalog({
7
+ items: [
8
+ {
9
+ rel: "self",
10
+ href: "/.well-known/api-catalog",
11
+ type: "application/linkset+json"
12
+ },
13
+ {
14
+ rel: "alternate",
15
+ href: "/sitemap.xml",
16
+ type: "application/xml"
17
+ }
18
+ ]
19
+ })
20
+
21
+ const items = catalog["api-catalog"]
22
+ expect(items).toHaveLength(2)
23
+ expect(items[0]?.rel).toBe("self")
24
+ expect(items[1]?.href).toBe("/sitemap.xml")
25
+ })
26
+ })
package/src/catalog.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { ApiCatalog, ApiCatalogOptions } from "./types.js"
2
+
3
+ /**
4
+ * Build RFC 9652 Application Linkset API Catalog. https://www.rfc-editor.org/rfc/rfc9652
5
+ */
6
+ export function buildApiCatalog(options: ApiCatalogOptions): ApiCatalog {
7
+ const { items } = options
8
+ return {
9
+ "api-catalog": items
10
+ }
11
+ }
@@ -39,7 +39,7 @@ const {
39
39
  // Resolve defaults from config if available
40
40
  const siteName = config?.name || 'Rimelight'
41
41
  const siteDescription = config?.description || ''
42
- const ogImageFallback = config?.seo?.ogImageFallback || config?.ogImage || ''
42
+ const ogImageFallback = config?.seo?.ogImageFallback || config?.ogImage || '/og.png'
43
43
  const themeColor = config?.branding?.colors?.themeColor
44
44
  const favicon = customFavicon || config?.branding?.favicon?.svg || '/favicon.svg'
45
45
  const faviconType = favicon.endsWith('.ico')
package/src/env.d.ts CHANGED
@@ -1,8 +1,56 @@
1
- declare module "*.astro" {
2
- export default {} as import("astro/runtime/server").AstroComponentFactory
3
- }
4
1
 
5
2
  declare module "virtual:rimelight-seo-config" {
6
3
  const config: import("./integration.js").RimelightSeoOptions
7
4
  export default config
8
5
  }
6
+
7
+ declare module "virtual:rimelight-seo/options" {
8
+ const config: import("./integration.js").RimelightSeoOptions
9
+ export default config
10
+ }
11
+
12
+ declare module "virtual:rimelight-seo/config" {
13
+ export const SEO_LOCALES: import("./types.js").LocaleConfig | undefined
14
+ export const PRIVATE_PATH_PREFIXES: string[] | undefined
15
+ export const seoEntries: (() => Promise<import("./types.js").SeoEntry[]>) | undefined
16
+ export const rssEntries: (() => Promise<import("./types.js").RssItem[]>) | undefined
17
+ }
18
+
19
+ declare module "virtual:rimelight-seo/site-config" {
20
+ export const siteConfig: import("./types.js").SiteConfig | Record<string, any>
21
+ }
22
+
23
+ declare module "virtual:rimelight-seo/db" {
24
+ export const db: any
25
+ export const pages: any
26
+ }
27
+
28
+ declare module "virtual:rimelight-seo/corpus" {
29
+ export const corpusPages: any[]
30
+ }
31
+
32
+ declare module "#db/schema" {
33
+ export const pages: any
34
+ }
35
+
36
+ declare module "drizzle-orm" {
37
+ export const and: (...args: any[]) => any
38
+ export const isNull: (column: any) => any
39
+ export const isNotNull: (column: any) => any
40
+ export const sql: any
41
+ export const eq: (column: any, value: any) => any
42
+ }
43
+
44
+ declare module "@rimelight/cms" {
45
+ export const renderCorpusMarkdown: (db: any, options: any) => Promise<string>
46
+ }
47
+
48
+ declare module "astro:content" {
49
+ export const getCollection: (collection: string) => Promise<any[]>
50
+ }
51
+
52
+ declare module "virtual:rimelight-seo/assets" {
53
+ export const masterIconBuffer: Buffer | null
54
+ export const isSvg: boolean
55
+ export const backgroundColor: string
56
+ }