@rimelight/seo 0.0.6 → 0.0.8

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 (67) 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 +194 -3
  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 +30 -7
  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 +55 -33
  43. package/src/index.ts +18 -1
  44. package/src/integration.ts +365 -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
  67. package/LICENSE +0 -21
@@ -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";
@@ -19,6 +20,11 @@ function rimelightSeo(options = {}) {
19
20
  "src/config/seo.config.js",
20
21
  "src/config/seo.config.mjs"
21
22
  ]);
23
+ const hasConfigExport = (name) => {
24
+ if (!seoConfigPath) return false;
25
+ const content = fs.readFileSync(seoConfigPath, "utf8");
26
+ return new RegExp(`export\\s+(?:const|let|var|async\\s+function|function|class)\\s+${name}\\b`).test(content) || new RegExp(`export\\s*\\{[^}]*\\b${name}\\b}`).test(content);
27
+ };
22
28
  const siteConfigPath = findFile([
23
29
  "src/config/site.config.ts",
24
30
  "src/config/site.config.js",
@@ -34,6 +40,25 @@ function rimelightSeo(options = {}) {
34
40
  "src/config/llms.corpus.js",
35
41
  "src/config/llms.corpus.mjs"
36
42
  ]);
43
+ const assetOpts = typeof options.assets === "object" ? options.assets : {};
44
+ const explicitIcon = assetOpts.icon ? path.resolve(rootDir, assetOpts.icon) : null;
45
+ const masterIconPath = explicitIcon && fs.existsSync(explicitIcon) ? explicitIcon : findFile([
46
+ "src/assets/logos/logomark_color.svg",
47
+ "src/assets/logos/logo_color.svg",
48
+ "src/assets/logos/logomark.svg",
49
+ "src/assets/logos/logo.svg",
50
+ "src/assets/icon.svg",
51
+ "src/assets/logo.svg",
52
+ "src/assets/icon.png",
53
+ "public/favicon.svg"
54
+ ]);
55
+ let masterIconBuffer = null;
56
+ let isSvg = false;
57
+ if (masterIconPath && fs.existsSync(masterIconPath)) {
58
+ masterIconBuffer = fs.readFileSync(masterIconPath);
59
+ isSvg = masterIconPath.endsWith(".svg");
60
+ }
61
+ const bgColor = assetOpts.backgroundColor || "#ffffff";
37
62
  if (options.robots !== false) injectRoute({
38
63
  pattern: typeof options.robots === "object" && options.robots.path ? options.robots.path : "/robots.txt",
39
64
  entrypoint: "@rimelight/seo/routes/robots.txt.ts"
@@ -54,7 +79,169 @@ function rimelightSeo(options = {}) {
54
79
  pattern: typeof options.llmsFull === "object" && options.llmsFull.path ? options.llmsFull.path : "/llms-full.txt",
55
80
  entrypoint: "@rimelight/seo/routes/llms-full.txt.ts"
56
81
  });
82
+ if (options.security !== false) {
83
+ const pattern = typeof options.security === "object" && options.security.path ? options.security.path : "/.well-known/security.txt";
84
+ injectRoute({
85
+ pattern,
86
+ entrypoint: "@rimelight/seo/routes/security.txt.ts"
87
+ });
88
+ if ((typeof options.security === "object" ? options.security.legacyRoute !== false : true) && pattern === "/.well-known/security.txt") injectRoute({
89
+ pattern: "/security.txt",
90
+ entrypoint: "@rimelight/seo/routes/security.txt.ts"
91
+ });
92
+ }
93
+ if (options.apiCatalog !== false) injectRoute({
94
+ pattern: typeof options.apiCatalog === "object" && options.apiCatalog.path ? options.apiCatalog.path : "/.well-known/api-catalog",
95
+ entrypoint: "@rimelight/seo/routes/api-catalog.ts"
96
+ });
97
+ if (options.manifest !== false) injectRoute({
98
+ pattern: typeof options.manifest === "object" && options.manifest.path ? options.manifest.path : "/site.webmanifest",
99
+ entrypoint: "@rimelight/seo/routes/site.webmanifest.ts"
100
+ });
57
101
  updateConfig({ vite: { plugins: [{
102
+ name: "vite-plugin-rimelight-seo-assets",
103
+ configureServer(server) {
104
+ if (!masterIconBuffer) return;
105
+ server.middlewares.use(async (req, res, next) => {
106
+ const url = req.url?.split("?")[0];
107
+ if (url === "/favicon.ico" && assetOpts.faviconIco !== false) {
108
+ const ico = await generateIco(masterIconBuffer, 32);
109
+ res.setHeader("Content-Type", "image/x-icon");
110
+ res.setHeader("Cache-Control", "no-cache");
111
+ res.end(ico);
112
+ return;
113
+ }
114
+ if (url === "/apple-touch-icon.png" && assetOpts.appleTouchIcon !== false) {
115
+ const png = await generatePng(masterIconBuffer, {
116
+ width: 180,
117
+ height: 180
118
+ });
119
+ res.setHeader("Content-Type", "image/png");
120
+ res.setHeader("Cache-Control", "no-cache");
121
+ res.end(png);
122
+ return;
123
+ }
124
+ if (url === "/icon-192.png" && assetOpts.icon192 !== false) {
125
+ const png = await generatePng(masterIconBuffer, {
126
+ width: 192,
127
+ height: 192
128
+ });
129
+ res.setHeader("Content-Type", "image/png");
130
+ res.setHeader("Cache-Control", "no-cache");
131
+ res.end(png);
132
+ return;
133
+ }
134
+ if (url === "/icon-512.png" && assetOpts.icon512 !== false) {
135
+ const png = await generatePng(masterIconBuffer, {
136
+ width: 512,
137
+ height: 512
138
+ });
139
+ res.setHeader("Content-Type", "image/png");
140
+ res.setHeader("Cache-Control", "no-cache");
141
+ res.end(png);
142
+ return;
143
+ }
144
+ if (url === "/icon-maskable-512.png" && assetOpts.iconMaskable !== false) {
145
+ const png = await generateMaskablePng(masterIconBuffer, {
146
+ size: 512,
147
+ background: bgColor
148
+ });
149
+ res.setHeader("Content-Type", "image/png");
150
+ res.setHeader("Cache-Control", "no-cache");
151
+ res.end(png);
152
+ return;
153
+ }
154
+ if (url === "/favicon.svg" && isSvg) {
155
+ res.setHeader("Content-Type", "image/svg+xml");
156
+ res.setHeader("Cache-Control", "no-cache");
157
+ res.end(masterIconBuffer);
158
+ return;
159
+ }
160
+ if (url === "/og.png" && assetOpts.ogImage !== false) {
161
+ const og = await generateOgCard({
162
+ logoBuffer: masterIconBuffer,
163
+ background: bgColor
164
+ });
165
+ res.setHeader("Content-Type", "image/png");
166
+ res.setHeader("Cache-Control", "no-cache");
167
+ res.end(og);
168
+ return;
169
+ }
170
+ next();
171
+ });
172
+ },
173
+ async generateBundle() {
174
+ if (options.assets === false || !masterIconBuffer) return;
175
+ if (assetOpts.faviconIco !== false) {
176
+ const ico = await generateIco(masterIconBuffer, 32);
177
+ this.emitFile({
178
+ type: "asset",
179
+ fileName: "favicon.ico",
180
+ source: ico
181
+ });
182
+ }
183
+ if (assetOpts.appleTouchIcon !== false) {
184
+ const png = await generatePng(masterIconBuffer, {
185
+ width: 180,
186
+ height: 180
187
+ });
188
+ this.emitFile({
189
+ type: "asset",
190
+ fileName: "apple-touch-icon.png",
191
+ source: png
192
+ });
193
+ }
194
+ if (assetOpts.icon192 !== false) {
195
+ const png = await generatePng(masterIconBuffer, {
196
+ width: 192,
197
+ height: 192
198
+ });
199
+ this.emitFile({
200
+ type: "asset",
201
+ fileName: "icon-192.png",
202
+ source: png
203
+ });
204
+ }
205
+ if (assetOpts.icon512 !== false) {
206
+ const png = await generatePng(masterIconBuffer, {
207
+ width: 512,
208
+ height: 512
209
+ });
210
+ this.emitFile({
211
+ type: "asset",
212
+ fileName: "icon-512.png",
213
+ source: png
214
+ });
215
+ }
216
+ if (assetOpts.iconMaskable !== false) {
217
+ const png = await generateMaskablePng(masterIconBuffer, {
218
+ size: 512,
219
+ background: bgColor
220
+ });
221
+ this.emitFile({
222
+ type: "asset",
223
+ fileName: "icon-maskable-512.png",
224
+ source: png
225
+ });
226
+ }
227
+ if (assetOpts.ogImage !== false) {
228
+ const og = await generateOgCard({
229
+ logoBuffer: masterIconBuffer,
230
+ background: bgColor
231
+ });
232
+ this.emitFile({
233
+ type: "asset",
234
+ fileName: "og.png",
235
+ source: og
236
+ });
237
+ }
238
+ if (isSvg) this.emitFile({
239
+ type: "asset",
240
+ fileName: "favicon.svg",
241
+ source: masterIconBuffer
242
+ });
243
+ }
244
+ }, {
58
245
  name: "vite-plugin-rimelight-seo",
59
246
  resolveId(id) {
60
247
  if (id === "virtual:rimelight-seo-config" || id === "virtual:rimelight-seo/options") return "\0virtual:rimelight-seo/options";
@@ -67,12 +254,16 @@ function rimelightSeo(options = {}) {
67
254
  load(id) {
68
255
  if (id === "\0virtual:rimelight-seo/options") return `export default ${JSON.stringify(options)};`;
69
256
  if (id === "\0virtual:rimelight-seo/config") {
70
- if (seoConfigPath) return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
257
+ if (seoConfigPath) {
258
+ const seoEntriesExport = hasConfigExport("seoEntries");
259
+ const rssEntriesExport = hasConfigExport("rssEntries");
260
+ return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
71
261
  export const SEO_LOCALES = customConfig.SEO_LOCALES;
72
262
  export const PRIVATE_PATH_PREFIXES = customConfig.PRIVATE_PATH_PREFIXES || [];
73
- export const seoEntries = customConfig.seoEntries;
74
- export const rssEntries = customConfig.rssEntries;
263
+ export const seoEntries = ${seoEntriesExport ? "customConfig.seoEntries" : "undefined"};
264
+ export const rssEntries = ${rssEntriesExport ? "customConfig.rssEntries" : "undefined"};
75
265
  export default customConfig;`;
266
+ }
76
267
  return `export const SEO_LOCALES = undefined;
77
268
  export const PRIVATE_PATH_PREFIXES = [];
78
269
  export const seoEntries = async () => [];
@@ -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