@rimelight/seo 0.0.6 → 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 (66) 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 +6 -2
  6. package/dist/index.mjs +5 -1
  7. package/dist/integration.mjs +182 -0
  8. package/dist/manifest.d.mts +7 -0
  9. package/dist/manifest.mjs +44 -0
  10. package/dist/og.mjs +1 -1
  11. package/dist/routes/api-catalog.d.mts +5 -0
  12. package/dist/routes/api-catalog.mjs +38 -0
  13. package/dist/routes/apple-touch-icon.png.d.mts +5 -0
  14. package/dist/routes/apple-touch-icon.png.mjs +17 -0
  15. package/dist/routes/favicon.ico.d.mts +5 -0
  16. package/dist/routes/favicon.ico.mjs +14 -0
  17. package/dist/routes/favicon.svg.d.mts +5 -0
  18. package/dist/routes/favicon.svg.mjs +12 -0
  19. package/dist/routes/icon-192.png.d.mts +5 -0
  20. package/dist/routes/icon-192.png.mjs +17 -0
  21. package/dist/routes/icon-512.png.d.mts +5 -0
  22. package/dist/routes/icon-512.png.mjs +17 -0
  23. package/dist/routes/icon-maskable-512.png.d.mts +5 -0
  24. package/dist/routes/icon-maskable-512.png.mjs +18 -0
  25. package/dist/routes/llms.txt.mjs +1 -1
  26. package/dist/routes/robots.txt.mjs +1 -1
  27. package/dist/routes/rss.xml.mjs +48 -20
  28. package/dist/routes/security.txt.d.mts +5 -0
  29. package/dist/routes/security.txt.mjs +24 -0
  30. package/dist/routes/site.webmanifest.d.mts +5 -0
  31. package/dist/routes/site.webmanifest.mjs +23 -0
  32. package/dist/routes/sitemap.xml.mjs +1 -1
  33. package/dist/security.d.mts +7 -0
  34. package/dist/security.mjs +34 -0
  35. package/dist/types.d.mts +191 -8
  36. package/package.json +23 -2
  37. package/src/assets.test.ts +70 -0
  38. package/src/assets.ts +137 -0
  39. package/src/catalog.test.ts +26 -0
  40. package/src/catalog.ts +11 -0
  41. package/src/components/SEOHead.astro +1 -1
  42. package/src/env.d.ts +56 -33
  43. package/src/index.ts +18 -1
  44. package/src/integration.ts +353 -172
  45. package/src/manifest.test.ts +41 -0
  46. package/src/manifest.ts +59 -0
  47. package/src/og.ts +1 -9
  48. package/src/routes/api-catalog.ts +55 -0
  49. package/src/routes/apple-touch-icon.png.ts +19 -0
  50. package/src/routes/favicon.ico.ts +19 -0
  51. package/src/routes/favicon.svg.ts +17 -0
  52. package/src/routes/icon-192.png.ts +19 -0
  53. package/src/routes/icon-512.png.ts +19 -0
  54. package/src/routes/icon-maskable-512.png.ts +24 -0
  55. package/src/routes/llms-full.txt.ts +77 -77
  56. package/src/routes/llms.txt.ts +93 -90
  57. package/src/routes/robots.txt.ts +63 -63
  58. package/src/routes/rss.xml.ts +129 -87
  59. package/src/routes/security.txt.ts +30 -0
  60. package/src/routes/site.webmanifest.ts +28 -0
  61. package/src/routes/sitemap.xml.ts +43 -43
  62. package/src/rss.test.ts +28 -28
  63. package/src/rss.ts +63 -63
  64. package/src/security.test.ts +46 -0
  65. package/src/security.ts +78 -0
  66. package/src/types.ts +564 -367
@@ -0,0 +1,39 @@
1
+ //#region src/assets.d.ts
2
+ export interface GeneratePngOptions {
3
+ width: number;
4
+ height: number;
5
+ background?: string;
6
+ }
7
+ export interface GenerateMaskableOptions {
8
+ size?: number;
9
+ background?: string;
10
+ paddingRatio?: number;
11
+ }
12
+ export interface GenerateOgCardOptions {
13
+ width?: number;
14
+ height?: number;
15
+ background?: string;
16
+ logoBuffer?: Buffer | Uint8Array | string | null;
17
+ }
18
+ /**
19
+ * Generate a PNG buffer resized to specific dimensions from an SVG or image buffer.
20
+ */
21
+ export declare function generatePng(source: Buffer | Uint8Array | string, options: {
22
+ width: number;
23
+ height: number;
24
+ }): Promise<Buffer>;
25
+ /**
26
+ * Generate a maskable PWA PNG icon with safe-zone padding and solid background. The standard
27
+ * recommendation is to place the master icon in the inner 80% safe zone.
28
+ */
29
+ export declare function generateMaskablePng(source: Buffer | Uint8Array | string, options?: GenerateMaskableOptions): Promise<Buffer>;
30
+ /**
31
+ * Generate a valid .ico buffer containing a 32x32 PNG image according to the Windows ICO
32
+ * specification.
33
+ */
34
+ export declare function generateIco(source: Buffer | Uint8Array | string, size?: number): Promise<Buffer>;
35
+ /**
36
+ * Generate a standard 1200x630 branded Open Graph card from a master logo and background color.
37
+ */
38
+ export declare function generateOgCard(options?: GenerateOgCardOptions): Promise<Buffer>;
39
+ //#endregion
@@ -0,0 +1,96 @@
1
+ import sharp from "sharp";
2
+ //#region src/assets.ts
3
+ /**
4
+ * Generate a PNG buffer resized to specific dimensions from an SVG or image buffer.
5
+ */
6
+ async function generatePng(source, options) {
7
+ return sharp(source).resize(options.width, options.height, {
8
+ fit: "contain",
9
+ background: {
10
+ r: 0,
11
+ g: 0,
12
+ b: 0,
13
+ alpha: 0
14
+ }
15
+ }).png().toBuffer();
16
+ }
17
+ /**
18
+ * Generate a maskable PWA PNG icon with safe-zone padding and solid background. The standard
19
+ * recommendation is to place the master icon in the inner 80% safe zone.
20
+ */
21
+ async function generateMaskablePng(source, options = {}) {
22
+ const { size = 512, background = "#ffffff", paddingRatio = .15 } = options;
23
+ const innerSize = Math.round(size * (1 - paddingRatio * 2));
24
+ const pad = Math.floor((size - innerSize) / 2);
25
+ const innerPng = await sharp(source).resize(innerSize, innerSize, {
26
+ fit: "contain",
27
+ background: {
28
+ r: 0,
29
+ g: 0,
30
+ b: 0,
31
+ alpha: 0
32
+ }
33
+ }).png().toBuffer();
34
+ return sharp(innerPng).extend({
35
+ top: pad,
36
+ bottom: size - innerSize - pad,
37
+ left: pad,
38
+ right: size - innerSize - pad,
39
+ background
40
+ }).png().toBuffer();
41
+ }
42
+ /**
43
+ * Generate a valid .ico buffer containing a 32x32 PNG image according to the Windows ICO
44
+ * specification.
45
+ */
46
+ async function generateIco(source, size = 32) {
47
+ const pngBuffer = await generatePng(source, {
48
+ width: size,
49
+ height: size
50
+ });
51
+ const header = Buffer.alloc(22);
52
+ header.writeUInt16LE(0, 0);
53
+ header.writeUInt16LE(1, 2);
54
+ header.writeUInt16LE(1, 4);
55
+ header.writeUInt8(size === 256 ? 0 : size, 6);
56
+ header.writeUInt8(size === 256 ? 0 : size, 7);
57
+ header.writeUInt8(0, 8);
58
+ header.writeUInt8(0, 9);
59
+ header.writeUInt16LE(1, 10);
60
+ header.writeUInt16LE(32, 12);
61
+ header.writeUInt32LE(pngBuffer.length, 14);
62
+ header.writeUInt32LE(22, 18);
63
+ return Buffer.concat([header, pngBuffer]);
64
+ }
65
+ /**
66
+ * Generate a standard 1200x630 branded Open Graph card from a master logo and background color.
67
+ */
68
+ async function generateOgCard(options = {}) {
69
+ const { width = 1200, height = 630, background = "#0a0a0a", logoBuffer } = options;
70
+ if (logoBuffer) {
71
+ const logoSize = Math.min(Math.round(height * .45), 280);
72
+ const logoPng = await generatePng(logoBuffer, {
73
+ width: logoSize,
74
+ height: logoSize
75
+ });
76
+ const topPad = Math.floor((height - logoSize) / 2);
77
+ const bottomPad = height - logoSize - topPad;
78
+ const leftPad = Math.floor((width - logoSize) / 2);
79
+ const rightPad = width - logoSize - leftPad;
80
+ return sharp(logoPng).extend({
81
+ top: topPad,
82
+ bottom: bottomPad,
83
+ left: leftPad,
84
+ right: rightPad,
85
+ background
86
+ }).png().toBuffer();
87
+ }
88
+ return sharp({ create: {
89
+ width,
90
+ height,
91
+ channels: 4,
92
+ background
93
+ } }).png().toBuffer();
94
+ }
95
+ //#endregion
96
+ export { generateIco, generateMaskablePng, generateOgCard, generatePng };
@@ -0,0 +1,7 @@
1
+ import { ApiCatalog, ApiCatalogOptions } from "./types.mjs";
2
+ //#region src/catalog.d.ts
3
+ /**
4
+ * Build RFC 9652 Application Linkset API Catalog. https://www.rfc-editor.org/rfc/rfc9652
5
+ */
6
+ export declare function buildApiCatalog(options: ApiCatalogOptions): ApiCatalog;
7
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/catalog.ts
2
+ /**
3
+ * Build RFC 9652 Application Linkset API Catalog. https://www.rfc-editor.org/rfc/rfc9652
4
+ */
5
+ function buildApiCatalog(options) {
6
+ const { items } = options;
7
+ return { "api-catalog": items };
8
+ }
9
+ //#endregion
10
+ export { buildApiCatalog };
package/dist/index.d.mts CHANGED
@@ -1,10 +1,14 @@
1
+ import { generateIco, generateMaskablePng, generateOgCard, generatePng } from "./assets.mjs";
1
2
  import { i as buildBreadcrumbSchema, n as BreadcrumbItem, r as buildArticleSchema, t as ArticleSchemaOptions } from "./schema-C8v69blQ.mjs";
2
3
  import { a as buildLlmsTxt, i as buildLlmsFullTxt, n as LlmsPage, r as LlmsTxtOptions, t as LlmsFullTxtOptions } from "./llms-BVLxzjr-.mjs";
3
- import { ChangeFreq, HeadProps, LlmsFullRouteOptions, LlmsRouteOptions, LocaleConfig, OGImage, OpenGraphMeta, RSSFeed, RimelightSeoOptions, RobotsOptions, RobotsRouteOptions, RssFeedOptions, RssItem, RssOptions, SeoEntry, SiteConfig, SitemapAlternate, SitemapOptions, SitemapUrl, TwitterMeta, UserAgentRule } from "./types.mjs";
4
+ import { ApiCatalog, ApiCatalogItem, ApiCatalogOptions, ApiCatalogRouteOptions, AssetRouteOptions, ChangeFreq, HeadProps, LlmsFullRouteOptions, LlmsRouteOptions, LocaleConfig, ManifestRouteOptions, OGImage, OpenGraphMeta, RSSFeed, RimelightSeoOptions, RobotsOptions, RobotsRouteOptions, RssCollectionConfig, RssFeedOptions, RssItem, RssOptions, SecurityRouteOptions, SecurityTxtOptions, SeoEntry, SiteConfig, SitemapAlternate, SitemapOptions, SitemapUrl, TwitterMeta, UserAgentRule, WebManifest, WebManifestIcon, WebManifestOptions, WebManifestShortcut } from "./types.mjs";
5
+ import { buildApiCatalog } from "./catalog.mjs";
4
6
  import { buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, chunkSitemapUrls } from "./sitemap.mjs";
5
7
  import { buildRobotsTxt } from "./robots.mjs";
6
8
  import { buildRssXml, escapeXml } from "./rss.mjs";
7
9
  import { buildCanonicalUrl, buildPageTitle, buildRobotsMetaContent, buildSafeDescription, buildWebSiteSchema, deepMerge } from "./meta.mjs";
10
+ import { buildSecurityTxt } from "./security.mjs";
11
+ import { buildWebManifest } from "./manifest.mjs";
8
12
  import { OgFontConfig, OgLayoutOptions, OgRenderConfig } from "./og.mjs";
9
13
  import rimelightSeo from "./integration.mjs";
10
- export { type ArticleSchemaOptions, type BreadcrumbItem, type ChangeFreq, type HeadProps, type LlmsFullRouteOptions, type LlmsFullTxtOptions, type LlmsPage, type LlmsRouteOptions, type LlmsTxtOptions, type LocaleConfig, type OGImage, type OgFontConfig, type OgLayoutOptions, type OgRenderConfig, type OpenGraphMeta, type RSSFeed, type RimelightSeoOptions, type RobotsOptions, type RobotsRouteOptions, type RssFeedOptions, type RssItem, type RssOptions, type SeoEntry, type SiteConfig, type SitemapAlternate, type SitemapOptions, type SitemapUrl, type TwitterMeta, type UserAgentRule, buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebSiteSchema, chunkSitemapUrls, deepMerge, rimelightSeo as default, rimelightSeo, escapeXml };
14
+ export { type ApiCatalog, type ApiCatalogItem, type ApiCatalogOptions, type ApiCatalogRouteOptions, type ArticleSchemaOptions, type AssetRouteOptions, type BreadcrumbItem, type ChangeFreq, type HeadProps, type LlmsFullRouteOptions, type LlmsFullTxtOptions, type LlmsPage, type LlmsRouteOptions, type LlmsTxtOptions, type LocaleConfig, type ManifestRouteOptions, type OGImage, type OgFontConfig, type OgLayoutOptions, type OgRenderConfig, type OpenGraphMeta, type RSSFeed, type RimelightSeoOptions, type RobotsOptions, type RobotsRouteOptions, type RssCollectionConfig, type RssFeedOptions, type RssItem, type RssOptions, type SecurityRouteOptions, type SecurityTxtOptions, type SeoEntry, type SiteConfig, type SitemapAlternate, type SitemapOptions, type SitemapUrl, type TwitterMeta, type UserAgentRule, type WebManifest, type WebManifestIcon, type WebManifestOptions, type WebManifestShortcut, buildApiCatalog, buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSecurityTxt, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebManifest, buildWebSiteSchema, chunkSitemapUrls, deepMerge, rimelightSeo as default, rimelightSeo, escapeXml, generateIco, generateMaskablePng, generateOgCard, generatePng };
package/dist/index.mjs CHANGED
@@ -1,11 +1,15 @@
1
+ import { generateIco, generateMaskablePng, generateOgCard, generatePng } from "./assets.mjs";
2
+ import { buildApiCatalog } from "./catalog.mjs";
1
3
  import { buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, chunkSitemapUrls } from "./sitemap.mjs";
2
4
  import { buildRobotsTxt } from "./robots.mjs";
3
5
  import { buildRssXml, escapeXml } from "./rss.mjs";
4
6
  import { buildCanonicalUrl, buildPageTitle, buildRobotsMetaContent, buildSafeDescription, buildWebSiteSchema, deepMerge } from "./meta.mjs";
5
7
  import { buildLlmsFullTxt, buildLlmsTxt } from "./llms.mjs";
8
+ import { buildSecurityTxt } from "./security.mjs";
9
+ import { buildWebManifest } from "./manifest.mjs";
6
10
  import { buildArticleSchema, buildBreadcrumbSchema } from "./schema.mjs";
7
11
  import rimelightSeo from "./integration.mjs";
8
12
  //#region src/index.ts
9
13
  var src_default = rimelightSeo;
10
14
  //#endregion
11
- export { buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebSiteSchema, chunkSitemapUrls, deepMerge, src_default as default, escapeXml, rimelightSeo };
15
+ export { buildApiCatalog, buildArticleSchema, buildBreadcrumbSchema, buildCanonicalUrl, buildLlmsFullTxt, buildLlmsTxt, buildPageTitle, buildRobotsMetaContent, buildRobotsTxt, buildRssXml, buildSafeDescription, buildSecurityTxt, buildSitemapIndexXml, buildSitemapUrls, buildSitemapXml, buildWebManifest, buildWebSiteSchema, chunkSitemapUrls, deepMerge, src_default as default, escapeXml, generateIco, generateMaskablePng, generateOgCard, generatePng, rimelightSeo };
@@ -1,3 +1,4 @@
1
+ import { generateIco, generateMaskablePng, generateOgCard, generatePng } from "./assets.mjs";
1
2
  import path from "node:path";
2
3
  import fs from "node:fs";
3
4
  import { fileURLToPath } from "node:url";
@@ -34,6 +35,25 @@ function rimelightSeo(options = {}) {
34
35
  "src/config/llms.corpus.js",
35
36
  "src/config/llms.corpus.mjs"
36
37
  ]);
38
+ const assetOpts = typeof options.assets === "object" ? options.assets : {};
39
+ const explicitIcon = assetOpts.icon ? path.resolve(rootDir, assetOpts.icon) : null;
40
+ const masterIconPath = explicitIcon && fs.existsSync(explicitIcon) ? explicitIcon : findFile([
41
+ "src/assets/logos/logomark_color.svg",
42
+ "src/assets/logos/logo_color.svg",
43
+ "src/assets/logos/logomark.svg",
44
+ "src/assets/logos/logo.svg",
45
+ "src/assets/icon.svg",
46
+ "src/assets/logo.svg",
47
+ "src/assets/icon.png",
48
+ "public/favicon.svg"
49
+ ]);
50
+ let masterIconBuffer = null;
51
+ let isSvg = false;
52
+ if (masterIconPath && fs.existsSync(masterIconPath)) {
53
+ masterIconBuffer = fs.readFileSync(masterIconPath);
54
+ isSvg = masterIconPath.endsWith(".svg");
55
+ }
56
+ const bgColor = assetOpts.backgroundColor || "#ffffff";
37
57
  if (options.robots !== false) injectRoute({
38
58
  pattern: typeof options.robots === "object" && options.robots.path ? options.robots.path : "/robots.txt",
39
59
  entrypoint: "@rimelight/seo/routes/robots.txt.ts"
@@ -54,7 +74,169 @@ function rimelightSeo(options = {}) {
54
74
  pattern: typeof options.llmsFull === "object" && options.llmsFull.path ? options.llmsFull.path : "/llms-full.txt",
55
75
  entrypoint: "@rimelight/seo/routes/llms-full.txt.ts"
56
76
  });
77
+ if (options.security !== false) {
78
+ const pattern = typeof options.security === "object" && options.security.path ? options.security.path : "/.well-known/security.txt";
79
+ injectRoute({
80
+ pattern,
81
+ entrypoint: "@rimelight/seo/routes/security.txt.ts"
82
+ });
83
+ if ((typeof options.security === "object" ? options.security.legacyRoute !== false : true) && pattern === "/.well-known/security.txt") injectRoute({
84
+ pattern: "/security.txt",
85
+ entrypoint: "@rimelight/seo/routes/security.txt.ts"
86
+ });
87
+ }
88
+ if (options.apiCatalog !== false) injectRoute({
89
+ pattern: typeof options.apiCatalog === "object" && options.apiCatalog.path ? options.apiCatalog.path : "/.well-known/api-catalog",
90
+ entrypoint: "@rimelight/seo/routes/api-catalog.ts"
91
+ });
92
+ if (options.manifest !== false) injectRoute({
93
+ pattern: typeof options.manifest === "object" && options.manifest.path ? options.manifest.path : "/site.webmanifest",
94
+ entrypoint: "@rimelight/seo/routes/site.webmanifest.ts"
95
+ });
57
96
  updateConfig({ vite: { plugins: [{
97
+ name: "vite-plugin-rimelight-seo-assets",
98
+ configureServer(server) {
99
+ if (!masterIconBuffer) return;
100
+ server.middlewares.use(async (req, res, next) => {
101
+ const url = req.url?.split("?")[0];
102
+ if (url === "/favicon.ico" && assetOpts.faviconIco !== false) {
103
+ const ico = await generateIco(masterIconBuffer, 32);
104
+ res.setHeader("Content-Type", "image/x-icon");
105
+ res.setHeader("Cache-Control", "no-cache");
106
+ res.end(ico);
107
+ return;
108
+ }
109
+ if (url === "/apple-touch-icon.png" && assetOpts.appleTouchIcon !== false) {
110
+ const png = await generatePng(masterIconBuffer, {
111
+ width: 180,
112
+ height: 180
113
+ });
114
+ res.setHeader("Content-Type", "image/png");
115
+ res.setHeader("Cache-Control", "no-cache");
116
+ res.end(png);
117
+ return;
118
+ }
119
+ if (url === "/icon-192.png" && assetOpts.icon192 !== false) {
120
+ const png = await generatePng(masterIconBuffer, {
121
+ width: 192,
122
+ height: 192
123
+ });
124
+ res.setHeader("Content-Type", "image/png");
125
+ res.setHeader("Cache-Control", "no-cache");
126
+ res.end(png);
127
+ return;
128
+ }
129
+ if (url === "/icon-512.png" && assetOpts.icon512 !== false) {
130
+ const png = await generatePng(masterIconBuffer, {
131
+ width: 512,
132
+ height: 512
133
+ });
134
+ res.setHeader("Content-Type", "image/png");
135
+ res.setHeader("Cache-Control", "no-cache");
136
+ res.end(png);
137
+ return;
138
+ }
139
+ if (url === "/icon-maskable-512.png" && assetOpts.iconMaskable !== false) {
140
+ const png = await generateMaskablePng(masterIconBuffer, {
141
+ size: 512,
142
+ background: bgColor
143
+ });
144
+ res.setHeader("Content-Type", "image/png");
145
+ res.setHeader("Cache-Control", "no-cache");
146
+ res.end(png);
147
+ return;
148
+ }
149
+ if (url === "/favicon.svg" && isSvg) {
150
+ res.setHeader("Content-Type", "image/svg+xml");
151
+ res.setHeader("Cache-Control", "no-cache");
152
+ res.end(masterIconBuffer);
153
+ return;
154
+ }
155
+ if (url === "/og.png" && assetOpts.ogImage !== false) {
156
+ const og = await generateOgCard({
157
+ logoBuffer: masterIconBuffer,
158
+ background: bgColor
159
+ });
160
+ res.setHeader("Content-Type", "image/png");
161
+ res.setHeader("Cache-Control", "no-cache");
162
+ res.end(og);
163
+ return;
164
+ }
165
+ next();
166
+ });
167
+ },
168
+ async generateBundle() {
169
+ if (options.assets === false || !masterIconBuffer) return;
170
+ if (assetOpts.faviconIco !== false) {
171
+ const ico = await generateIco(masterIconBuffer, 32);
172
+ this.emitFile({
173
+ type: "asset",
174
+ fileName: "favicon.ico",
175
+ source: ico
176
+ });
177
+ }
178
+ if (assetOpts.appleTouchIcon !== false) {
179
+ const png = await generatePng(masterIconBuffer, {
180
+ width: 180,
181
+ height: 180
182
+ });
183
+ this.emitFile({
184
+ type: "asset",
185
+ fileName: "apple-touch-icon.png",
186
+ source: png
187
+ });
188
+ }
189
+ if (assetOpts.icon192 !== false) {
190
+ const png = await generatePng(masterIconBuffer, {
191
+ width: 192,
192
+ height: 192
193
+ });
194
+ this.emitFile({
195
+ type: "asset",
196
+ fileName: "icon-192.png",
197
+ source: png
198
+ });
199
+ }
200
+ if (assetOpts.icon512 !== false) {
201
+ const png = await generatePng(masterIconBuffer, {
202
+ width: 512,
203
+ height: 512
204
+ });
205
+ this.emitFile({
206
+ type: "asset",
207
+ fileName: "icon-512.png",
208
+ source: png
209
+ });
210
+ }
211
+ if (assetOpts.iconMaskable !== false) {
212
+ const png = await generateMaskablePng(masterIconBuffer, {
213
+ size: 512,
214
+ background: bgColor
215
+ });
216
+ this.emitFile({
217
+ type: "asset",
218
+ fileName: "icon-maskable-512.png",
219
+ source: png
220
+ });
221
+ }
222
+ if (assetOpts.ogImage !== false) {
223
+ const og = await generateOgCard({
224
+ logoBuffer: masterIconBuffer,
225
+ background: bgColor
226
+ });
227
+ this.emitFile({
228
+ type: "asset",
229
+ fileName: "og.png",
230
+ source: og
231
+ });
232
+ }
233
+ if (isSvg) this.emitFile({
234
+ type: "asset",
235
+ fileName: "favicon.svg",
236
+ source: masterIconBuffer
237
+ });
238
+ }
239
+ }, {
58
240
  name: "vite-plugin-rimelight-seo",
59
241
  resolveId(id) {
60
242
  if (id === "virtual:rimelight-seo-config" || id === "virtual:rimelight-seo/options") return "\0virtual:rimelight-seo/options";
@@ -0,0 +1,7 @@
1
+ import { WebManifest, WebManifestOptions } from "./types.mjs";
2
+ //#region src/manifest.d.ts
3
+ /**
4
+ * Build web app manifest (site.webmanifest) compliant with W3C Web App Manifest.
5
+ */
6
+ export declare function buildWebManifest(options: WebManifestOptions): WebManifest;
7
+ //#endregion
@@ -0,0 +1,44 @@
1
+ //#region src/manifest.ts
2
+ /**
3
+ * Build web app manifest (site.webmanifest) compliant with W3C Web App Manifest.
4
+ */
5
+ function buildWebManifest(options) {
6
+ const { name, shortName = name, description = "", startUrl = "/", display = "standalone", backgroundColor = "#ffffff", themeColor = "#ffffff", icons = [
7
+ {
8
+ src: "/icon-192.png",
9
+ sizes: "192x192",
10
+ type: "image/png",
11
+ purpose: "any"
12
+ },
13
+ {
14
+ src: "/icon-512.png",
15
+ sizes: "512x512",
16
+ type: "image/png",
17
+ purpose: "any"
18
+ },
19
+ {
20
+ src: "/icon-maskable-512.png",
21
+ sizes: "512x512",
22
+ type: "image/png",
23
+ purpose: "maskable"
24
+ }
25
+ ], shortcuts, categories, lang, dir, orientation, scope } = options;
26
+ return {
27
+ name,
28
+ short_name: shortName,
29
+ ...description ? { description } : {},
30
+ start_url: startUrl,
31
+ display,
32
+ background_color: backgroundColor,
33
+ theme_color: themeColor,
34
+ icons,
35
+ ...shortcuts ? { shortcuts } : {},
36
+ ...categories ? { categories } : {},
37
+ ...lang ? { lang } : {},
38
+ ...dir ? { dir } : {},
39
+ ...orientation ? { orientation } : {},
40
+ ...scope ? { scope } : {}
41
+ };
42
+ }
43
+ //#endregion
44
+ export { buildWebManifest };
package/dist/og.mjs CHANGED
@@ -45,7 +45,7 @@ async function loadOgFonts(config = NOTO_SANS, timeoutMs = 5e3) {
45
45
  * `renderOgResponse()` directly -- this function is just the house style.
46
46
  */
47
47
  function defaultOgLayout(options, font = NOTO_SANS) {
48
- const { title = "", description, brand, badge, date, background = "dark", preview = false } = options;
48
+ const { title, description, brand, badge, date, background = "dark", preview = false } = options;
49
49
  const MAX_TITLE_LEN = 160;
50
50
  const MAX_DESC_LEN = 240;
51
51
  const safeTitle = title.length > MAX_TITLE_LEN ? title.slice(0, 159) + "…" : title;
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/api-catalog.d.ts
3
+ export declare const prerender = false;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,38 @@
1
+ import { buildApiCatalog } from "../catalog.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
3
+ //#region src/routes/api-catalog.ts
4
+ const prerender = false;
5
+ const GET = () => {
6
+ const catalogOpts = typeof seoOptions?.apiCatalog === "object" ? seoOptions.apiCatalog : {};
7
+ const defaultItems = [{
8
+ rel: "self",
9
+ href: "/.well-known/api-catalog",
10
+ type: "application/linkset+json"
11
+ }];
12
+ if (seoOptions?.sitemap !== false) defaultItems.push({
13
+ rel: "alternate",
14
+ href: "/sitemap.xml",
15
+ type: "application/xml",
16
+ title: "Sitemap"
17
+ });
18
+ if (seoOptions?.rss !== false) defaultItems.push({
19
+ rel: "alternate",
20
+ href: "/rss.xml",
21
+ type: "application/rss+xml",
22
+ title: "RSS Feed"
23
+ });
24
+ if (seoOptions?.llms !== false) defaultItems.push({
25
+ rel: "llms",
26
+ href: "/llms.txt",
27
+ type: "text/plain",
28
+ title: "LLMs.txt"
29
+ });
30
+ const items = catalogOpts.items || defaultItems;
31
+ const catalog = buildApiCatalog({ items });
32
+ return new Response(JSON.stringify(catalog, null, 2), { headers: {
33
+ "Content-Type": "application/linkset+json; charset=utf-8",
34
+ "Cache-Control": "public, max-age=86400, stale-while-revalidate=604800"
35
+ } });
36
+ };
37
+ //#endregion
38
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/apple-touch-icon.png.d.ts
3
+ export declare const prerender = true;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,17 @@
1
+ import { generatePng } from "../assets.mjs";
2
+ import { masterIconBuffer } from "virtual:rimelight-seo/assets";
3
+ //#region src/routes/apple-touch-icon.png.ts
4
+ const prerender = true;
5
+ const GET = async () => {
6
+ if (!masterIconBuffer) return new Response(null, { status: 404 });
7
+ const png = await generatePng(masterIconBuffer, {
8
+ width: 180,
9
+ height: 180
10
+ });
11
+ return new Response(new Uint8Array(png), { headers: {
12
+ "Content-Type": "image/png",
13
+ "Cache-Control": "public, max-age=31536000, immutable"
14
+ } });
15
+ };
16
+ //#endregion
17
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/favicon.ico.d.ts
3
+ export declare const prerender = true;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,14 @@
1
+ import { generateIco } from "../assets.mjs";
2
+ import { masterIconBuffer } from "virtual:rimelight-seo/assets";
3
+ //#region src/routes/favicon.ico.ts
4
+ const prerender = true;
5
+ const GET = async () => {
6
+ if (!masterIconBuffer) return new Response(null, { status: 404 });
7
+ const ico = await generateIco(masterIconBuffer, 32);
8
+ return new Response(new Uint8Array(ico), { headers: {
9
+ "Content-Type": "image/x-icon",
10
+ "Cache-Control": "public, max-age=31536000, immutable"
11
+ } });
12
+ };
13
+ //#endregion
14
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/favicon.svg.d.ts
3
+ export declare const prerender = true;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,12 @@
1
+ import { isSvg, masterIconBuffer } from "virtual:rimelight-seo/assets";
2
+ //#region src/routes/favicon.svg.ts
3
+ const prerender = true;
4
+ const GET = async () => {
5
+ if (!masterIconBuffer || !isSvg) return new Response(null, { status: 404 });
6
+ return new Response(new Uint8Array(masterIconBuffer), { headers: {
7
+ "Content-Type": "image/svg+xml",
8
+ "Cache-Control": "public, max-age=31536000, immutable"
9
+ } });
10
+ };
11
+ //#endregion
12
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/icon-192.png.d.ts
3
+ export declare const prerender = true;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,17 @@
1
+ import { generatePng } from "../assets.mjs";
2
+ import { masterIconBuffer } from "virtual:rimelight-seo/assets";
3
+ //#region src/routes/icon-192.png.ts
4
+ const prerender = true;
5
+ const GET = async () => {
6
+ if (!masterIconBuffer) return new Response(null, { status: 404 });
7
+ const png = await generatePng(masterIconBuffer, {
8
+ width: 192,
9
+ height: 192
10
+ });
11
+ return new Response(new Uint8Array(png), { headers: {
12
+ "Content-Type": "image/png",
13
+ "Cache-Control": "public, max-age=31536000, immutable"
14
+ } });
15
+ };
16
+ //#endregion
17
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/icon-512.png.d.ts
3
+ export declare const prerender = true;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,17 @@
1
+ import { generatePng } from "../assets.mjs";
2
+ import { masterIconBuffer } from "virtual:rimelight-seo/assets";
3
+ //#region src/routes/icon-512.png.ts
4
+ const prerender = true;
5
+ const GET = async () => {
6
+ if (!masterIconBuffer) return new Response(null, { status: 404 });
7
+ const png = await generatePng(masterIconBuffer, {
8
+ width: 512,
9
+ height: 512
10
+ });
11
+ return new Response(new Uint8Array(png), { headers: {
12
+ "Content-Type": "image/png",
13
+ "Cache-Control": "public, max-age=31536000, immutable"
14
+ } });
15
+ };
16
+ //#endregion
17
+ export { GET, prerender };
@@ -0,0 +1,5 @@
1
+ import { APIRoute } from "astro";
2
+ //#region src/routes/icon-maskable-512.png.d.ts
3
+ export declare const prerender = true;
4
+ export declare const GET: APIRoute;
5
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import { generateMaskablePng } from "../assets.mjs";
2
+ import { backgroundColor, masterIconBuffer } from "virtual:rimelight-seo/assets";
3
+ //#region src/routes/icon-maskable-512.png.ts
4
+ const prerender = true;
5
+ const GET = async () => {
6
+ if (!masterIconBuffer) return new Response(null, { status: 404 });
7
+ const png = await generateMaskablePng(masterIconBuffer, {
8
+ size: 512,
9
+ background: backgroundColor || "#ffffff",
10
+ paddingRatio: .15
11
+ });
12
+ return new Response(new Uint8Array(png), { headers: {
13
+ "Content-Type": "image/png",
14
+ "Cache-Control": "public, max-age=31536000, immutable"
15
+ } });
16
+ };
17
+ //#endregion
18
+ export { GET, prerender };
@@ -1,8 +1,8 @@
1
1
  import { buildLlmsTxt } from "../llms.mjs";
2
+ import seoOptions from "virtual:rimelight-seo/options";
2
3
  import * as siteConfigMod from "virtual:rimelight-seo/site-config";
3
4
  import * as dbMod from "virtual:rimelight-seo/db";
4
5
  import * as corpusMod from "virtual:rimelight-seo/corpus";
5
- import seoOptions from "virtual:rimelight-seo/options";
6
6
  //#region src/routes/llms.txt.ts
7
7
  const prerender = false;
8
8
  const GET = async ({ site, url }) => {