@hagicode/hagilight 0.3.2-dev.58.1.bee3693 → 0.4.1-dev.60.1.9bbf68a

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 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, optional localized RSS feeds and favicon, and a reusable SEO head component.
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
- For localized RSS, use `hagilightRss({ locales, getFeed: './src/rss-feed.ts' })` from the same `/integration` entry point; `getFeed` points to a project-root-relative module exporting a feed callback. Import `@hagicode/hagilight/SEOHead` for SEO metadata in an Astro page or layout.
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.
@@ -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: RssLocalesInput;
13
- /** Project-root-relative module whose default export is an `RssFeedCallback`. */
14
- getFeed: string;
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. An existing
22
- * `@astrojs/sitemap` or Starlight integration owns sitemap generation, and a
23
- * consumer-owned robots page or public file takes precedence.
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;
@@ -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 { RSS_OWNER, resolvePlainAstroRssOwner, } from '@hagicode/hagilight-core/rss-ownership';
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
- * Generate a sitemap and `robots.txt` for a plain Astro site. An existing
35
- * `@astrojs/sitemap` or Starlight integration owns sitemap generation, and a
36
- * consumer-owned robots page or public file takes precedence.
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
- export function hagilight({ enabled = true } = {}) {
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
- /** Inject the bundled HagiCode favicon unless the site head already declares an icon. */
72
- export function hagilightFavicon(options = {}) {
73
- if (!isRecord(options)) {
74
- throw new TypeError('Hagilight favicon options must be an object.');
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 resolveRssOptions(options) {
90
- if (!isRecord(options)) {
91
- throw new TypeError('Hagilight RSS integration options must be an object.');
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 locales = resolveRssLocales(options.locales, { requireNonEmpty: true });
94
- if (typeof options.getFeed !== 'string' || !options.getFeed.trim()) {
95
- throw new TypeError('Hagilight RSS getFeed must be a non-empty module path such as "./src/rss-feed.mjs".');
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 { locales, getFeed: options.getFeed.trim() };
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 do not enable hagilightRss().`);
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
- `import * as siteFeedModule from ${JSON.stringify(modulePath)};`,
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
- ' getFeed: siteFeedModule.default,',
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
- * Generate localized `/rss.xml`, `/rss.en.xml`, and `/rss.<language>.xml`
190
- * routes and expose their URLs to the plain-Astro Footer. An enabled Starlight
191
- * Hagilight RSS integration in the same project owns the routes instead.
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:rss',
197
- [RSS_OWNER]: { package: 'astro', enabled: true },
237
+ name: '@hagicode/hagilight:favicon',
198
238
  hooks: {
199
- 'astro:config:setup'({ config, injectRoute, addMiddleware, updateConfig }) {
200
- if (resolvePlainAstroRssOwner(config.integrations) === 'starlight')
239
+ 'astro:config:setup'({ config, updateConfig }) {
240
+ const head = config.head;
241
+ const entry = resolveFaviconHeadEntry(head, options);
242
+ if (!entry)
201
243
  return;
202
- const site = resolveSite(config.site);
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
  };
@@ -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 `hagilightRss({ getFeed })`. */
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.3.2-dev.58.1.bee3693",
3
+ "version": "0.4.1-dev.60.1.9bbf68a",
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.3.2-dev.58.1.bee3693"
42
+ "@hagicode/hagilight-core": "0.4.1-dev.60.1.9bbf68a"
43
43
  },
44
44
  "keywords": [
45
45
  "astro-component",