@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.
- 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 +9 -3
- package/dist/index.mjs +10 -2
- package/dist/integration.d.mts +3 -20
- package/dist/integration.mjs +257 -7
- package/dist/manifest.d.mts +7 -0
- package/dist/manifest.mjs +44 -0
- package/dist/og.d.mts +117 -0
- package/dist/og.mjs +302 -0
- 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-full.txt.d.mts +5 -0
- package/dist/routes/llms-full.txt.mjs +57 -0
- package/dist/routes/llms.txt.d.mts +1 -0
- package/dist/routes/llms.txt.mjs +60 -12
- package/dist/routes/robots.txt.mjs +32 -4
- package/dist/routes/rss.xml.d.mts +4 -0
- package/dist/routes/rss.xml.mjs +79 -0
- 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 +18 -4
- package/dist/rss.d.mts +8 -0
- package/dist/rss.mjs +44 -0
- package/dist/security.d.mts +7 -0
- package/dist/security.mjs +34 -0
- package/dist/types.d.mts +263 -3
- package/package.json +38 -3
- 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 +51 -3
- package/src/index.ts +33 -2
- package/src/integration.ts +307 -28
- package/src/manifest.test.ts +41 -0
- package/src/manifest.ts +59 -0
- package/src/og.ts +436 -0
- 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 -0
- package/src/routes/llms.txt.ts +84 -13
- package/src/routes/robots.txt.ts +47 -4
- package/src/routes/rss.xml.ts +129 -0
- package/src/routes/security.txt.ts +30 -0
- package/src/routes/site.webmanifest.ts +28 -0
- package/src/routes/sitemap.xml.ts +32 -5
- package/src/rss.test.ts +28 -0
- package/src/rss.ts +63 -0
- package/src/security.test.ts +46 -0
- package/src/security.ts +78 -0
- 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 {
|
|
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
|
package/src/integration.ts
CHANGED
|
@@ -1,67 +1,344 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro"
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
57
|
-
|
|
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
|
|
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
|
+
})
|
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
|
+
}
|