@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/src/index.ts CHANGED
@@ -2,21 +2,41 @@ export type {
2
2
  ChangeFreq,
3
3
  LocaleConfig,
4
4
  RobotsOptions,
5
+ RobotsRouteOptions,
5
6
  SeoEntry,
6
7
  SitemapAlternate,
7
8
  SitemapUrl,
9
+ SitemapOptions,
8
10
  UserAgentRule,
9
11
  SiteConfig,
10
12
  OGImage,
11
13
  RSSFeed,
14
+ RssItem,
15
+ RssFeedOptions,
16
+ RssOptions,
17
+ RssCollectionConfig,
12
18
  HeadProps,
13
19
  OpenGraphMeta,
14
20
  TwitterMeta,
15
21
  LlmsPage,
16
22
  LlmsTxtOptions,
17
23
  LlmsFullTxtOptions,
24
+ LlmsRouteOptions,
25
+ LlmsFullRouteOptions,
18
26
  ArticleSchemaOptions,
19
- BreadcrumbItem
27
+ BreadcrumbItem,
28
+ SecurityTxtOptions,
29
+ SecurityRouteOptions,
30
+ WebManifestIcon,
31
+ WebManifestShortcut,
32
+ WebManifest,
33
+ WebManifestOptions,
34
+ ManifestRouteOptions,
35
+ ApiCatalogItem,
36
+ ApiCatalog,
37
+ ApiCatalogOptions,
38
+ ApiCatalogRouteOptions,
39
+ AssetRouteOptions
20
40
  } from "./types.js"
21
41
 
22
42
  export {
@@ -28,6 +48,8 @@ export {
28
48
 
29
49
  export { buildRobotsTxt } from "./robots.js"
30
50
 
51
+ export { buildRssXml, escapeXml } from "./rss.js"
52
+
31
53
  export {
32
54
  buildCanonicalUrl,
33
55
  buildPageTitle,
@@ -38,5 +60,14 @@ export {
38
60
  } from "./meta.js"
39
61
 
40
62
  export { buildLlmsTxt, buildLlmsFullTxt } from "./llms.js"
63
+ export { buildSecurityTxt } from "./security.js"
64
+ export { buildWebManifest } from "./manifest.js"
65
+ export { buildApiCatalog } from "./catalog.js"
41
66
  export { buildArticleSchema, buildBreadcrumbSchema } from "./schema.js"
42
- export { rimelightSeo, type RimelightSeoOptions } from "./integration.js"
67
+ export { generatePng, generateMaskablePng, generateIco, generateOgCard } from "./assets.js"
68
+ export type { OgFontConfig, OgLayoutOptions, OgRenderConfig } from "./og.js"
69
+
70
+ import { rimelightSeo } from "./integration.js"
71
+ export type { RimelightSeoOptions } from "./integration.js"
72
+ export { rimelightSeo }
73
+ export default rimelightSeo
@@ -1,67 +1,344 @@
1
1
  import type { AstroIntegration } from "astro"
2
- import type { RobotsOptions, SitemapUrl, LlmsTxtOptions } from "./types.js"
3
-
4
- export interface RimelightSeoOptions {
5
- /**
6
- * Sitemap configuration or false to disable automatic /sitemap.xml
7
- */
8
- sitemap?:
9
- | boolean
10
- | {
11
- urls?: SitemapUrl[]
12
- [key: string]: any
13
- }
14
- /**
15
- * Robots.txt configuration or false to disable automatic /robots.txt
16
- */
17
- robots?: boolean | RobotsOptions
18
- /**
19
- * LLMs.txt configuration or false to disable automatic /llms.txt
20
- */
21
- llms?: boolean | LlmsTxtOptions
22
- }
2
+ import path from "node:path"
3
+ import fs from "node:fs"
4
+ import { fileURLToPath } from "node:url"
5
+ import type { RimelightSeoOptions } from "./types.js"
6
+ import { generateIco, generatePng, generateMaskablePng, generateOgCard } from "./assets.js"
7
+
8
+ export { type RimelightSeoOptions } from "./types.js"
23
9
 
24
10
  export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegration {
25
11
  return {
26
12
  name: "@rimelight/seo",
27
13
  hooks: {
28
- "astro:config:setup": ({ injectRoute, updateConfig }) => {
14
+ "astro:config:setup": ({ injectRoute, updateConfig, config }) => {
15
+ const rootDir = fileURLToPath(config.root)
16
+
17
+ // Resolve site files
18
+ const findFile = (candidates: string[]): string | null => {
19
+ for (const c of candidates) {
20
+ const resolved = path.resolve(rootDir, c)
21
+ if (fs.existsSync(resolved)) return resolved
22
+ }
23
+ return null
24
+ }
25
+
26
+ const seoConfigPath = findFile([
27
+ "src/config/seo.config.ts",
28
+ "src/config/seo.config.js",
29
+ "src/config/seo.config.mjs"
30
+ ])
31
+
32
+ const siteConfigPath = findFile([
33
+ "src/config/site.config.ts",
34
+ "src/config/site.config.js",
35
+ "src/config/site.config.mjs"
36
+ ])
37
+
38
+ const dbPath = findFile(["src/db/index.ts", "src/db/index.js", "src/db/index.mjs"])
39
+
40
+ const corpusPath = findFile([
41
+ "src/config/llms.corpus.ts",
42
+ "src/config/llms.corpus.js",
43
+ "src/config/llms.corpus.mjs"
44
+ ])
45
+
46
+ // Master Icon Discovery for dynamic public asset generation
47
+ const assetOpts = typeof options.assets === "object" ? options.assets : {}
48
+ const explicitIcon = assetOpts.icon ? path.resolve(rootDir, assetOpts.icon) : null
49
+ const masterIconPath =
50
+ explicitIcon && fs.existsSync(explicitIcon)
51
+ ? explicitIcon
52
+ : findFile([
53
+ "src/assets/logos/logomark_color.svg",
54
+ "src/assets/logos/logo_color.svg",
55
+ "src/assets/logos/logomark.svg",
56
+ "src/assets/logos/logo.svg",
57
+ "src/assets/icon.svg",
58
+ "src/assets/logo.svg",
59
+ "src/assets/icon.png",
60
+ "public/favicon.svg"
61
+ ])
62
+
63
+ let masterIconBuffer: Buffer | null = null
64
+ let isSvg = false
65
+ if (masterIconPath && fs.existsSync(masterIconPath)) {
66
+ masterIconBuffer = fs.readFileSync(masterIconPath)
67
+ isSvg = masterIconPath.endsWith(".svg")
68
+ }
69
+
70
+ const bgColor = assetOpts.backgroundColor || "#ffffff"
71
+
72
+ // Inject standard routes
29
73
  if (options.robots !== false) {
74
+ const pattern =
75
+ typeof options.robots === "object" && options.robots.path
76
+ ? options.robots.path
77
+ : "/robots.txt"
30
78
  injectRoute({
31
- pattern: "/robots.txt",
79
+ pattern,
32
80
  entrypoint: "@rimelight/seo/routes/robots.txt.ts"
33
81
  })
34
82
  }
35
83
 
36
84
  if (options.sitemap !== false) {
85
+ const pattern =
86
+ typeof options.sitemap === "object" && options.sitemap.path
87
+ ? options.sitemap.path
88
+ : "/sitemap.xml"
37
89
  injectRoute({
38
- pattern: "/sitemap.xml",
90
+ pattern,
39
91
  entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
40
92
  })
41
93
  }
42
94
 
95
+ if (options.rss !== false) {
96
+ const pattern =
97
+ typeof options.rss === "object" && options.rss.path ? options.rss.path : "/rss.xml"
98
+ injectRoute({
99
+ pattern,
100
+ entrypoint: "@rimelight/seo/routes/rss.xml.ts"
101
+ })
102
+ }
103
+
43
104
  if (options.llms !== false) {
105
+ const pattern =
106
+ typeof options.llms === "object" && options.llms.path ? options.llms.path : "/llms.txt"
44
107
  injectRoute({
45
- pattern: "/llms.txt",
108
+ pattern,
46
109
  entrypoint: "@rimelight/seo/routes/llms.txt.ts"
47
110
  })
48
111
  }
49
112
 
113
+ if (options.llmsFull !== false) {
114
+ const pattern =
115
+ typeof options.llmsFull === "object" && options.llmsFull.path
116
+ ? options.llmsFull.path
117
+ : "/llms-full.txt"
118
+ injectRoute({
119
+ pattern,
120
+ entrypoint: "@rimelight/seo/routes/llms-full.txt.ts"
121
+ })
122
+ }
123
+
124
+ if (options.security !== false) {
125
+ const pattern =
126
+ typeof options.security === "object" && options.security.path
127
+ ? options.security.path
128
+ : "/.well-known/security.txt"
129
+ injectRoute({
130
+ pattern,
131
+ entrypoint: "@rimelight/seo/routes/security.txt.ts"
132
+ })
133
+
134
+ const legacyRoute =
135
+ typeof options.security === "object" ? options.security.legacyRoute !== false : true
136
+ if (legacyRoute && pattern === "/.well-known/security.txt") {
137
+ injectRoute({
138
+ pattern: "/security.txt",
139
+ entrypoint: "@rimelight/seo/routes/security.txt.ts"
140
+ })
141
+ }
142
+ }
143
+
144
+ if (options.apiCatalog !== false) {
145
+ const pattern =
146
+ typeof options.apiCatalog === "object" && options.apiCatalog.path
147
+ ? options.apiCatalog.path
148
+ : "/.well-known/api-catalog"
149
+ injectRoute({
150
+ pattern,
151
+ entrypoint: "@rimelight/seo/routes/api-catalog.ts"
152
+ })
153
+ }
154
+
155
+ if (options.manifest !== false) {
156
+ const pattern =
157
+ typeof options.manifest === "object" && options.manifest.path
158
+ ? options.manifest.path
159
+ : "/site.webmanifest"
160
+ injectRoute({
161
+ pattern,
162
+ entrypoint: "@rimelight/seo/routes/site.webmanifest.ts"
163
+ })
164
+ }
165
+
50
166
  updateConfig({
51
167
  vite: {
52
168
  plugins: [
169
+ {
170
+ name: "vite-plugin-rimelight-seo-assets",
171
+ configureServer(server) {
172
+ if (!masterIconBuffer) return
173
+ server.middlewares.use(async (req, res, next) => {
174
+ const url = req.url?.split("?")[0]
175
+ if (url === "/favicon.ico" && assetOpts.faviconIco !== false) {
176
+ const ico = await generateIco(masterIconBuffer, 32)
177
+ res.setHeader("Content-Type", "image/x-icon")
178
+ res.setHeader("Cache-Control", "no-cache")
179
+ res.end(ico)
180
+ return
181
+ }
182
+ if (url === "/apple-touch-icon.png" && assetOpts.appleTouchIcon !== false) {
183
+ const png = await generatePng(masterIconBuffer, { width: 180, height: 180 })
184
+ res.setHeader("Content-Type", "image/png")
185
+ res.setHeader("Cache-Control", "no-cache")
186
+ res.end(png)
187
+ return
188
+ }
189
+ if (url === "/icon-192.png" && assetOpts.icon192 !== false) {
190
+ const png = await generatePng(masterIconBuffer, { width: 192, height: 192 })
191
+ res.setHeader("Content-Type", "image/png")
192
+ res.setHeader("Cache-Control", "no-cache")
193
+ res.end(png)
194
+ return
195
+ }
196
+ if (url === "/icon-512.png" && assetOpts.icon512 !== false) {
197
+ const png = await generatePng(masterIconBuffer, { width: 512, height: 512 })
198
+ res.setHeader("Content-Type", "image/png")
199
+ res.setHeader("Cache-Control", "no-cache")
200
+ res.end(png)
201
+ return
202
+ }
203
+ if (url === "/icon-maskable-512.png" && assetOpts.iconMaskable !== false) {
204
+ const png = await generateMaskablePng(masterIconBuffer, {
205
+ size: 512,
206
+ background: bgColor
207
+ })
208
+ res.setHeader("Content-Type", "image/png")
209
+ res.setHeader("Cache-Control", "no-cache")
210
+ res.end(png)
211
+ return
212
+ }
213
+ if (url === "/favicon.svg" && isSvg) {
214
+ res.setHeader("Content-Type", "image/svg+xml")
215
+ res.setHeader("Cache-Control", "no-cache")
216
+ res.end(masterIconBuffer)
217
+ return
218
+ }
219
+ if (url === "/og.png" && assetOpts.ogImage !== false) {
220
+ const og = await generateOgCard({
221
+ logoBuffer: masterIconBuffer,
222
+ background: bgColor
223
+ })
224
+ res.setHeader("Content-Type", "image/png")
225
+ res.setHeader("Cache-Control", "no-cache")
226
+ res.end(og)
227
+ return
228
+ }
229
+ next()
230
+ })
231
+ },
232
+ async generateBundle() {
233
+ if (options.assets === false || !masterIconBuffer) return
234
+
235
+ if (assetOpts.faviconIco !== false) {
236
+ const ico = await generateIco(masterIconBuffer, 32)
237
+ this.emitFile({ type: "asset", fileName: "favicon.ico", source: ico })
238
+ }
239
+ if (assetOpts.appleTouchIcon !== false) {
240
+ const png = await generatePng(masterIconBuffer, { width: 180, height: 180 })
241
+ this.emitFile({ type: "asset", fileName: "apple-touch-icon.png", source: png })
242
+ }
243
+ if (assetOpts.icon192 !== false) {
244
+ const png = await generatePng(masterIconBuffer, { width: 192, height: 192 })
245
+ this.emitFile({ type: "asset", fileName: "icon-192.png", source: png })
246
+ }
247
+ if (assetOpts.icon512 !== false) {
248
+ const png = await generatePng(masterIconBuffer, { width: 512, height: 512 })
249
+ this.emitFile({ type: "asset", fileName: "icon-512.png", source: png })
250
+ }
251
+ if (assetOpts.iconMaskable !== false) {
252
+ const png = await generateMaskablePng(masterIconBuffer, {
253
+ size: 512,
254
+ background: bgColor
255
+ })
256
+ this.emitFile({
257
+ type: "asset",
258
+ fileName: "icon-maskable-512.png",
259
+ source: png
260
+ })
261
+ }
262
+ if (assetOpts.ogImage !== false) {
263
+ const og = await generateOgCard({
264
+ logoBuffer: masterIconBuffer,
265
+ background: bgColor
266
+ })
267
+ this.emitFile({
268
+ type: "asset",
269
+ fileName: "og.png",
270
+ source: og
271
+ })
272
+ }
273
+ if (isSvg) {
274
+ this.emitFile({
275
+ type: "asset",
276
+ fileName: "favicon.svg",
277
+ source: masterIconBuffer
278
+ })
279
+ }
280
+ }
281
+ },
53
282
  {
54
283
  name: "vite-plugin-rimelight-seo",
55
284
  resolveId(id) {
56
- if (id === "virtual:rimelight-seo-config") {
57
- return "\0virtual:rimelight-seo-config"
285
+ if (
286
+ id === "virtual:rimelight-seo-config" ||
287
+ id === "virtual:rimelight-seo/options"
288
+ ) {
289
+ return "\0virtual:rimelight-seo/options"
290
+ }
291
+ if (id === "virtual:rimelight-seo/config") {
292
+ return "\0virtual:rimelight-seo/config"
293
+ }
294
+ if (id === "virtual:rimelight-seo/site-config") {
295
+ return "\0virtual:rimelight-seo/site-config"
296
+ }
297
+ if (id === "virtual:rimelight-seo/db") {
298
+ return "\0virtual:rimelight-seo/db"
299
+ }
300
+ if (id === "virtual:rimelight-seo/corpus") {
301
+ return "\0virtual:rimelight-seo/corpus"
58
302
  }
59
303
  return null
60
304
  },
61
305
  load(id) {
62
- if (id === "\0virtual:rimelight-seo-config") {
306
+ if (id === "\0virtual:rimelight-seo/options") {
63
307
  return `export default ${JSON.stringify(options)};`
64
308
  }
309
+ if (id === "\0virtual:rimelight-seo/config") {
310
+ if (seoConfigPath) {
311
+ return `import * as customConfig from ${JSON.stringify(seoConfigPath)};
312
+ export const SEO_LOCALES = customConfig.SEO_LOCALES;
313
+ export const PRIVATE_PATH_PREFIXES = customConfig.PRIVATE_PATH_PREFIXES || [];
314
+ export const seoEntries = customConfig.seoEntries;
315
+ export const rssEntries = customConfig.rssEntries;
316
+ export default customConfig;`
317
+ }
318
+ return `export const SEO_LOCALES = undefined;
319
+ export const PRIVATE_PATH_PREFIXES = [];
320
+ export const seoEntries = async () => [];
321
+ export const rssEntries = undefined;`
322
+ }
323
+ if (id === "\0virtual:rimelight-seo/site-config") {
324
+ if (siteConfigPath) {
325
+ return `export * from ${JSON.stringify(siteConfigPath)};`
326
+ }
327
+ return `export const siteConfig = { name: "Site", description: "", url: "" };`
328
+ }
329
+ if (id === "\0virtual:rimelight-seo/db") {
330
+ if (dbPath) {
331
+ return `export * from ${JSON.stringify(dbPath)};`
332
+ }
333
+ return `export const db = null;
334
+ export const pages = null;`
335
+ }
336
+ if (id === "\0virtual:rimelight-seo/corpus") {
337
+ if (corpusPath) {
338
+ return `export * from ${JSON.stringify(corpusPath)};`
339
+ }
340
+ return `export const corpusPages = [];`
341
+ }
65
342
  return null
66
343
  }
67
344
  }
@@ -72,3 +349,5 @@ export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegratio
72
349
  }
73
350
  }
74
351
  }
352
+
353
+ export default rimelightSeo
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from "vite-plus/test"
2
+ import { buildWebManifest } from "./manifest.js"
3
+
4
+ describe("buildWebManifest", () => {
5
+ it("generates standard web manifest with sensible defaults", () => {
6
+ const manifest = buildWebManifest({
7
+ name: "My Application"
8
+ })
9
+
10
+ expect(manifest.name).toBe("My Application")
11
+ expect(manifest.short_name).toBe("My Application")
12
+ expect(manifest.start_url).toBe("/")
13
+ expect(manifest.display).toBe("standalone")
14
+ expect(manifest.background_color).toBe("#ffffff")
15
+ expect(manifest.theme_color).toBe("#ffffff")
16
+ expect(manifest.icons).toHaveLength(3)
17
+ })
18
+
19
+ it("customizes theme, icons and shortcuts", () => {
20
+ const manifest = buildWebManifest({
21
+ name: "Custom App",
22
+ shortName: "App",
23
+ description: "A custom web application",
24
+ themeColor: "#000000",
25
+ backgroundColor: "#111111",
26
+ shortcuts: [
27
+ {
28
+ name: "Dashboard",
29
+ url: "/dashboard"
30
+ }
31
+ ]
32
+ })
33
+
34
+ expect(manifest.name).toBe("Custom App")
35
+ expect(manifest.short_name).toBe("App")
36
+ expect(manifest.description).toBe("A custom web application")
37
+ expect(manifest.theme_color).toBe("#000000")
38
+ expect(manifest.background_color).toBe("#111111")
39
+ expect(manifest.shortcuts).toHaveLength(1)
40
+ })
41
+ })
@@ -0,0 +1,59 @@
1
+ import type { WebManifest, WebManifestOptions } from "./types.js"
2
+
3
+ /**
4
+ * Build web app manifest (site.webmanifest) compliant with W3C Web App Manifest.
5
+ */
6
+ export function buildWebManifest(options: WebManifestOptions): WebManifest {
7
+ const {
8
+ name,
9
+ shortName = name,
10
+ description = "",
11
+ startUrl = "/",
12
+ display = "standalone",
13
+ backgroundColor = "#ffffff",
14
+ themeColor = "#ffffff",
15
+ icons = [
16
+ {
17
+ src: "/icon-192.png",
18
+ sizes: "192x192",
19
+ type: "image/png",
20
+ purpose: "any"
21
+ },
22
+ {
23
+ src: "/icon-512.png",
24
+ sizes: "512x512",
25
+ type: "image/png",
26
+ purpose: "any"
27
+ },
28
+ {
29
+ src: "/icon-maskable-512.png",
30
+ sizes: "512x512",
31
+ type: "image/png",
32
+ purpose: "maskable"
33
+ }
34
+ ],
35
+ shortcuts,
36
+ categories,
37
+ lang,
38
+ dir,
39
+ orientation,
40
+ scope
41
+ } = options
42
+
43
+ return {
44
+ name,
45
+ short_name: shortName,
46
+ ...(description ? { description } : {}),
47
+ start_url: startUrl,
48
+ display,
49
+ background_color: backgroundColor,
50
+ theme_color: themeColor,
51
+ icons,
52
+ ...(shortcuts ? { shortcuts } : {}),
53
+ ...(categories ? { categories } : {}),
54
+ ...(lang ? { lang } : {}),
55
+ ...(dir ? { dir } : {}),
56
+ ...(orientation ? { orientation } : {}),
57
+ ...(scope ? { scope } : {})
58
+ }
59
+ }