@hagicode/hagilight-starlight 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.
@@ -57,7 +57,8 @@ const gallery = copy.gallery.flatMap((item) => {
57
57
  href={homeHref}
58
58
  {...showcaseTag('navigation', 'home')}
59
59
  >
60
- {copy.visitLabel}
60
+ <span>{copy.visitLabel}</span>
61
+ <svg class="hagilight-article-promotion__arrow" aria-hidden="true" focusable="false" viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10h11M11 5l5 5-5 5" /></svg>
61
62
  </a>
62
63
  <a
63
64
  class="hagilight-article-promotion__cta hagilight-article-promotion__cta--secondary"
@@ -137,7 +138,7 @@ const gallery = copy.gallery.flatMap((item) => {
137
138
  class="hagilight-article-promotion__downloads"
138
139
  href={downloadsHref}
139
140
  {...showcaseTag('download', 'downloadClient')}
140
- >{copy.allDownloadsLabel}</a>
141
+ ><span>{copy.allDownloadsLabel}</span><svg class="hagilight-article-promotion__arrow" aria-hidden="true" focusable="false" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10h11M11 5l5 5-5 5" /></svg></a>
141
142
  </div>
142
143
  </div>
143
144
 
@@ -151,43 +152,74 @@ const gallery = copy.gallery.flatMap((item) => {
151
152
 
152
153
  <style>
153
154
  /*
154
- * Zones, top to bottom: pitch (what it is, where to go, the three pillars), gallery (proof),
155
- * closing (get it, share it). Zones are separated by space and hairlines rather than nested
156
- * boxes. The topology follows the section's own width, so the wide-content toggle and the
157
- * sidebar layouts reflow it, not just the viewport. Mobile-first: the base rules are the
158
- * single-column layout, which is also what a browser without container queries renders.
155
+ * One stage, four beats, top to bottom: pitch (what it is, the way in, the three pillars),
156
+ * gallery (proof), closing band (get it, share it). The stage is tinted from the active theme's
157
+ * accent so it reads as a deliberate HagiCode moment against the article's neutral page, and the
158
+ * closing band bleeds to the stage edges so "get it" is the last, heaviest surface. Pillars are
159
+ * separated by accent rules rather than boxes. The topology follows the section's own width, so
160
+ * the wide-content toggle and the sidebar layouts reflow it, not just the viewport. Mobile-first:
161
+ * the base rules are the single-column layout, which is also what a browser without container
162
+ * queries renders. Every color derives from Starlight's tokens, so all themes and both modes work.
159
163
  */
160
164
  .hagilight-article-promotion {
165
+ --hagilight-promo-pad: 1.5rem;
166
+ --hagilight-promo-edge: color-mix(in srgb, var(--sl-color-text-accent) 38%, var(--sl-color-gray-5));
167
+ --hagilight-promo-tint: color-mix(in srgb, var(--sl-color-accent-low) 55%, var(--sl-color-bg-nav));
168
+ position: relative;
161
169
  container-type: inline-size;
162
170
  display: grid;
163
171
  grid-template-columns: minmax(0, 1fr);
164
- gap: 2rem;
172
+ gap: 2.25rem;
165
173
  margin: 3rem auto 2rem;
166
- padding: clamp(1rem, 3vw, 1.75rem);
167
- border: 1px solid var(--sl-color-gray-5);
174
+ padding: calc(var(--hagilight-promo-pad) + 0.25rem) var(--hagilight-promo-pad) var(--hagilight-promo-pad);
175
+ overflow: clip;
176
+ border: 1px solid var(--hagilight-promo-edge);
168
177
  border-radius: var(--sl-radius-lg, 0.75rem);
169
- background: var(--sl-color-bg-nav);
178
+ background:
179
+ radial-gradient(70% 45% at 0% 0%, color-mix(in srgb, var(--sl-color-text-accent) 16%, transparent), transparent 70%),
180
+ linear-gradient(180deg, var(--hagilight-promo-tint), var(--sl-color-bg-nav) 55%);
170
181
  color: var(--sl-color-text);
171
182
  }
172
183
 
184
+ /* The accent rule across the top edge: the stage's signature, drawn in on scroll where supported. */
185
+ .hagilight-article-promotion::before {
186
+ content: '';
187
+ position: absolute;
188
+ inset: 0 0 auto;
189
+ height: 4px;
190
+ background: linear-gradient(90deg, var(--sl-color-text-accent), color-mix(in srgb, var(--sl-color-text-accent) 20%, transparent));
191
+ transform-origin: left center;
192
+ pointer-events: none;
193
+ }
194
+
195
+ .hagilight-article-promotion ::selection {
196
+ background: color-mix(in srgb, var(--sl-color-text-accent) 38%, transparent);
197
+ color: var(--sl-color-white);
198
+ }
199
+
173
200
  .hagilight-article-promotion h2,
174
201
  .hagilight-article-promotion h3 {
175
202
  margin: 0;
176
203
  color: var(--sl-color-white);
177
- line-height: 1.25;
204
+ line-height: 1.2;
205
+ text-wrap: balance;
178
206
  }
179
207
 
180
208
  .hagilight-article-promotion h2 {
181
- font-size: var(--sl-text-2xl, 1.5rem);
209
+ font-size: clamp(2rem, 1.3rem + 3.2cqi, 3rem);
210
+ font-weight: 800;
211
+ letter-spacing: -0.02em;
182
212
  }
183
213
 
184
214
  .hagilight-article-promotion h3 {
185
- font-size: var(--sl-text-lg, 1.125rem);
215
+ font-size: var(--sl-text-xl, 1.25rem);
216
+ font-weight: 700;
186
217
  }
187
218
 
188
219
  .hagilight-article-promotion p {
189
220
  margin: 0;
190
221
  line-height: 1.6;
222
+ text-wrap: pretty;
191
223
  }
192
224
 
193
225
  .hagilight-article-promotion ul {
@@ -201,17 +233,17 @@ const gallery = copy.gallery.flatMap((item) => {
201
233
  outline-offset: 3px;
202
234
  }
203
235
 
204
- /* Pitch: headline, definition, promise, then the way in. */
236
+ /* Pitch: headline, one emphasized promise, the way in, then the pillars. */
205
237
  .hagilight-article-promotion__pitch {
206
238
  display: grid;
207
- gap: 1.75rem;
239
+ gap: 2rem;
208
240
  }
209
241
 
210
242
  .hagilight-article-promotion__intro {
211
243
  display: grid;
212
244
  align-content: start;
213
245
  justify-items: start;
214
- gap: 0.75rem;
246
+ gap: 0.875rem;
215
247
  }
216
248
 
217
249
  .hagilight-article-promotion__lead,
@@ -219,9 +251,15 @@ const gallery = copy.gallery.flatMap((item) => {
219
251
  max-inline-size: 68ch;
220
252
  }
221
253
 
254
+ .hagilight-article-promotion__lead {
255
+ color: var(--sl-color-gray-2);
256
+ }
257
+
222
258
  .hagilight-article-promotion__tagline {
223
259
  color: var(--sl-color-white);
224
- font-weight: 600;
260
+ font-size: var(--sl-text-lg, 1.125rem);
261
+ font-weight: 650;
262
+ line-height: 1.45;
225
263
  }
226
264
 
227
265
  .hagilight-article-promotion__actions {
@@ -236,32 +274,60 @@ const gallery = copy.gallery.flatMap((item) => {
236
274
  display: inline-flex;
237
275
  align-items: center;
238
276
  justify-content: center;
277
+ gap: 0.5rem;
239
278
  min-height: 2.75rem;
240
- padding: 0.5rem 1rem;
279
+ padding: 0.5rem 1.25rem;
241
280
  border: 1px solid var(--sl-color-text-accent);
242
281
  border-radius: var(--sl-radius-sm, 0.375rem);
243
282
  font-weight: 700;
244
283
  text-decoration: none;
284
+ transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.2s cubic-bezier(0.22, 1, 0.36, 1), background-color 0.2s;
245
285
  }
246
286
 
287
+ /* The one loud control: bigger than its sibling, lifted, with an arrow that leans forward on hover. */
247
288
  .hagilight-article-promotion__cta--primary {
289
+ min-height: 3.25rem;
290
+ padding-inline: 1.625rem;
248
291
  background: var(--sl-color-text-accent);
249
292
  color: var(--sl-color-bg);
293
+ font-size: var(--sl-text-lg, 1.125rem);
294
+ box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--sl-color-text-accent) 70%, transparent);
250
295
  }
251
296
 
252
297
  .hagilight-article-promotion__cta--secondary {
253
298
  background: transparent;
299
+ color: var(--sl-color-white);
300
+ border-color: var(--sl-color-gray-3);
301
+ }
302
+
303
+ .hagilight-article-promotion__arrow {
304
+ flex: none;
305
+ transition: transform 0.2s cubic-bezier(0.22, 1, 0.36, 1);
306
+ }
307
+
308
+ .hagilight-article-promotion__cta--primary:hover {
309
+ transform: translateY(-2px);
310
+ box-shadow: 0 12px 24px -8px color-mix(in srgb, var(--sl-color-text-accent) 80%, transparent);
311
+ }
312
+
313
+ .hagilight-article-promotion__cta--primary:active {
314
+ transform: translateY(0);
315
+ }
316
+
317
+ .hagilight-article-promotion__cta--secondary:hover {
318
+ border-color: var(--sl-color-text-accent);
254
319
  color: var(--sl-color-text-accent);
255
320
  }
256
321
 
257
- .hagilight-article-promotion__cta:hover {
258
- filter: brightness(1.08);
322
+ .hagilight-article-promotion__cta:hover .hagilight-article-promotion__arrow,
323
+ .hagilight-article-promotion__downloads:hover .hagilight-article-promotion__arrow {
324
+ transform: translateX(3px);
259
325
  }
260
326
 
261
- /* Pillars: one hairline-topped row each, icon and word on a line, description under it. */
327
+ /* Pillars: each opens with an accent rule; the pillar word is the loudest text after the title. */
262
328
  .hagilight-article-promotion__features {
263
329
  display: grid;
264
- gap: 1rem 1.5rem;
330
+ gap: 1.25rem 1.5rem;
265
331
  }
266
332
 
267
333
  .hagilight-article-promotion__features li {
@@ -269,16 +335,17 @@ const gallery = copy.gallery.flatMap((item) => {
269
335
  grid-template-columns: auto minmax(0, 1fr);
270
336
  align-content: start;
271
337
  align-items: center;
272
- gap: 0.5rem 0.625rem;
338
+ gap: 0.625rem 0.75rem;
273
339
  min-width: 0;
274
- padding-block-start: 0.875rem;
275
- border-block-start: 1px solid var(--sl-color-gray-5);
340
+ padding-block-start: 1rem;
341
+ border-block-start: 2px solid color-mix(in srgb, var(--sl-color-text-accent) 55%, transparent);
276
342
  }
277
343
 
278
344
  .hagilight-article-promotion__features strong {
279
345
  color: var(--sl-color-text-accent);
280
- font-size: var(--sl-text-lg, 1.125rem);
281
- line-height: 1.25;
346
+ font-size: var(--sl-text-xl, 1.25rem);
347
+ font-weight: 750;
348
+ line-height: 1.2;
282
349
  }
283
350
 
284
351
  .hagilight-article-promotion__feature-text {
@@ -288,6 +355,17 @@ const gallery = copy.gallery.flatMap((item) => {
288
355
  }
289
356
 
290
357
  .hagilight-article-promotion__icon {
358
+ position: relative;
359
+ display: grid;
360
+ place-items: center;
361
+ width: 2.5rem;
362
+ height: 2.5rem;
363
+ border-radius: var(--sl-radius-md, 0.5rem);
364
+ background: color-mix(in srgb, var(--sl-color-text-accent) 18%, transparent);
365
+ }
366
+
367
+ .hagilight-article-promotion__icon::after {
368
+ content: '';
291
369
  width: 1.5rem;
292
370
  height: 1.5rem;
293
371
  background: var(--sl-color-text-accent);
@@ -298,12 +376,12 @@ const gallery = copy.gallery.flatMap((item) => {
298
376
  /* Gallery: three audited screenshots, kept in the catalog's narrative order. */
299
377
  .hagilight-article-promotion__gallery-block {
300
378
  display: grid;
301
- gap: 1rem;
379
+ gap: 1.125rem;
302
380
  }
303
381
 
304
382
  .hagilight-article-promotion__gallery {
305
383
  display: grid;
306
- gap: 1.25rem 1rem;
384
+ gap: 1.5rem 1.25rem;
307
385
  }
308
386
 
309
387
  .hagilight-article-promotion__gallery li {
@@ -312,7 +390,7 @@ const gallery = copy.gallery.flatMap((item) => {
312
390
 
313
391
  .hagilight-article-promotion figure {
314
392
  display: grid;
315
- gap: 0.5rem;
393
+ gap: 0.625rem;
316
394
  margin: 0;
317
395
  }
318
396
 
@@ -324,22 +402,31 @@ const gallery = copy.gallery.flatMap((item) => {
324
402
  display: block;
325
403
  width: 100%;
326
404
  height: auto;
327
- border: 1px solid var(--sl-color-gray-5);
405
+ border: 1px solid var(--hagilight-promo-edge);
328
406
  border-radius: var(--sl-radius-md, 0.5rem);
407
+ box-shadow: 0 18px 32px -18px color-mix(in srgb, var(--sl-color-black) 80%, transparent);
408
+ transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.35s;
409
+ }
410
+
411
+ .hagilight-article-promotion figure:hover img {
412
+ transform: translateY(-3px);
413
+ border-color: var(--sl-color-text-accent);
329
414
  }
330
415
 
331
416
  .hagilight-article-promotion figcaption {
332
- color: var(--sl-color-gray-2);
417
+ color: var(--sl-color-gray-1);
333
418
  font-size: var(--sl-text-sm, 0.875rem);
334
419
  line-height: 1.5;
335
420
  }
336
421
 
337
- /* Closing: the practical next steps, as two parallel headed blocks. */
422
+ /* Closing: a full-bleed band, the heaviest surface, so "get it" is where the eye lands last. */
338
423
  .hagilight-article-promotion__closing {
339
424
  display: grid;
340
- gap: 1.5rem;
341
- padding-block-start: 1.5rem;
342
- border-block-start: 1px solid var(--sl-color-gray-5);
425
+ gap: 1.75rem;
426
+ margin: 0 calc(var(--hagilight-promo-pad) * -1) calc(var(--hagilight-promo-pad) * -1);
427
+ padding: var(--hagilight-promo-pad);
428
+ border-block-start: 1px solid var(--hagilight-promo-edge);
429
+ background: color-mix(in srgb, var(--sl-color-accent-low) 80%, var(--sl-color-bg));
343
430
  }
344
431
 
345
432
  .hagilight-article-promotion__windows,
@@ -347,7 +434,7 @@ const gallery = copy.gallery.flatMap((item) => {
347
434
  display: grid;
348
435
  align-content: start;
349
436
  justify-items: start;
350
- gap: 0.5rem;
437
+ gap: 0.625rem;
351
438
  min-width: 0;
352
439
  }
353
440
 
@@ -356,7 +443,7 @@ const gallery = copy.gallery.flatMap((item) => {
356
443
  display: flex;
357
444
  flex-wrap: wrap;
358
445
  align-items: center;
359
- gap: 0 1.25rem;
446
+ gap: 0 1.5rem;
360
447
  max-width: 100%;
361
448
  }
362
449
 
@@ -378,15 +465,18 @@ const gallery = copy.gallery.flatMap((item) => {
378
465
  .hagilight-article-promotion__downloads {
379
466
  display: inline-flex;
380
467
  align-items: center;
468
+ gap: 0.375rem;
381
469
  min-height: 2.75rem;
382
470
  color: var(--sl-color-text-accent);
383
- font-weight: 600;
471
+ font-weight: 650;
472
+ text-decoration-thickness: 1px;
473
+ text-underline-offset: 0.25em;
384
474
  }
385
475
 
386
476
  .hagilight-article-promotion__share-text {
387
477
  justify-self: stretch;
388
478
  padding: 0.75rem 1rem;
389
- border: 1px dashed var(--sl-color-gray-4);
479
+ border: 1px dashed var(--sl-color-text-accent);
390
480
  border-radius: var(--sl-radius-md, 0.5rem);
391
481
  background: var(--sl-color-bg);
392
482
  overflow-wrap: anywhere;
@@ -452,4 +542,53 @@ const gallery = copy.gallery.flatMap((item) => {
452
542
  justify-self: stretch;
453
543
  }
454
544
  }
545
+
546
+ /*
547
+ * The one authored moment: as the stage scrolls into view its accent rule draws across and the
548
+ * screenshots rise into place one after another. Scroll-driven, so there is no JS and nothing
549
+ * replays; engines without view timelines, and readers who ask for less motion, get the final
550
+ * state. The animations start at 0.35 opacity, so a tall item that never reaches its full entry
551
+ * range is still readable.
552
+ */
553
+ @media (prefers-reduced-motion: no-preference) {
554
+ @supports (animation-timeline: view()) {
555
+ .hagilight-article-promotion::before {
556
+ animation-name: hagilight-promotion-rule;
557
+ animation-timing-function: linear;
558
+ animation-fill-mode: both;
559
+ animation-timeline: view();
560
+ animation-range: entry 0% entry 80%;
561
+ }
562
+
563
+ .hagilight-article-promotion__gallery li {
564
+ animation-name: hagilight-promotion-rise;
565
+ animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
566
+ animation-fill-mode: both;
567
+ animation-timeline: view();
568
+ animation-range: entry 5% entry 45%;
569
+ }
570
+
571
+ .hagilight-article-promotion__gallery li:nth-child(2) {
572
+ animation-range: entry 12% entry 52%;
573
+ }
574
+
575
+ .hagilight-article-promotion__gallery li:nth-child(3) {
576
+ animation-range: entry 19% entry 59%;
577
+ }
578
+ }
579
+ }
580
+
581
+ @keyframes hagilight-promotion-rule {
582
+ from {
583
+ transform: scaleX(0.04);
584
+ }
585
+ }
586
+
587
+ @keyframes hagilight-promotion-rise {
588
+ from {
589
+ opacity: 0.35;
590
+ translate: 0 1.75rem;
591
+ scale: 0.97;
592
+ }
593
+ }
455
594
  </style>
package/README.md CHANGED
@@ -82,6 +82,15 @@ hagilight({ themes: { enabled: false } })
82
82
 
83
83
  The plugin footer shows the same build line as the Astro package: `Branch:`, `Commit:`, and `Built:` after the attribution. It is always on and has no option of its own. The values come from `HAGILIGHT_BUILD_BRANCH` and `HAGILIGHT_BUILD_COMMIT`, then Git, then `unknown`; the core README gives the order. Detached-HEAD CI checkouts report `Branch: unknown` unless the build step sets `HAGILIGHT_BUILD_BRANCH`, as shown in the Astro README. Footer components mounted directly in a page (`@hagicode/hagilight-starlight/Footer` and `PromotoFooter`) render no build text.
84
84
 
85
+ ### Site metadata
86
+
87
+ The plugin writes `/.well-known/hagilight.metadata.json` once per build, after the sitemap, so `discovery` lists the sitemap and any RSS feed that exist. The file's schema and privacy rules are in `@hagicode/hagilight-core`'s README.
88
+
89
+ - `metadata: false` opts out. `metadata: { name, description }` sets the values; the name defaults to the Starlight `title` for the default locale, then the site hostname.
90
+ - Analytics flags report the resolved options. A provider you disable with `analytics` reads `false`. The IDs and keys are never written.
91
+ - When the plain-Astro `hagilight()` integration is also registered, this plugin writes the file, including `llms.txt` in `discovery`, whatever the order of registration. Set `metadata: false` here to let the plain-Astro integration write it instead.
92
+ - GitHub Pages needs a `.nojekyll` file in the published root, or the `.well-known` directory is dropped. The `/docs/` base path puts the file at `<site URL>/docs/.well-known/hagilight.metadata.json`.
93
+
85
94
  ### Body images
86
95
 
87
96
  `hagilight()` registers the content-image plugin from `@hagicode/hagilight-core/content-images` for the whole site. Every Markdown or MDX body image outside a `<picture>` element gets `loading="lazy"`, `decoding="async"`, and the class `hagilight-content-image`. Values the author set are kept, and logos and showcase images are not changed.
package/dist/index.d.ts CHANGED
@@ -55,7 +55,22 @@ export interface HagilightContentComponentsOptions {
55
55
  markdownContent?: boolean;
56
56
  }
57
57
  export type HagilightAIDisclosureOptions = Partial<AIDisclosureDefaults>;
58
+ export interface HagilightMetadataOptions {
59
+ /** Defaults to `true`. */
60
+ enabled?: boolean;
61
+ /** Site name. Defaults to the Starlight `title` for the default locale, then the site hostname. */
62
+ name?: string;
63
+ /** Site description. Defaults to the Starlight `description` when the site sets one. */
64
+ description?: string;
65
+ }
58
66
  export interface HagilightStarlightOptions {
67
+ /**
68
+ * Generate `/.well-known/hagilight.metadata.json` after the build. Defaults to `true`; `false`
69
+ * opts out, and an options object may set `name` and `description`. A site-provided file is
70
+ * never overwritten. When a plain-Astro `hagilight()` is also registered, this plugin writes
71
+ * the file and the plain-Astro integration defers to it.
72
+ */
73
+ metadata?: boolean | HagilightMetadataOptions;
59
74
  header?: HagilightToggleOptions;
60
75
  notFoundPage?: HagilightToggleOptions;
61
76
  rss?: HagilightRssOptions;
package/dist/index.js CHANGED
@@ -7,8 +7,11 @@ import { DEFAULT_POSTHOG_API_KEY, POSTHOG_API_KEY_PATTERN } from '@hagicode/hagi
7
7
  import { resolveRssLocales } from '@hagicode/hagilight-core/rss';
8
8
  import { RSS_OWNER, registerStarlightRssOwner, } from '@hagicode/hagilight-core/rss-ownership';
9
9
  import { isValidSeoImageReference, resolveSeoImageUrl } from '@hagicode/hagilight-core/seo';
10
+ import { resolveSiteMetadataOption } from '@hagicode/hagilight-core/site-metadata';
11
+ import { registerStarlightSiteMetadataOwner } from '@hagicode/hagilight-core/site-metadata-ownership';
10
12
  import { resolveRssOptions } from './rss-utils.js';
11
13
  import { resolveSeoLocales } from './seo-utils.js';
14
+ import { createStarlightMetadataFinalizer } from './site-metadata.js';
12
15
  import { THEME_BOOTSTRAP } from './themes.js';
13
16
  const packageRoot = new URL('../', import.meta.url);
14
17
  const packageFile = (name) => fileURLToPath(new URL(name, packageRoot));
@@ -202,7 +205,7 @@ function resolveDefaultSeoLocale(defaultLocale, locales) {
202
205
  }
203
206
  return found.route;
204
207
  }
205
- function createConfiguredIntegration(instanceId, serializedOptions, rssConfig, rssLocaleFeedUrls, componentIds, getConfiguredRssFeed, generateRss, unregisterRssOwner, contentImagesEnabled) {
208
+ function createConfiguredIntegration(instanceId, serializedOptions, rssConfig, rssLocaleFeedUrls, componentIds, getConfiguredRssFeed, generateRss, unregisterRssOwner, contentImagesEnabled, siteMetadata, unregisterSiteMetadataOwner) {
206
209
  const optionsId = `virtual:hagilight-starlight/${instanceId}/options`;
207
210
  const rssConfigId = 'virtual:hagilight-starlight/rss-config';
208
211
  const headerPath = componentIds.header ? packageFile('Header.astro') : undefined;
@@ -328,10 +331,18 @@ ${serializedOptions.posthogApiKey ? '<PostHog apiKey={options.posthogApiKey} />'
328
331
  prerender: true,
329
332
  });
330
333
  }
331
- updateConfig({ vite: { plugins: [vitePlugin] } });
334
+ // Starlight splices its own sitemap into the integration list before this setup runs, so
335
+ // the finalizer appended in the same update runs after the sitemap has written its index.
336
+ updateConfig({
337
+ vite: { plugins: [vitePlugin] },
338
+ ...(siteMetadata.enabled
339
+ ? { integrations: [createStarlightMetadataFinalizer(siteMetadata, () => buildInfo)] }
340
+ : {}),
341
+ });
332
342
  },
333
343
  'astro:config:done'({ config, logger }) {
334
344
  unregisterRssOwner();
345
+ unregisterSiteMetadataOwner();
335
346
  checkContentImageRegistration({ config, logger, enabled: contentImagesEnabled });
336
347
  if (config.image?.service?.entrypoint === NOOP_IMAGE_SERVICE) {
337
348
  logger.warn(NOOP_IMAGE_SERVICE_WARNING);
@@ -416,6 +427,8 @@ export default function hagilight(options = {}) {
416
427
  seo,
417
428
  };
418
429
  const unregisterRssOwner = registerStarlightRssOwner(rssEnabled);
430
+ const metadata = resolveSiteMetadataOption(input.metadata);
431
+ const unregisterSiteMetadataOwner = registerStarlightSiteMetadataOwner(metadata.enabled);
419
432
  return {
420
433
  name: '@hagicode/hagilight-starlight',
421
434
  hooks: {
@@ -495,7 +508,24 @@ export default function hagilight(options = {}) {
495
508
  new URL(`rss.${filename}.xml`, baseUrl).toString(),
496
509
  ]))
497
510
  : {};
498
- addIntegration(createConfiguredIntegration(instanceId, serializedOptions, { options: rssOptions, locales: rssLocales }, rssLocaleFeedUrls, componentIds, () => getRssFeedUrl(config) ?? rssFeedUrl, generateRss, unregisterRssOwner, contentImagesEnabled));
511
+ addIntegration(createConfiguredIntegration(instanceId, serializedOptions, { options: rssOptions, locales: rssLocales }, rssLocaleFeedUrls, componentIds, () => getRssFeedUrl(config) ?? rssFeedUrl, generateRss, unregisterRssOwner, contentImagesEnabled, {
512
+ enabled: metadata.enabled,
513
+ name: metadata.name,
514
+ description: metadata.description,
515
+ title: config.title,
516
+ siteDescription: config.description,
517
+ locales: config.locales,
518
+ defaultLocale: config.defaultLocale,
519
+ site: astroConfig.site,
520
+ base: astroConfig.base,
521
+ publicDir: astroConfig.publicDir,
522
+ analytics: {
523
+ googleAnalytics: serializedOptions.googleAnalyticsMeasurementId !== undefined,
524
+ fiftyOneLa: serializedOptions.fiftyOneLaId !== undefined,
525
+ posthog: serializedOptions.posthogApiKey !== undefined,
526
+ },
527
+ features: { seo: seo.enabled, promoto: promotoEnabled },
528
+ }, unregisterSiteMetadataOwner));
499
529
  updateConfig({
500
530
  ...(config.logo === undefined ? { logo: { src: defaultLogo, alt: 'HagiCode' } } : {}),
501
531
  customCss: [
@@ -0,0 +1,38 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import type { BuildInfo } from '@hagicode/hagilight-core/build-info';
3
+ import { type SiteMetadataAnalytics, type SiteMetadataFeatures } from '@hagicode/hagilight-core/site-metadata';
4
+ import { type SiteMetadataLogger } from '@hagicode/hagilight-core/site-metadata-writer';
5
+ export interface StarlightSiteMetadataSetup {
6
+ enabled: boolean;
7
+ /** Explicit `metadata.name`, when set. */
8
+ name?: string | undefined;
9
+ /** Explicit `metadata.description`, when set. */
10
+ description?: string | undefined;
11
+ /** Starlight `title`: a string or a localized record. */
12
+ title: unknown;
13
+ /** Starlight `description`, when the site configures one. */
14
+ siteDescription?: unknown;
15
+ /** Starlight `locales` map, for example `{ root: { lang: 'en' } }`. */
16
+ locales: unknown;
17
+ /** Starlight `defaultLocale`. */
18
+ defaultLocale: unknown;
19
+ site: unknown;
20
+ base: string | undefined;
21
+ publicDir: URL;
22
+ analytics: SiteMetadataAnalytics;
23
+ features: SiteMetadataFeatures;
24
+ }
25
+ export interface WriteStarlightSiteMetadataInput {
26
+ outDir: string;
27
+ setup: StarlightSiteMetadataSetup;
28
+ build: BuildInfo;
29
+ logger: SiteMetadataLogger;
30
+ }
31
+ /** Build and write the metadata file for a Starlight build. Skips with a warning when no absolute site URL exists. */
32
+ export declare function writeStarlightSiteMetadata({ outDir, setup, build, logger }: WriteStarlightSiteMetadataInput): void;
33
+ /**
34
+ * Integration appended after this plugin's sitemap, so the write runs after
35
+ * `@astrojs/sitemap` has written its index. The owner's write also waits for any
36
+ * deferring plain-Astro peer that reported before it.
37
+ */
38
+ export declare function createStarlightMetadataFinalizer(setup: StarlightSiteMetadataSetup, buildInfo: () => BuildInfo | undefined): AstroIntegration;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Facts for `.well-known/hagilight.metadata.json` from a Starlight build. The
3
+ * Starlight plugin owns the file when it is enabled, so this module collects the
4
+ * title, locales and resolved feature flags from its own configuration.
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { buildSiteMetadata, SITE_METADATA_PATH, } from '@hagicode/hagilight-core/site-metadata';
10
+ import { findDiscoveryOutputs, writeSiteMetadata } from '@hagicode/hagilight-core/site-metadata-writer';
11
+ import { completeSiteMetadataOwnerBuild } from '@hagicode/hagilight-core/site-metadata-ownership';
12
+ import { resolveSeoLocales } from './seo-utils.js';
13
+ const STARLIGHT_PACKAGE_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
14
+ function isRecord(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
16
+ }
17
+ function absoluteSite(site) {
18
+ try {
19
+ const parsed = new URL(site);
20
+ return ['http:', 'https:'].includes(parsed.protocol) && !parsed.username && !parsed.password ? parsed.href : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ function trimmedText(value) {
27
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
28
+ }
29
+ /** Starlight locale routes become metadata locales. The root route uses its language tag as the code. */
30
+ function toMetadataLocales(locales) {
31
+ const routes = new Map();
32
+ const entries = locales.map(({ route, lang }) => {
33
+ const code = route === 'root' ? lang : route;
34
+ routes.set(route, code);
35
+ return { code, lang, path: route === 'root' ? '/' : `/${route}/` };
36
+ });
37
+ return { locales: entries, routes };
38
+ }
39
+ /** Starlight's default locale is the route named in `defaultLocale`, or `root`. */
40
+ function defaultRoute(defaultLocale, locales) {
41
+ const configured = isRecord(defaultLocale) ? defaultLocale.locale ?? defaultLocale.lang : defaultLocale;
42
+ if (typeof configured !== 'string' || !configured.trim())
43
+ return locales.find(({ route }) => route === 'root')?.route ?? locales[0]?.route ?? 'root';
44
+ const match = locales.find(({ route, lang }) => route === configured || lang === configured);
45
+ return match?.route ?? configured;
46
+ }
47
+ /** Starlight's title, read for the default locale. A localized record uses `root` or its first entry. */
48
+ function siteTitle(title, route) {
49
+ if (typeof title === 'string')
50
+ return trimmedText(title);
51
+ if (!isRecord(title))
52
+ return undefined;
53
+ return trimmedText(title[route] ?? title.root ?? Object.values(title)[0]);
54
+ }
55
+ /** Build and write the metadata file for a Starlight build. Skips with a warning when no absolute site URL exists. */
56
+ export function writeStarlightSiteMetadata({ outDir, setup, build, logger }) {
57
+ const siteUrl = absoluteSite(setup.site);
58
+ if (!siteUrl) {
59
+ logger.warn(`${SITE_METADATA_PATH} requires an absolute Astro site URL, so it was not generated.`);
60
+ return;
61
+ }
62
+ const seoLocales = resolveSeoLocales(setup.locales);
63
+ const { locales, routes } = toMetadataLocales(seoLocales);
64
+ const defaultCode = routes.get(defaultRoute(setup.defaultLocale, seoLocales));
65
+ const route = defaultRoute(setup.defaultLocale, seoLocales);
66
+ const base = `${setup.base || '/'}`.replace(/\/?$/u, '/');
67
+ const metadata = buildSiteMetadata({
68
+ generator: { name: '@hagicode/hagilight-starlight', version: STARLIGHT_PACKAGE_VERSION },
69
+ site: {
70
+ name: setup.name ?? siteTitle(setup.title, route),
71
+ url: siteUrl,
72
+ base,
73
+ description: setup.description ?? trimmedText(setup.siteDescription),
74
+ },
75
+ locales,
76
+ defaultLocale: defaultCode,
77
+ fallbackLang: seoLocales.find((entry) => entry.route === route)?.lang,
78
+ build,
79
+ discovery: findDiscoveryOutputs(outDir),
80
+ analytics: setup.analytics,
81
+ features: setup.features,
82
+ });
83
+ writeSiteMetadata({
84
+ outDir,
85
+ metadata,
86
+ siteOwnedFiles: [join(fileURLToPath(setup.publicDir), SITE_METADATA_PATH)],
87
+ logger,
88
+ });
89
+ }
90
+ /**
91
+ * Integration appended after this plugin's sitemap, so the write runs after
92
+ * `@astrojs/sitemap` has written its index. The owner's write also waits for any
93
+ * deferring plain-Astro peer that reported before it.
94
+ */
95
+ export function createStarlightMetadataFinalizer(setup, buildInfo) {
96
+ return {
97
+ name: '@hagicode/hagilight-starlight:metadata',
98
+ hooks: {
99
+ 'astro:build:done'({ dir, logger }) {
100
+ completeSiteMetadataOwnerBuild(() => {
101
+ const build = buildInfo();
102
+ if (!build)
103
+ return;
104
+ writeStarlightSiteMetadata({ outDir: fileURLToPath(dir), setup, build, logger });
105
+ });
106
+ },
107
+ },
108
+ };
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagilight-starlight",
3
- "version": "0.11.1-dev.129.1.ea757b8",
3
+ "version": "0.11.1-dev.133.1.338317a",
4
4
  "type": "module",
5
5
  "description": "Starlight plugin and components for HagiCode documentation",
6
6
  "repository": {
@@ -53,7 +53,7 @@
53
53
  "assets/showcase/*"
54
54
  ],
55
55
  "dependencies": {
56
- "@hagicode/hagilight-core": "0.11.1-dev.129.1.ea757b8"
56
+ "@hagicode/hagilight-core": "0.11.1-dev.133.1.338317a"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@astrojs/starlight": "^0.42.4",