@hagicode/hagilight 0.4.0 → 0.4.1
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/README.md +4 -4
- package/dist/integration.d.ts +19 -18
- package/dist/integration.js +96 -91
- package/dist/rss-config.d.ts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hagicode/hagilight
|
|
2
2
|
|
|
3
|
-
Astro integrations and components for HagiCode sites without Starlight. Provides sitemap and `robots.txt` generation,
|
|
3
|
+
Astro integrations and components for HagiCode sites without Starlight. Provides sitemap and `robots.txt` generation, localized RSS feeds (enabled by default) and favicon, and a reusable SEO head component.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -23,8 +23,8 @@ export default defineConfig({
|
|
|
23
23
|
});
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`hagilight()` generates a sitemap and `robots.txt` when the site does not already provide them. Set an absolute HTTP(S) `site` URL. `hagilightFavicon()` adds the bundled icon unless the site already declares one.
|
|
26
|
+
`hagilight()` generates a sitemap and `robots.txt` when the site does not already provide them, and localized RSS feeds (`/rss.xml`, `/rss.<language>.xml`) by default. Set an absolute HTTP(S) `site` URL. `hagilightFavicon()` adds the bundled icon unless the site already declares one.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
RSS is on by default: `hagilight()` derives feed locales from the Astro `i18n` config and serves a built-in empty feed. Customize it with the `rss` option, for example `hagilight({ rss: { getFeed: './src/rss-feed.ts' } })`; `getFeed` points to a project-root-relative module exporting a feed callback. Pass `rss: false` to disable RSS. Import `@hagicode/hagilight/SEOHead` for SEO metadata in an Astro page or layout.
|
|
29
29
|
|
|
30
|
-
See the [repository documentation](https://github.com/HagiCode-org/hagilight#readme) for RSS configuration, feed callback types, and integration behavior.
|
|
30
|
+
See the [repository documentation](https://github.com/HagiCode-org/hagilight#readme) for RSS configuration, feed callback types, and integration behavior.
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import type { AstroIntegration } from 'astro';
|
|
2
2
|
import { type FaviconOptions } from '@hagicode/hagilight-core/favicon';
|
|
3
3
|
import { type RssLocalesInput } from '@hagicode/hagilight-core/rss';
|
|
4
|
-
import { RSS_OWNER, type RssOwnerClaim } from '@hagicode/hagilight-core/rss-ownership';
|
|
5
4
|
export type { RssFeedCallback, RssFeedContent, RssFeedRequest, RssFooterContext, } from './rss-config.js';
|
|
6
5
|
export interface HagilightOptions {
|
|
7
6
|
/** Generate `sitemap-index.xml` and `robots.txt`. Defaults to `true`. */
|
|
8
7
|
enabled?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Generate localized `/rss.xml` and `/rss.<language>.xml` feeds. Defaults to
|
|
10
|
+
* `true`, so integrating `hagilight()` is enough to publish an RSS feed.
|
|
11
|
+
* Pass `false` to disable, or an options object to customize the feed:
|
|
12
|
+
* - `locales`: Starlight-shaped locale map. Derived from the Astro `i18n`
|
|
13
|
+
* config when omitted.
|
|
14
|
+
* - `getFeed`: project-root-relative module whose default export is an
|
|
15
|
+
* `RssFeedCallback`. A built-in empty feed is used when omitted.
|
|
16
|
+
*/
|
|
17
|
+
rss?: boolean | HagilightRssOptions;
|
|
9
18
|
}
|
|
10
19
|
export interface HagilightRssOptions {
|
|
11
|
-
/** Starlight-shaped locale map, for example `{ root: { lang: 'en-US' }, 'zh-CN': { lang: 'zh-CN' } }`. */
|
|
12
|
-
locales
|
|
13
|
-
/** Project-root-relative module whose default export is an `RssFeedCallback`. */
|
|
14
|
-
getFeed
|
|
20
|
+
/** Starlight-shaped locale map, for example `{ root: { lang: 'en-US' }, 'zh-CN': { lang: 'zh-CN' } }`. Derived from Astro `i18n` when omitted. */
|
|
21
|
+
locales?: RssLocalesInput;
|
|
22
|
+
/** Project-root-relative module whose default export is an `RssFeedCallback`. A built-in empty feed is used when omitted. */
|
|
23
|
+
getFeed?: string;
|
|
15
24
|
}
|
|
16
25
|
export type HagilightFaviconOptions = FaviconOptions;
|
|
17
|
-
export type HagilightRssIntegration = AstroIntegration & {
|
|
18
|
-
readonly [RSS_OWNER]: RssOwnerClaim;
|
|
19
|
-
};
|
|
20
26
|
/**
|
|
21
|
-
* Generate a sitemap and `robots.txt` for a plain Astro site
|
|
22
|
-
* `@astrojs/sitemap` or Starlight integration
|
|
23
|
-
* consumer-owned robots page or public file takes
|
|
27
|
+
* Generate a sitemap and `robots.txt` for a plain Astro site, and (by default)
|
|
28
|
+
* localized RSS feeds. An existing `@astrojs/sitemap` or Starlight integration
|
|
29
|
+
* owns sitemap generation, a consumer-owned robots page or public file takes
|
|
30
|
+
* precedence, and an enabled Starlight Hagilight integration owns RSS feeds.
|
|
24
31
|
*/
|
|
25
|
-
export declare function hagilight({ enabled }?: HagilightOptions): AstroIntegration;
|
|
32
|
+
export declare function hagilight({ enabled, rss }?: HagilightOptions): AstroIntegration;
|
|
26
33
|
/** Inject the bundled HagiCode favicon unless the site head already declares an icon. */
|
|
27
34
|
export declare function hagilightFavicon(options?: HagilightFaviconOptions): AstroIntegration;
|
|
28
|
-
/**
|
|
29
|
-
* Generate localized `/rss.xml`, `/rss.en.xml`, and `/rss.<language>.xml`
|
|
30
|
-
* routes and expose their URLs to the plain-Astro Footer. An enabled Starlight
|
|
31
|
-
* Hagilight RSS integration in the same project owns the routes instead.
|
|
32
|
-
*/
|
|
33
|
-
export declare function hagilightRss(input: HagilightRssOptions): HagilightRssIntegration;
|
package/dist/integration.js
CHANGED
|
@@ -4,7 +4,7 @@ import { extname, isAbsolute, resolve, win32 } from 'node:path';
|
|
|
4
4
|
import sitemap from '@astrojs/sitemap';
|
|
5
5
|
import { resolveFaviconHeadEntry } from '@hagicode/hagilight-core/favicon';
|
|
6
6
|
import { resolveRssLocales } from '@hagicode/hagilight-core/rss';
|
|
7
|
-
import {
|
|
7
|
+
import { resolvePlainAstroRssOwner } from '@hagicode/hagilight-core/rss-ownership';
|
|
8
8
|
const VIRTUAL_RSS_CONFIG = 'virtual:hagilight/rss-config';
|
|
9
9
|
const VIRTUAL_RSS_CONFIG_RESOLVED = `\0${VIRTUAL_RSS_CONFIG}`;
|
|
10
10
|
const PAGE_SOURCE_EXTENSIONS = new Set(['.astro', '.html', '.js', '.jsx', '.md', '.mdx', '.ts', '.tsx']);
|
|
@@ -31,18 +31,41 @@ function resolveDiscoverySite(site) {
|
|
|
31
31
|
return parsed;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Built-in RSS feed used when a consumer does not supply a `getFeed` module.
|
|
35
|
+
* It is serialized into the generated virtual module, so it must stay a pure
|
|
36
|
+
* function with no external references.
|
|
37
37
|
*/
|
|
38
|
-
|
|
38
|
+
const BUILTIN_RSS_FEED = async () => ({
|
|
39
|
+
title: 'Site Updates',
|
|
40
|
+
description: 'RSS feed for this site.',
|
|
41
|
+
items: [],
|
|
42
|
+
});
|
|
43
|
+
const BUILTIN_RSS_FEED_SOURCE = BUILTIN_RSS_FEED.toString();
|
|
44
|
+
/**
|
|
45
|
+
* Generate a sitemap and `robots.txt` for a plain Astro site, and (by default)
|
|
46
|
+
* localized RSS feeds. An existing `@astrojs/sitemap` or Starlight integration
|
|
47
|
+
* owns sitemap generation, a consumer-owned robots page or public file takes
|
|
48
|
+
* precedence, and an enabled Starlight Hagilight integration owns RSS feeds.
|
|
49
|
+
*/
|
|
50
|
+
export function hagilight({ enabled = true, rss } = {}) {
|
|
39
51
|
if (typeof enabled !== 'boolean') {
|
|
40
52
|
throw new TypeError('Hagilight sitemap and robots enabled option must be a boolean.');
|
|
41
53
|
}
|
|
54
|
+
if (rss !== undefined && typeof rss !== 'boolean' && !isRecord(rss)) {
|
|
55
|
+
throw new TypeError('Hagilight RSS option must be a boolean or an options object.');
|
|
56
|
+
}
|
|
57
|
+
if (isRecord(rss)) {
|
|
58
|
+
if (rss.locales !== undefined && !isRecord(rss.locales)) {
|
|
59
|
+
throw new TypeError('Hagilight RSS locales must be an object.');
|
|
60
|
+
}
|
|
61
|
+
if (rss.getFeed !== undefined && (typeof rss.getFeed !== 'string' || !rss.getFeed.trim())) {
|
|
62
|
+
throw new TypeError('Hagilight RSS getFeed must be a non-empty module path.');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
42
65
|
return {
|
|
43
66
|
name: '@hagicode/hagilight:discovery',
|
|
44
67
|
hooks: {
|
|
45
|
-
'astro:config:setup'({ config, injectRoute, updateConfig }) {
|
|
68
|
+
'astro:config:setup'({ config, injectRoute, addMiddleware, updateConfig }) {
|
|
46
69
|
if (!enabled)
|
|
47
70
|
return;
|
|
48
71
|
resolveDiscoverySite(config.site);
|
|
@@ -64,37 +87,65 @@ export function hagilight({ enabled = true } = {}) {
|
|
|
64
87
|
prerender: true,
|
|
65
88
|
});
|
|
66
89
|
}
|
|
90
|
+
if (rss === false)
|
|
91
|
+
return;
|
|
92
|
+
if (resolvePlainAstroRssOwner(config.integrations) === 'starlight')
|
|
93
|
+
return;
|
|
94
|
+
const rssOptions = rss === true || rss === undefined ? {} : rss;
|
|
95
|
+
const locales = resolveRssLocales(deriveRssLocales(rssOptions.locales, config), { requireNonEmpty: false });
|
|
96
|
+
const modulePath = rssOptions.getFeed
|
|
97
|
+
? resolveFeedModule(config.root, rssOptions.getFeed)
|
|
98
|
+
: null;
|
|
99
|
+
const site = resolveSite(config.site);
|
|
100
|
+
assertNoRouteCollisions(config, locales);
|
|
101
|
+
const baseSegments = `${config.base || '/'}`.split('/').filter(Boolean);
|
|
102
|
+
const baseUrl = baseSegments.length > 0 ? `/${baseSegments.join('/')}/` : '/';
|
|
103
|
+
const baseLocation = new URL(baseUrl, site);
|
|
104
|
+
const defaultFeedUrl = new URL('rss.xml', baseLocation).href;
|
|
105
|
+
const localeFeedUrls = Object.fromEntries(locales
|
|
106
|
+
.filter(({ filename }) => filename !== 'en')
|
|
107
|
+
.map(({ lang, filename }) => [lang, new URL(`rss.${filename}.xml`, baseLocation).href]));
|
|
108
|
+
injectRoute({ pattern: '/rss.xml', entrypoint: routeEntrypoint('rss.xml.ts'), prerender: true });
|
|
109
|
+
injectRoute({ pattern: '/rss.[language].xml', entrypoint: routeEntrypoint('rss.[language].xml.ts'), prerender: true });
|
|
110
|
+
addMiddleware({ entrypoint: fileURLToPath(new URL('./rss-middleware.js', import.meta.url)), order: 'pre' });
|
|
111
|
+
updateConfig({
|
|
112
|
+
vite: {
|
|
113
|
+
plugins: [createVirtualPlugin({ modulePath, site, baseUrl, locales, defaultFeedUrl, localeFeedUrls })],
|
|
114
|
+
},
|
|
115
|
+
});
|
|
67
116
|
},
|
|
68
117
|
},
|
|
69
118
|
};
|
|
70
119
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
120
|
+
function deriveRssLocales(locales, config) {
|
|
121
|
+
if (locales !== undefined)
|
|
122
|
+
return locales;
|
|
123
|
+
const i18n = config.i18n;
|
|
124
|
+
if (!i18n?.locales?.length)
|
|
125
|
+
return undefined;
|
|
126
|
+
const defaultLocale = i18n.defaultLocale ?? String(i18n.locales[0]);
|
|
127
|
+
const map = {};
|
|
128
|
+
for (const entry of i18n.locales) {
|
|
129
|
+
const lang = typeof entry === 'string' ? entry : entry.codes?.[0]
|
|
130
|
+
?? entry.path;
|
|
131
|
+
if (typeof lang !== 'string' || !lang.trim())
|
|
132
|
+
continue;
|
|
133
|
+
if (lang === defaultLocale)
|
|
134
|
+
map.root = { lang };
|
|
135
|
+
else
|
|
136
|
+
map[lang] = { lang };
|
|
75
137
|
}
|
|
76
|
-
return
|
|
77
|
-
name: '@hagicode/hagilight:favicon',
|
|
78
|
-
hooks: {
|
|
79
|
-
'astro:config:setup'({ config, updateConfig }) {
|
|
80
|
-
const head = config.head;
|
|
81
|
-
const entry = resolveFaviconHeadEntry(head, options);
|
|
82
|
-
if (!entry)
|
|
83
|
-
return;
|
|
84
|
-
updateConfig({ head: [...(head ?? []), entry] });
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
};
|
|
138
|
+
return map;
|
|
88
139
|
}
|
|
89
|
-
function
|
|
90
|
-
if (
|
|
91
|
-
throw new TypeError('Hagilight RSS
|
|
140
|
+
function resolveFeedModule(root, reference) {
|
|
141
|
+
if (reference.startsWith('file:') || isAbsolute(reference) || win32.isAbsolute(reference)) {
|
|
142
|
+
throw new TypeError('Hagilight RSS getFeed must be a path relative to the Astro project root.');
|
|
92
143
|
}
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
throw new
|
|
144
|
+
const modulePath = resolve(fileURLToPath(root), reference);
|
|
145
|
+
if (!existsSync(modulePath) || !statSync(modulePath).isFile()) {
|
|
146
|
+
throw new Error(`Hagilight RSS getFeed module "${reference}" was not found relative to the Astro project root.`);
|
|
96
147
|
}
|
|
97
|
-
return
|
|
148
|
+
return modulePath;
|
|
98
149
|
}
|
|
99
150
|
function resolveSite(site) {
|
|
100
151
|
let parsed;
|
|
@@ -109,16 +160,6 @@ function resolveSite(site) {
|
|
|
109
160
|
}
|
|
110
161
|
return parsed.href;
|
|
111
162
|
}
|
|
112
|
-
function resolveFeedModule(root, reference) {
|
|
113
|
-
if (reference.startsWith('file:') || isAbsolute(reference) || win32.isAbsolute(reference)) {
|
|
114
|
-
throw new TypeError('Hagilight RSS getFeed must be a path relative to the Astro project root.');
|
|
115
|
-
}
|
|
116
|
-
const modulePath = resolve(fileURLToPath(root), reference);
|
|
117
|
-
if (!existsSync(modulePath) || !statSync(modulePath).isFile()) {
|
|
118
|
-
throw new Error(`Hagilight RSS getFeed module "${reference}" was not found relative to the Astro project root.`);
|
|
119
|
-
}
|
|
120
|
-
return modulePath;
|
|
121
|
-
}
|
|
122
163
|
function assertNoRouteCollisions(config, locales) {
|
|
123
164
|
const expectedFiles = new Set(['rss.xml', 'rss.en.xml', ...locales.map(({ filename }) => `rss.${filename}.xml`)]);
|
|
124
165
|
const pageRoot = resolve(fileURLToPath(config.srcDir), 'pages');
|
|
@@ -159,10 +200,12 @@ function assertNoRouteCollisions(config, locales) {
|
|
|
159
200
|
collisions.push(publicFile);
|
|
160
201
|
}
|
|
161
202
|
if (collisions.length > 0) {
|
|
162
|
-
throw new Error(`Hagilight RSS route conflicts with consumer-owned file(s): ${collisions.join(', ')}. Remove the conflicting route or
|
|
203
|
+
throw new Error(`Hagilight RSS route conflicts with consumer-owned file(s): ${collisions.join(', ')}. Remove the conflicting route or disable RSS with rss: false.`);
|
|
163
204
|
}
|
|
164
205
|
}
|
|
165
206
|
function createVirtualPlugin({ modulePath, site, baseUrl, locales, defaultFeedUrl, localeFeedUrls }) {
|
|
207
|
+
const feedImport = modulePath ? `import * as siteFeedModule from ${JSON.stringify(modulePath)};` : '';
|
|
208
|
+
const getFeedExpr = modulePath ? 'siteFeedModule.default' : `(${BUILTIN_RSS_FEED_SOURCE})`;
|
|
166
209
|
return {
|
|
167
210
|
name: '@hagicode/hagilight:rss',
|
|
168
211
|
resolveId(id) {
|
|
@@ -172,12 +215,12 @@ function createVirtualPlugin({ modulePath, site, baseUrl, locales, defaultFeedUr
|
|
|
172
215
|
if (id !== VIRTUAL_RSS_CONFIG_RESOLVED)
|
|
173
216
|
return null;
|
|
174
217
|
return [
|
|
175
|
-
|
|
218
|
+
feedImport,
|
|
176
219
|
'const config = {',
|
|
177
220
|
` site: ${JSON.stringify(site)},`,
|
|
178
221
|
` baseUrl: ${JSON.stringify(baseUrl)},`,
|
|
179
222
|
` locales: ${JSON.stringify(locales)},`,
|
|
180
|
-
|
|
223
|
+
` getFeed: ${getFeedExpr},`,
|
|
181
224
|
` footer: ${JSON.stringify({ defaultFeedUrl, localeFeedUrls, locales })},`,
|
|
182
225
|
'};',
|
|
183
226
|
'export default config;',
|
|
@@ -185,58 +228,20 @@ function createVirtualPlugin({ modulePath, site, baseUrl, locales, defaultFeedUr
|
|
|
185
228
|
},
|
|
186
229
|
};
|
|
187
230
|
}
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
export function hagilightRss(input) {
|
|
194
|
-
const options = resolveRssOptions(input);
|
|
231
|
+
/** Inject the bundled HagiCode favicon unless the site head already declares an icon. */
|
|
232
|
+
export function hagilightFavicon(options = {}) {
|
|
233
|
+
if (!isRecord(options)) {
|
|
234
|
+
throw new TypeError('Hagilight favicon options must be an object.');
|
|
235
|
+
}
|
|
195
236
|
return {
|
|
196
|
-
name: '@hagicode/hagilight:
|
|
197
|
-
[RSS_OWNER]: { package: 'astro', enabled: true },
|
|
237
|
+
name: '@hagicode/hagilight:favicon',
|
|
198
238
|
hooks: {
|
|
199
|
-
'astro:config:setup'({ config,
|
|
200
|
-
|
|
239
|
+
'astro:config:setup'({ config, updateConfig }) {
|
|
240
|
+
const head = config.head;
|
|
241
|
+
const entry = resolveFaviconHeadEntry(head, options);
|
|
242
|
+
if (!entry)
|
|
201
243
|
return;
|
|
202
|
-
|
|
203
|
-
const modulePath = resolveFeedModule(config.root, options.getFeed);
|
|
204
|
-
assertNoRouteCollisions(config, options.locales);
|
|
205
|
-
const baseSegments = `${config.base || '/'}`
|
|
206
|
-
.split('/')
|
|
207
|
-
.filter(Boolean);
|
|
208
|
-
const baseUrl = baseSegments.length > 0 ? `/${baseSegments.join('/')}/` : '/';
|
|
209
|
-
const baseLocation = new URL(baseUrl, site);
|
|
210
|
-
const defaultFeedUrl = new URL('rss.xml', baseLocation).href;
|
|
211
|
-
const localeFeedUrls = Object.fromEntries(options.locales
|
|
212
|
-
.filter(({ filename }) => filename !== 'en')
|
|
213
|
-
.map(({ lang, filename }) => [lang, new URL(`rss.${filename}.xml`, baseLocation).href]));
|
|
214
|
-
injectRoute({
|
|
215
|
-
pattern: '/rss.xml',
|
|
216
|
-
entrypoint: routeEntrypoint('rss.xml.ts'),
|
|
217
|
-
prerender: true,
|
|
218
|
-
});
|
|
219
|
-
injectRoute({
|
|
220
|
-
pattern: '/rss.[language].xml',
|
|
221
|
-
entrypoint: routeEntrypoint('rss.[language].xml.ts'),
|
|
222
|
-
prerender: true,
|
|
223
|
-
});
|
|
224
|
-
addMiddleware({
|
|
225
|
-
entrypoint: fileURLToPath(new URL('./rss-middleware.js', import.meta.url)),
|
|
226
|
-
order: 'pre',
|
|
227
|
-
});
|
|
228
|
-
updateConfig({
|
|
229
|
-
vite: {
|
|
230
|
-
plugins: [createVirtualPlugin({
|
|
231
|
-
modulePath,
|
|
232
|
-
site,
|
|
233
|
-
baseUrl,
|
|
234
|
-
locales: options.locales,
|
|
235
|
-
defaultFeedUrl,
|
|
236
|
-
localeFeedUrls,
|
|
237
|
-
})],
|
|
238
|
-
},
|
|
239
|
-
});
|
|
244
|
+
updateConfig({ head: [...(head ?? []), entry] });
|
|
240
245
|
},
|
|
241
246
|
},
|
|
242
247
|
};
|
package/dist/rss-config.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface RssFeedContent {
|
|
|
10
10
|
description: string;
|
|
11
11
|
items: readonly RssFeedItem[];
|
|
12
12
|
}
|
|
13
|
-
/** Default export of the module referenced by `
|
|
13
|
+
/** Default export of the module referenced by `hagilight({ rss: { getFeed } })`. */
|
|
14
14
|
export type RssFeedCallback = (request: RssFeedRequest) => RssFeedContent | Promise<RssFeedContent>;
|
|
15
15
|
/** Request-local feed URLs that the plain-Astro Footer reads from `Astro.locals.hagilightRss`. */
|
|
16
16
|
export interface RssFooterContext {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Astro integrations and components for HagiCode sites",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@astrojs/sitemap": "^3.7.4",
|
|
42
|
-
"@hagicode/hagilight-core": "0.4.
|
|
42
|
+
"@hagicode/hagilight-core": "0.4.1"
|
|
43
43
|
},
|
|
44
44
|
"keywords": [
|
|
45
45
|
"astro-component",
|