@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.
- package/dist/assets.d.mts +39 -0
- package/dist/assets.mjs +96 -0
- package/dist/catalog.d.mts +7 -0
- package/dist/catalog.mjs +10 -0
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +5 -1
- package/dist/integration.mjs +182 -0
- package/dist/manifest.d.mts +7 -0
- package/dist/manifest.mjs +44 -0
- package/dist/og.mjs +1 -1
- package/dist/routes/api-catalog.d.mts +5 -0
- package/dist/routes/api-catalog.mjs +38 -0
- package/dist/routes/apple-touch-icon.png.d.mts +5 -0
- package/dist/routes/apple-touch-icon.png.mjs +17 -0
- package/dist/routes/favicon.ico.d.mts +5 -0
- package/dist/routes/favicon.ico.mjs +14 -0
- package/dist/routes/favicon.svg.d.mts +5 -0
- package/dist/routes/favicon.svg.mjs +12 -0
- package/dist/routes/icon-192.png.d.mts +5 -0
- package/dist/routes/icon-192.png.mjs +17 -0
- package/dist/routes/icon-512.png.d.mts +5 -0
- package/dist/routes/icon-512.png.mjs +17 -0
- package/dist/routes/icon-maskable-512.png.d.mts +5 -0
- package/dist/routes/icon-maskable-512.png.mjs +18 -0
- package/dist/routes/llms.txt.mjs +1 -1
- package/dist/routes/robots.txt.mjs +1 -1
- package/dist/routes/rss.xml.mjs +48 -20
- package/dist/routes/security.txt.d.mts +5 -0
- package/dist/routes/security.txt.mjs +24 -0
- package/dist/routes/site.webmanifest.d.mts +5 -0
- package/dist/routes/site.webmanifest.mjs +23 -0
- package/dist/routes/sitemap.xml.mjs +1 -1
- package/dist/security.d.mts +7 -0
- package/dist/security.mjs +34 -0
- package/dist/types.d.mts +191 -8
- package/package.json +23 -2
- package/src/assets.test.ts +70 -0
- package/src/assets.ts +137 -0
- package/src/catalog.test.ts +26 -0
- package/src/catalog.ts +11 -0
- package/src/components/SEOHead.astro +1 -1
- package/src/env.d.ts +56 -33
- package/src/index.ts +18 -1
- package/src/integration.ts +353 -172
- package/src/manifest.test.ts +41 -0
- package/src/manifest.ts +59 -0
- package/src/og.ts +1 -9
- package/src/routes/api-catalog.ts +55 -0
- package/src/routes/apple-touch-icon.png.ts +19 -0
- package/src/routes/favicon.ico.ts +19 -0
- package/src/routes/favicon.svg.ts +17 -0
- package/src/routes/icon-192.png.ts +19 -0
- package/src/routes/icon-512.png.ts +19 -0
- package/src/routes/icon-maskable-512.png.ts +24 -0
- package/src/routes/llms-full.txt.ts +77 -77
- package/src/routes/llms.txt.ts +93 -90
- package/src/routes/robots.txt.ts +63 -63
- package/src/routes/rss.xml.ts +129 -87
- package/src/routes/security.txt.ts +30 -0
- package/src/routes/site.webmanifest.ts +28 -0
- package/src/routes/sitemap.xml.ts +43 -43
- package/src/rss.test.ts +28 -28
- package/src/rss.ts +63 -63
- package/src/security.test.ts +46 -0
- package/src/security.ts +78 -0
- package/src/types.ts +564 -367
package/src/integration.ts
CHANGED
|
@@ -1,172 +1,353 @@
|
|
|
1
|
-
import type { AstroIntegration } from "astro"
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"src/config/seo.config.
|
|
28
|
-
"src/config/seo.config.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"src/config/site.config.
|
|
34
|
-
"src/config/site.config.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"src/config/llms.corpus.
|
|
42
|
-
"src/config/llms.corpus.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
1
|
+
import type { AstroIntegration } from "astro"
|
|
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"
|
|
9
|
+
|
|
10
|
+
export function rimelightSeo(options: RimelightSeoOptions = {}): AstroIntegration {
|
|
11
|
+
return {
|
|
12
|
+
name: "@rimelight/seo",
|
|
13
|
+
hooks: {
|
|
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
|
|
73
|
+
if (options.robots !== false) {
|
|
74
|
+
const pattern =
|
|
75
|
+
typeof options.robots === "object" && options.robots.path
|
|
76
|
+
? options.robots.path
|
|
77
|
+
: "/robots.txt"
|
|
78
|
+
injectRoute({
|
|
79
|
+
pattern,
|
|
80
|
+
entrypoint: "@rimelight/seo/routes/robots.txt.ts"
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (options.sitemap !== false) {
|
|
85
|
+
const pattern =
|
|
86
|
+
typeof options.sitemap === "object" && options.sitemap.path
|
|
87
|
+
? options.sitemap.path
|
|
88
|
+
: "/sitemap.xml"
|
|
89
|
+
injectRoute({
|
|
90
|
+
pattern,
|
|
91
|
+
entrypoint: "@rimelight/seo/routes/sitemap.xml.ts"
|
|
92
|
+
})
|
|
93
|
+
}
|
|
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
|
+
|
|
104
|
+
if (options.llms !== false) {
|
|
105
|
+
const pattern =
|
|
106
|
+
typeof options.llms === "object" && options.llms.path ? options.llms.path : "/llms.txt"
|
|
107
|
+
injectRoute({
|
|
108
|
+
pattern,
|
|
109
|
+
entrypoint: "@rimelight/seo/routes/llms.txt.ts"
|
|
110
|
+
})
|
|
111
|
+
}
|
|
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
|
+
|
|
166
|
+
updateConfig({
|
|
167
|
+
vite: {
|
|
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
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: "vite-plugin-rimelight-seo",
|
|
284
|
+
resolveId(id) {
|
|
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"
|
|
302
|
+
}
|
|
303
|
+
return null
|
|
304
|
+
},
|
|
305
|
+
load(id) {
|
|
306
|
+
if (id === "\0virtual:rimelight-seo/options") {
|
|
307
|
+
return `export default ${JSON.stringify(options)};`
|
|
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
|
+
}
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
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
|
+
})
|
package/src/manifest.ts
ADDED
|
@@ -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
|
+
}
|
package/src/og.ts
CHANGED
|
@@ -135,15 +135,7 @@ export async function loadOgFonts(
|
|
|
135
135
|
* `renderOgResponse()` directly -- this function is just the house style.
|
|
136
136
|
*/
|
|
137
137
|
export function defaultOgLayout(options: OgLayoutOptions, font: OgFontConfig = NOTO_SANS): object {
|
|
138
|
-
const {
|
|
139
|
-
title = "",
|
|
140
|
-
description,
|
|
141
|
-
brand,
|
|
142
|
-
badge,
|
|
143
|
-
date,
|
|
144
|
-
background = "dark",
|
|
145
|
-
preview = false
|
|
146
|
-
} = options
|
|
138
|
+
const { title, description, brand, badge, date, background = "dark", preview = false } = options
|
|
147
139
|
|
|
148
140
|
const MAX_TITLE_LEN = 160
|
|
149
141
|
const MAX_DESC_LEN = 240
|