@hagicode/hagilight 0.11.1-dev.129.1.ea757b8 → 0.11.1-dev.133.1.338317a
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 +45 -0
- package/dist/integration.d.ts +16 -1
- package/dist/integration.js +102 -52
- package/dist/llms.d.ts +5 -0
- package/dist/llms.js +22 -11
- package/dist/site-metadata.d.ts +31 -0
- package/dist/site-metadata.js +107 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -73,6 +73,51 @@ hagilight({
|
|
|
73
73
|
|
|
74
74
|
Pass `llms: false` (or `{ enabled: false }`) to opt out. The file is only as good as the page titles and descriptions it reads, so write a real `description` for important pages.
|
|
75
75
|
|
|
76
|
+
## Site metadata
|
|
77
|
+
|
|
78
|
+
`hagilight()` writes `/.well-known/hagilight.metadata.json` after the build, next to `llms.txt`. A consumer that needs one machine-readable description of the site reads this file. The schema and its privacy rules are in `@hagicode/hagilight-core`'s README.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"schemaVersion": 1,
|
|
83
|
+
"generator": { "name": "@hagicode/hagilight-starlight", "version": "0.1.0" },
|
|
84
|
+
"site": {
|
|
85
|
+
"name": "HagiCode Docs",
|
|
86
|
+
"url": "https://docs.hagicode.com/",
|
|
87
|
+
"base": "/",
|
|
88
|
+
"description": "Documentation for HagiCode."
|
|
89
|
+
},
|
|
90
|
+
"languages": {
|
|
91
|
+
"default": "en-US",
|
|
92
|
+
"locales": [
|
|
93
|
+
{ "code": "en-US", "lang": "en-US", "path": "/" },
|
|
94
|
+
{ "code": "zh-CN", "lang": "zh-CN", "path": "/zh-CN/" }
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
"build": {
|
|
98
|
+
"branch": "main",
|
|
99
|
+
"commit": "a1b2c3d4e5f60718293a4b5c6d7e8f9012345678",
|
|
100
|
+
"builtAt": "2026-10-17T08:12:34Z"
|
|
101
|
+
},
|
|
102
|
+
"discovery": {
|
|
103
|
+
"sitemap": "/sitemap-index.xml",
|
|
104
|
+
"robots": "/robots.txt",
|
|
105
|
+
"rss": "/rss.xml",
|
|
106
|
+
"llms": "/llms.txt"
|
|
107
|
+
},
|
|
108
|
+
"analytics": { "googleAnalytics": true, "fiftyOneLa": false, "posthog": true },
|
|
109
|
+
"features": { "seo": true, "promoto": true }
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- The file is served under the site's base path, at `<site URL><base>.well-known/hagilight.metadata.json`. A site under `/docs/` gets `/docs/.well-known/hagilight.metadata.json`. An origin-level `/.well-known/` path needs a proxy or a second deployment; a hagilight build controls only its own output.
|
|
114
|
+
- Name and description come from the `name` and `description` options, then the home page `og:site_name` or title and description. Locales come from Astro `i18n`. Analytics flags are `true` only when the built pages contain that provider's loader.
|
|
115
|
+
- The file needs an absolute `site` URL. Without one, the build warns and skips the file; it does not fail.
|
|
116
|
+
- Sitemap and `llms.txt` are checked after they are written, so `discovery` lists what this build actually produced.
|
|
117
|
+
- Pass `metadata: false` to opt out, or `metadata: { name, description }` to set the values. The `enabled` option does not turn it off. A `public/.well-known/hagilight.metadata.json` file is copied as the site's own and kept byte for byte.
|
|
118
|
+
- Hosts that drop dot-directories need an explicit opt-in. GitHub Pages' Jekyll step skips `.well-known` unless the published root contains an empty `.nojekyll` file.
|
|
119
|
+
- When the Starlight plugin is also registered with metadata enabled, the Starlight plugin writes the file, and this integration defers to it. Registration order does not change the output.
|
|
120
|
+
|
|
76
121
|
## Body images
|
|
77
122
|
|
|
78
123
|
`hagilight()` registers the content-image plugin from `@hagicode/hagilight-core/content-images`. Markdown and MDX body images then get `loading="lazy"`, `decoding="async"`, and the class `hagilight-content-image`, with the author's values kept. This registration does not depend on `enabled`, so `hagilight({ enabled: false })` still registers the plugin.
|
package/dist/integration.d.ts
CHANGED
|
@@ -27,6 +27,21 @@ export interface HagilightOptions {
|
|
|
27
27
|
* overwritten. It also follows `enabled`: `hagilight({ enabled: false })` skips it.
|
|
28
28
|
*/
|
|
29
29
|
llms?: boolean | HagilightLlmsOptions;
|
|
30
|
+
/**
|
|
31
|
+
* Generate `/.well-known/hagilight.metadata.json` after the build. Defaults to `true`; `false`
|
|
32
|
+
* opts out, and an options object may set `name` and `description`. A site-provided file is
|
|
33
|
+
* never overwritten. Independent of `enabled`. When the Starlight plugin is also registered
|
|
34
|
+
* and generates metadata, the Starlight plugin writes the file.
|
|
35
|
+
*/
|
|
36
|
+
metadata?: boolean | HagilightMetadataOptions;
|
|
37
|
+
}
|
|
38
|
+
export interface HagilightMetadataOptions {
|
|
39
|
+
/** Defaults to `true`. */
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
/** Site name. Defaults to the home page `og:site_name` or title, then the site hostname. */
|
|
42
|
+
name?: string;
|
|
43
|
+
/** Site description. Defaults to the home page description. */
|
|
44
|
+
description?: string;
|
|
30
45
|
}
|
|
31
46
|
export interface HagilightLlmsOptions {
|
|
32
47
|
/** Defaults to `true`. */
|
|
@@ -59,6 +74,6 @@ export type HagilightFaviconOptions = FaviconOptions;
|
|
|
59
74
|
* owns sitemap generation, a consumer-owned robots page or public file takes
|
|
60
75
|
* precedence, and an enabled Starlight Hagilight integration owns RSS feeds.
|
|
61
76
|
*/
|
|
62
|
-
export declare function hagilight({ enabled, rss, contentImages, llms }?: HagilightOptions): AstroIntegration;
|
|
77
|
+
export declare function hagilight({ enabled, rss, contentImages, llms, metadata }?: HagilightOptions): AstroIntegration;
|
|
63
78
|
/** Inject the bundled HagiCode favicon unless the site head already declares an icon. */
|
|
64
79
|
export declare function hagilightFavicon(options?: HagilightFaviconOptions): AstroIntegration;
|
package/dist/integration.js
CHANGED
|
@@ -7,7 +7,10 @@ import { checkContentImageRegistration, registerContentImages } from '@hagicode/
|
|
|
7
7
|
import { resolveFaviconHeadEntry } from '@hagicode/hagilight-core/favicon';
|
|
8
8
|
import { resolveRssLocales } from '@hagicode/hagilight-core/rss';
|
|
9
9
|
import { resolvePlainAstroRssOwner } from '@hagicode/hagilight-core/rss-ownership';
|
|
10
|
+
import { resolveSiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
|
|
11
|
+
import { expectSiteMetadataReport, reportSiteMetadataBuilt, resolveSiteMetadataOwner, } from '@hagicode/hagilight-core/site-metadata-ownership';
|
|
10
12
|
import { buildLlmsTxt, parsePageMeta } from './llms.js';
|
|
13
|
+
import { writeAstroSiteMetadata } from './site-metadata.js';
|
|
11
14
|
const VIRTUAL_RSS_CONFIG = 'virtual:hagilight/rss-config';
|
|
12
15
|
const VIRTUAL_RSS_CONFIG_RESOLVED = `\0${VIRTUAL_RSS_CONFIG}`;
|
|
13
16
|
const VIRTUAL_BUILD_INFO = 'virtual:hagilight/build-info';
|
|
@@ -52,7 +55,7 @@ const BUILTIN_RSS_FEED_SOURCE = BUILTIN_RSS_FEED.toString();
|
|
|
52
55
|
* owns sitemap generation, a consumer-owned robots page or public file takes
|
|
53
56
|
* precedence, and an enabled Starlight Hagilight integration owns RSS feeds.
|
|
54
57
|
*/
|
|
55
|
-
export function hagilight({ enabled = true, rss, contentImages, llms } = {}) {
|
|
58
|
+
export function hagilight({ enabled = true, rss, contentImages, llms, metadata } = {}) {
|
|
56
59
|
if (typeof enabled !== 'boolean') {
|
|
57
60
|
throw new TypeError('Hagilight sitemap and robots enabled option must be a boolean.');
|
|
58
61
|
}
|
|
@@ -63,6 +66,7 @@ export function hagilight({ enabled = true, rss, contentImages, llms } = {}) {
|
|
|
63
66
|
throw new TypeError('Hagilight content images enabled option must be a boolean.');
|
|
64
67
|
}
|
|
65
68
|
const llmsOptions = resolveLlmsOptions(llms);
|
|
69
|
+
const metadataOptions = resolveSiteMetadataOption(metadata);
|
|
66
70
|
const contentImagesEnabled = typeof contentImages === 'boolean' ? contentImages : contentImages?.enabled !== false;
|
|
67
71
|
if (rss !== undefined && typeof rss !== 'boolean' && !isRecord(rss)) {
|
|
68
72
|
throw new TypeError('Hagilight RSS option must be a boolean or an options object.');
|
|
@@ -76,70 +80,116 @@ export function hagilight({ enabled = true, rss, contentImages, llms } = {}) {
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
let resolvedConfig;
|
|
83
|
+
let resolvedBuildInfo;
|
|
84
|
+
// Set when the Starlight plugin owns the metadata file. This integration then reports
|
|
85
|
+
// after its `llms.txt` step, so the owner's write sees the final discovery outputs.
|
|
86
|
+
let metadataPeerToken;
|
|
79
87
|
return {
|
|
80
88
|
name: '@hagicode/hagilight:discovery',
|
|
81
89
|
hooks: {
|
|
82
90
|
'astro:config:setup'({ config, injectRoute, addMiddleware, updateConfig, logger }) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
91
|
+
// Integrations added by this hook go in one update, in order: the sitemap first, then the
|
|
92
|
+
// metadata finalizer, so the finalizer's build hook runs after the sitemap has written its index.
|
|
93
|
+
let sitemapNeeded = false;
|
|
94
|
+
try {
|
|
95
|
+
registerContentImages({ config, logger, enabled: contentImagesEnabled });
|
|
96
|
+
const buildInfo = resolveBuildInfo({ root: fileURLToPath(config.root) });
|
|
97
|
+
resolvedBuildInfo = buildInfo;
|
|
98
|
+
metadataPeerToken = resolveSiteMetadataOwner() === 'starlight' ? expectSiteMetadataReport() : undefined;
|
|
99
|
+
updateConfig({ vite: { plugins: [createBuildInfoPlugin(buildInfo)] } });
|
|
100
|
+
addMiddleware({ entrypoint: fileURLToPath(new URL('./build-info-middleware.js', import.meta.url)), order: 'pre' });
|
|
101
|
+
if (!enabled)
|
|
102
|
+
return;
|
|
103
|
+
resolveDiscoverySite(config.site);
|
|
104
|
+
const integrations = config.integrations ?? [];
|
|
105
|
+
sitemapNeeded = !integrations.some((integration) => {
|
|
106
|
+
const name = integrationName(integration);
|
|
107
|
+
return name === '@astrojs/sitemap' || name === '@astrojs/starlight';
|
|
108
|
+
});
|
|
109
|
+
const pageRoot = resolve(fileURLToPath(config.srcDir), 'pages');
|
|
110
|
+
const publicRoot = fileURLToPath(config.publicDir);
|
|
111
|
+
const hasRobotsPage = existsSync(pageRoot) && readdirSync(pageRoot).some((name) => /^robots\.txt\.(?:astro|html|js|jsx|md|mdx|ts|tsx)$/u.test(name)
|
|
112
|
+
|| name === 'robots.txt');
|
|
113
|
+
if (!hasRobotsPage && !existsSync(resolve(publicRoot, 'robots.txt'))) {
|
|
114
|
+
injectRoute({
|
|
115
|
+
pattern: '/robots.txt',
|
|
116
|
+
entrypoint: routeEntrypoint('robots.txt.ts'),
|
|
117
|
+
prerender: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (rss === false)
|
|
121
|
+
return;
|
|
122
|
+
if (resolvePlainAstroRssOwner(config.integrations) === 'starlight')
|
|
123
|
+
return;
|
|
124
|
+
const rssOptions = rss === true || rss === undefined ? {} : rss;
|
|
125
|
+
const locales = resolveRssLocales(deriveRssLocales(rssOptions.locales, config), { requireNonEmpty: false });
|
|
126
|
+
const modulePath = rssOptions.getFeed
|
|
127
|
+
? resolveFeedModule(config.root, rssOptions.getFeed)
|
|
128
|
+
: null;
|
|
129
|
+
const site = resolveSite(config.site);
|
|
130
|
+
assertNoRouteCollisions(config, locales);
|
|
131
|
+
const baseSegments = `${config.base || '/'}`.split('/').filter(Boolean);
|
|
132
|
+
const baseUrl = baseSegments.length > 0 ? `/${baseSegments.join('/')}/` : '/';
|
|
133
|
+
const baseLocation = new URL(baseUrl, site);
|
|
134
|
+
const defaultFeedUrl = new URL('rss.xml', baseLocation).href;
|
|
135
|
+
const localeFeedUrls = Object.fromEntries(locales
|
|
136
|
+
.filter(({ filename }) => filename !== 'en')
|
|
137
|
+
.map(({ lang, filename }) => [lang, new URL(`rss.${filename}.xml`, baseLocation).href]));
|
|
138
|
+
injectRoute({ pattern: '/rss.xml', entrypoint: routeEntrypoint('rss.xml.ts'), prerender: true });
|
|
139
|
+
injectRoute({ pattern: '/rss.[language].xml', entrypoint: routeEntrypoint('rss.[language].xml.ts'), prerender: true });
|
|
140
|
+
addMiddleware({ entrypoint: fileURLToPath(new URL('./rss-middleware.js', import.meta.url)), order: 'pre' });
|
|
141
|
+
updateConfig({
|
|
142
|
+
vite: {
|
|
143
|
+
plugins: [createVirtualPlugin({ modulePath, site, baseUrl, locales, defaultFeedUrl, localeFeedUrls })],
|
|
144
|
+
},
|
|
106
145
|
});
|
|
107
146
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const baseSegments = `${config.base || '/'}`.split('/').filter(Boolean);
|
|
120
|
-
const baseUrl = baseSegments.length > 0 ? `/${baseSegments.join('/')}/` : '/';
|
|
121
|
-
const baseLocation = new URL(baseUrl, site);
|
|
122
|
-
const defaultFeedUrl = new URL('rss.xml', baseLocation).href;
|
|
123
|
-
const localeFeedUrls = Object.fromEntries(locales
|
|
124
|
-
.filter(({ filename }) => filename !== 'en')
|
|
125
|
-
.map(({ lang, filename }) => [lang, new URL(`rss.${filename}.xml`, baseLocation).href]));
|
|
126
|
-
injectRoute({ pattern: '/rss.xml', entrypoint: routeEntrypoint('rss.xml.ts'), prerender: true });
|
|
127
|
-
injectRoute({ pattern: '/rss.[language].xml', entrypoint: routeEntrypoint('rss.[language].xml.ts'), prerender: true });
|
|
128
|
-
addMiddleware({ entrypoint: fileURLToPath(new URL('./rss-middleware.js', import.meta.url)), order: 'pre' });
|
|
129
|
-
updateConfig({
|
|
130
|
-
vite: {
|
|
131
|
-
plugins: [createVirtualPlugin({ modulePath, site, baseUrl, locales, defaultFeedUrl, localeFeedUrls })],
|
|
132
|
-
},
|
|
133
|
-
});
|
|
147
|
+
finally {
|
|
148
|
+
// The finalizer is skipped when a Starlight owner writes the file.
|
|
149
|
+
const appended = [
|
|
150
|
+
...(sitemapNeeded ? [sitemap()] : []),
|
|
151
|
+
...(metadataPeerToken === undefined && metadataOptions.enabled
|
|
152
|
+
? [createMetadataFinalizer(() => ({ config: resolvedConfig, build: resolvedBuildInfo, options: metadataOptions }))]
|
|
153
|
+
: []),
|
|
154
|
+
];
|
|
155
|
+
if (appended.length > 0)
|
|
156
|
+
updateConfig({ integrations: appended });
|
|
157
|
+
}
|
|
134
158
|
},
|
|
135
159
|
'astro:config:done'({ config, logger }) {
|
|
136
160
|
resolvedConfig = config;
|
|
137
161
|
checkContentImageRegistration({ config, logger, enabled: contentImagesEnabled });
|
|
138
162
|
},
|
|
139
163
|
'astro:build:done'({ dir, pages, logger }) {
|
|
140
|
-
|
|
164
|
+
try {
|
|
165
|
+
if (enabled && llmsOptions.enabled && resolvedConfig) {
|
|
166
|
+
writeLlmsTxt({ config: resolvedConfig, dir, pages, options: llmsOptions, logger });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
if (metadataPeerToken !== undefined) {
|
|
171
|
+
reportSiteMetadataBuilt(metadataPeerToken);
|
|
172
|
+
metadataPeerToken = undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Integration appended after the sitemap, so its `astro:build:done` runs after
|
|
181
|
+
* `@astrojs/sitemap` has written `sitemap-index.xml`. Hook order follows the
|
|
182
|
+
* integration list, which is why the metadata write does not run in this integration's own hook.
|
|
183
|
+
*/
|
|
184
|
+
function createMetadataFinalizer(source) {
|
|
185
|
+
return {
|
|
186
|
+
name: '@hagicode/hagilight:metadata',
|
|
187
|
+
hooks: {
|
|
188
|
+
'astro:build:done'({ dir, pages, logger }) {
|
|
189
|
+
const { config, build, options } = source();
|
|
190
|
+
if (!config || !build || !options.enabled)
|
|
141
191
|
return;
|
|
142
|
-
|
|
192
|
+
writeAstroSiteMetadata({ config, outDir: fileURLToPath(dir), pages, build, options, logger });
|
|
143
193
|
},
|
|
144
194
|
},
|
|
145
195
|
};
|
package/dist/llms.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export declare const DEFAULT_MAX_LINKS = 120;
|
|
|
38
38
|
* not be listed: `noindex`, instant redirects, 404 pages, and pages whose canonical URL points
|
|
39
39
|
* somewhere else.
|
|
40
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Read the page head without applying indexing rules. Site-level facts such as the
|
|
43
|
+
* site name and description stay available on pages that are `noindex`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseHeadMeta(html: string): LlmsPageMeta;
|
|
41
46
|
export declare function parsePageMeta(html: string, pageUrl: string): LlmsPageMeta | undefined;
|
|
42
47
|
/** Build the `llms.txt` body. Returns `undefined` when there is nothing to list. */
|
|
43
48
|
export declare function buildLlmsTxt(options: BuildLlmsTxtOptions): string | undefined;
|
package/dist/llms.js
CHANGED
|
@@ -46,12 +46,30 @@ function normalizeUrl(href, base) {
|
|
|
46
46
|
* not be listed: `noindex`, instant redirects, 404 pages, and pages whose canonical URL points
|
|
47
47
|
* somewhere else.
|
|
48
48
|
*/
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Read the page head without applying indexing rules. Site-level facts such as the
|
|
51
|
+
* site name and description stay available on pages that are `noindex`.
|
|
52
|
+
*/
|
|
53
|
+
export function parseHeadMeta(html) {
|
|
50
54
|
const headEnd = html.indexOf('</head>');
|
|
51
55
|
const head = headEnd === -1 ? html : html.slice(0, headEnd);
|
|
52
56
|
const metas = [...head.matchAll(/<meta\b[^>]*>/giu)].map(([tag]) => attributes(tag));
|
|
53
57
|
const named = (name) => metas.find((meta) => meta.name?.toLowerCase() === name)?.content;
|
|
54
58
|
const property = (name) => metas.find((meta) => meta.property?.toLowerCase() === name)?.content;
|
|
59
|
+
const title = clean(decodeEntities(head.match(/<title[^>]*>([\s\S]*?)<\/title>/iu)?.[1] ?? '')) ?? '';
|
|
60
|
+
const htmlTag = html.match(/<html\b[^>]*>/iu)?.[0];
|
|
61
|
+
return {
|
|
62
|
+
lang: clean(htmlTag ? attributes(htmlTag).lang : undefined),
|
|
63
|
+
title,
|
|
64
|
+
description: clean(named('description') ?? property('og:description')),
|
|
65
|
+
siteName: clean(property('og:site_name')),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function parsePageMeta(html, pageUrl) {
|
|
69
|
+
const headEnd = html.indexOf('</head>');
|
|
70
|
+
const head = headEnd === -1 ? html : html.slice(0, headEnd);
|
|
71
|
+
const metas = [...head.matchAll(/<meta\b[^>]*>/giu)].map(([tag]) => attributes(tag));
|
|
72
|
+
const named = (name) => metas.find((meta) => meta.name?.toLowerCase() === name)?.content;
|
|
55
73
|
if (/noindex/iu.test(named('robots') ?? ''))
|
|
56
74
|
return undefined;
|
|
57
75
|
if (metas.some((meta) => meta['http-equiv']?.toLowerCase() === 'refresh'))
|
|
@@ -66,17 +84,10 @@ export function parsePageMeta(html, pageUrl) {
|
|
|
66
84
|
if (canonical && own && canonical.replace(/\/$/u, '') !== own.replace(/\/$/u, ''))
|
|
67
85
|
return undefined;
|
|
68
86
|
}
|
|
69
|
-
const
|
|
70
|
-
if (!title || /^(?:404\b|page not found)/iu.test(title))
|
|
87
|
+
const meta = parseHeadMeta(html);
|
|
88
|
+
if (!meta.title || /^(?:404\b|page not found)/iu.test(meta.title))
|
|
71
89
|
return undefined;
|
|
72
|
-
|
|
73
|
-
const lang = clean(htmlTag ? attributes(htmlTag).lang : undefined);
|
|
74
|
-
return {
|
|
75
|
-
lang,
|
|
76
|
-
title,
|
|
77
|
-
description: clean(named('description') ?? property('og:description')),
|
|
78
|
-
siteName: clean(property('og:site_name')),
|
|
79
|
-
};
|
|
90
|
+
return meta;
|
|
80
91
|
}
|
|
81
92
|
function sameLanguage(a, b) {
|
|
82
93
|
if (!a || !b)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BuildInfo } from '@hagicode/hagilight-core/build-info';
|
|
2
|
+
import { type SiteMetadataLocale } from '@hagicode/hagilight-core/site-metadata';
|
|
3
|
+
import { type SiteMetadataLogger } from '@hagicode/hagilight-core/site-metadata-writer';
|
|
4
|
+
export interface WriteAstroSiteMetadataInput {
|
|
5
|
+
config: {
|
|
6
|
+
site?: unknown;
|
|
7
|
+
base?: string;
|
|
8
|
+
publicDir: URL;
|
|
9
|
+
i18n?: unknown;
|
|
10
|
+
};
|
|
11
|
+
outDir: string;
|
|
12
|
+
pages: readonly {
|
|
13
|
+
pathname: string;
|
|
14
|
+
}[];
|
|
15
|
+
build: BuildInfo;
|
|
16
|
+
options: {
|
|
17
|
+
name?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
};
|
|
20
|
+
logger: SiteMetadataLogger;
|
|
21
|
+
}
|
|
22
|
+
/** Map Astro `i18n` to locales relative to the base. Mirrors the RSS locale derivation. */
|
|
23
|
+
export declare function deriveAstroMetadataLocales(i18n: unknown): {
|
|
24
|
+
locales: SiteMetadataLocale[];
|
|
25
|
+
defaultLocale?: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Build and write the metadata file for a plain-Astro build. Skips with a warning
|
|
29
|
+
* when no absolute site URL is configured, because the file has to name the site.
|
|
30
|
+
*/
|
|
31
|
+
export declare function writeAstroSiteMetadata({ config, outDir, pages, build, options, logger }: WriteAstroSiteMetadataInput): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facts for `.well-known/hagilight.metadata.json` from a plain-Astro build. Site
|
|
3
|
+
* name and description come from the built home page, locales from Astro `i18n`,
|
|
4
|
+
* and analytics flags from the loaders present in the built pages.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { buildSiteMetadata, detectAnalyticsLoaders, SITE_METADATA_PATH, } from '@hagicode/hagilight-core/site-metadata';
|
|
10
|
+
import { findDiscoveryOutputs, writeSiteMetadata } from '@hagicode/hagilight-core/site-metadata-writer';
|
|
11
|
+
import { parseHeadMeta } from './llms.js';
|
|
12
|
+
const ASTRO_PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function absoluteSite(site) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = new URL(site);
|
|
19
|
+
return ['http:', 'https:'].includes(parsed.protocol) && !parsed.username && !parsed.password ? parsed.href : undefined;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Read the built HTML of each page that the build emitted, using the same lookup as `llms.txt`. */
|
|
26
|
+
function builtPageHtml(outDir, pages) {
|
|
27
|
+
const html = [];
|
|
28
|
+
for (const { pathname } of pages) {
|
|
29
|
+
const relative = pathname.replace(/^\/+/u, '');
|
|
30
|
+
const candidates = relative === '' || relative.endsWith('/')
|
|
31
|
+
? [join(outDir, relative, 'index.html')]
|
|
32
|
+
: [join(outDir, relative, 'index.html'), join(outDir, `${relative}.html`)];
|
|
33
|
+
const file = candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
|
|
34
|
+
if (file)
|
|
35
|
+
html.push(readFileSync(file, 'utf8'));
|
|
36
|
+
}
|
|
37
|
+
return html;
|
|
38
|
+
}
|
|
39
|
+
function localeEntryKey(entry) {
|
|
40
|
+
if (typeof entry === 'string')
|
|
41
|
+
return entry.trim() || undefined;
|
|
42
|
+
if (isRecord(entry) && typeof entry.path === 'string' && entry.path.trim())
|
|
43
|
+
return entry.path.trim();
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Map Astro `i18n` to locales relative to the base. Mirrors the RSS locale derivation. */
|
|
47
|
+
export function deriveAstroMetadataLocales(i18n) {
|
|
48
|
+
if (!isRecord(i18n) || !Array.isArray(i18n.locales) || i18n.locales.length === 0)
|
|
49
|
+
return { locales: [] };
|
|
50
|
+
const defaultLocale = typeof i18n.defaultLocale === 'string'
|
|
51
|
+
? i18n.defaultLocale
|
|
52
|
+
: localeEntryKey(i18n.locales[0]);
|
|
53
|
+
const prefixDefaultLocale = isRecord(i18n.routing) && i18n.routing.prefixDefaultLocale === true;
|
|
54
|
+
const locales = [];
|
|
55
|
+
for (const entry of i18n.locales) {
|
|
56
|
+
const code = localeEntryKey(entry);
|
|
57
|
+
if (!code)
|
|
58
|
+
continue;
|
|
59
|
+
const codes = isRecord(entry) && Array.isArray(entry.codes) ? entry.codes : [];
|
|
60
|
+
const lang = typeof codes[0] === 'string' && codes[0].trim() ? codes[0].trim() : code;
|
|
61
|
+
const isDefault = code === defaultLocale;
|
|
62
|
+
locales.push({ code, lang, path: isDefault && !prefixDefaultLocale ? '/' : `/${code}/` });
|
|
63
|
+
}
|
|
64
|
+
return { locales, ...(defaultLocale === undefined ? {} : { defaultLocale }) };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build and write the metadata file for a plain-Astro build. Skips with a warning
|
|
68
|
+
* when no absolute site URL is configured, because the file has to name the site.
|
|
69
|
+
*/
|
|
70
|
+
export function writeAstroSiteMetadata({ config, outDir, pages, build, options, logger }) {
|
|
71
|
+
const siteUrl = absoluteSite(config.site);
|
|
72
|
+
if (!siteUrl) {
|
|
73
|
+
logger.warn(`${SITE_METADATA_PATH} requires an absolute Astro site URL, so it was not generated.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const base = `${config.base || '/'}`.replace(/\/?$/u, '/');
|
|
77
|
+
const siteBase = new URL(base, siteUrl);
|
|
78
|
+
const homeFile = join(outDir, 'index.html');
|
|
79
|
+
const homeHtml = existsSync(homeFile) ? readFileSync(homeFile, 'utf8') : undefined;
|
|
80
|
+
// Site-level facts are read even when the home page is noindex, such as a root redirect page.
|
|
81
|
+
const homeMeta = homeHtml === undefined ? undefined : parseHeadMeta(homeHtml);
|
|
82
|
+
const pageHtml = builtPageHtml(outDir, pages);
|
|
83
|
+
if (homeHtml !== undefined && !pageHtml.includes(homeHtml))
|
|
84
|
+
pageHtml.push(homeHtml);
|
|
85
|
+
const locales = deriveAstroMetadataLocales(config.i18n);
|
|
86
|
+
const metadata = buildSiteMetadata({
|
|
87
|
+
generator: { name: '@hagicode/hagilight', version: ASTRO_PACKAGE_VERSION },
|
|
88
|
+
site: {
|
|
89
|
+
name: options.name ?? homeMeta?.siteName ?? homeMeta?.title,
|
|
90
|
+
url: siteUrl,
|
|
91
|
+
base,
|
|
92
|
+
description: options.description ?? homeMeta?.description,
|
|
93
|
+
},
|
|
94
|
+
locales: locales.locales,
|
|
95
|
+
defaultLocale: locales.defaultLocale,
|
|
96
|
+
fallbackLang: homeMeta?.lang,
|
|
97
|
+
build,
|
|
98
|
+
discovery: findDiscoveryOutputs(outDir),
|
|
99
|
+
analytics: detectAnalyticsLoaders(pageHtml),
|
|
100
|
+
});
|
|
101
|
+
writeSiteMetadata({
|
|
102
|
+
outDir,
|
|
103
|
+
metadata,
|
|
104
|
+
siteOwnedFiles: [join(fileURLToPath(config.publicDir), SITE_METADATA_PATH)],
|
|
105
|
+
logger,
|
|
106
|
+
});
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hagicode/hagilight",
|
|
3
|
-
"version": "0.11.1-dev.
|
|
3
|
+
"version": "0.11.1-dev.133.1.338317a",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Astro integrations and components for HagiCode sites",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@astrojs/sitemap": "^3.7.4",
|
|
43
|
-
"@hagicode/hagilight-core": "0.11.1-dev.
|
|
43
|
+
"@hagicode/hagilight-core": "0.11.1-dev.133.1.338317a"
|
|
44
44
|
},
|
|
45
45
|
"keywords": [
|
|
46
46
|
"hagicode",
|