@hagicode/hagilight-starlight 0.1.0-dev.148.1.e0c630e
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/AIDisclosureNotice.astro +12 -0
- package/ArticlePromotion.astro +630 -0
- package/ArticlePromotionPicker.astro +9 -0
- package/ContentLayoutToggle.astro +31 -0
- package/Footer.astro +201 -0
- package/Header.astro +131 -0
- package/LanguageChooser.astro +386 -0
- package/MarkdownContent.astro +45 -0
- package/NotFoundHero.astro +44 -0
- package/PageTitle.astro +7 -0
- package/PromotoFooter.astro +7 -0
- package/README.md +178 -0
- package/SEOHead.astro +67 -0
- package/ThemeSelect.astro +176 -0
- package/assets/showcase/hero-avatar.webp +0 -0
- package/assets/showcase/heroes.webp +0 -0
- package/assets/showcase/manifest.json +333 -0
- package/assets/showcase/pillar-efficient.svg +1 -0
- package/assets/showcase/pillar-fun.svg +1 -0
- package/assets/showcase/pillar-smart.svg +1 -0
- package/assets/showcase/proposal-workflow.webp +0 -0
- package/assets/showcase/workbench.webp +0 -0
- package/content-width.css +81 -0
- package/dist/ai-disclosures.d.ts +31 -0
- package/dist/ai-disclosures.js +105 -0
- package/dist/article-promotion-copy/de-DE.d.ts +34 -0
- package/dist/article-promotion-copy/de-DE.js +43 -0
- package/dist/article-promotion-copy/en-US.d.ts +34 -0
- package/dist/article-promotion-copy/en-US.js +43 -0
- package/dist/article-promotion-copy/es-ES.d.ts +34 -0
- package/dist/article-promotion-copy/es-ES.js +43 -0
- package/dist/article-promotion-copy/fr-FR.d.ts +34 -0
- package/dist/article-promotion-copy/fr-FR.js +43 -0
- package/dist/article-promotion-copy/index.d.ts +32 -0
- package/dist/article-promotion-copy/index.js +50 -0
- package/dist/article-promotion-copy/ja-JP.d.ts +34 -0
- package/dist/article-promotion-copy/ja-JP.js +43 -0
- package/dist/article-promotion-copy/ko-KR.d.ts +34 -0
- package/dist/article-promotion-copy/ko-KR.js +43 -0
- package/dist/article-promotion-copy/pt-BR.d.ts +34 -0
- package/dist/article-promotion-copy/pt-BR.js +43 -0
- package/dist/article-promotion-copy/ru-RU.d.ts +34 -0
- package/dist/article-promotion-copy/ru-RU.js +43 -0
- package/dist/article-promotion-copy/zh-CN.d.ts +34 -0
- package/dist/article-promotion-copy/zh-CN.js +43 -0
- package/dist/article-promotion-copy/zh-Hant.d.ts +34 -0
- package/dist/article-promotion-copy/zh-Hant.js +43 -0
- package/dist/article-promotion.d.ts +40 -0
- package/dist/article-promotion.js +14 -0
- package/dist/content-width-i18n.d.ts +6 -0
- package/dist/content-width-i18n.js +15 -0
- package/dist/content-width.d.ts +5 -0
- package/dist/content-width.js +42 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +583 -0
- package/dist/language-routing.d.ts +18 -0
- package/dist/language-routing.js +95 -0
- package/dist/locales.d.ts +48 -0
- package/dist/locales.js +13 -0
- package/dist/not-found.d.ts +9 -0
- package/dist/not-found.js +28 -0
- package/dist/rss-renderer.d.ts +14 -0
- package/dist/rss-renderer.js +30 -0
- package/dist/rss-utils.d.ts +27 -0
- package/dist/rss-utils.js +65 -0
- package/dist/schema.d.ts +32 -0
- package/dist/schema.js +25 -0
- package/dist/seo-utils.d.ts +74 -0
- package/dist/seo-utils.js +219 -0
- package/dist/showcase.d.ts +6 -0
- package/dist/showcase.js +5 -0
- package/dist/site-metadata.d.ts +38 -0
- package/dist/site-metadata.js +109 -0
- package/dist/themes.d.ts +43 -0
- package/dist/themes.js +156 -0
- package/dist/windows-download.d.ts +4 -0
- package/dist/windows-download.js +22 -0
- package/package.json +88 -0
- package/rss.[language].xml.ts +20 -0
- package/rss.xml.ts +15 -0
- package/themes.css +160 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import DefaultMarkdownContent from '@astrojs/starlight/components/MarkdownContent.astro';
|
|
4
|
+
import ArticlePromotion from './ArticlePromotion.astro';
|
|
5
|
+
import AIDisclosureNotice from './AIDisclosureNotice.astro';
|
|
6
|
+
import {
|
|
7
|
+
getSourcePath,
|
|
8
|
+
isTranslationLocale,
|
|
9
|
+
resolveAIDisclosureFlags,
|
|
10
|
+
} from './dist/ai-disclosures.js';
|
|
11
|
+
import { resolveArticlePromotion } from './dist/article-promotion.js';
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
aiDisclosures = { isAITranslation: false, isAIAuthor: false, sourceLocale: 'root' },
|
|
15
|
+
hagicodePromotionEnabled = true,
|
|
16
|
+
hagicodePromotionRandomAvatar = true,
|
|
17
|
+
} = Astro.props;
|
|
18
|
+
const route = Astro.locals.starlightRoute;
|
|
19
|
+
const isDocsEntry = route.entry.id !== '404' && !route.entry.id.endsWith('/404');
|
|
20
|
+
const flags = isDocsEntry
|
|
21
|
+
? resolveAIDisclosureFlags(route.entry.data, aiDisclosures)
|
|
22
|
+
: { isAITranslation: false, isAIAuthor: false };
|
|
23
|
+
const showHagicodePromotion = isDocsEntry
|
|
24
|
+
&& resolveArticlePromotion(route.entry.data.hagicodePromotion, hagicodePromotionEnabled);
|
|
25
|
+
const showTranslation = flags.isAITranslation
|
|
26
|
+
&& isTranslationLocale(route.entryMeta.locale, aiDisclosures.sourceLocale);
|
|
27
|
+
let sourceHref;
|
|
28
|
+
|
|
29
|
+
if (showTranslation) {
|
|
30
|
+
const docs = await getCollection('docs');
|
|
31
|
+
sourceHref = getSourcePath({
|
|
32
|
+
docs,
|
|
33
|
+
routeId: route.id,
|
|
34
|
+
currentLocale: route.locale,
|
|
35
|
+
sourceLocale: aiDisclosures.sourceLocale,
|
|
36
|
+
pathname: Astro.url.pathname,
|
|
37
|
+
baseUrl: import.meta.env.BASE_URL,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
{flags.isAIAuthor && <AIDisclosureNotice kind="author" lang={route.lang} />}
|
|
43
|
+
<DefaultMarkdownContent><slot /></DefaultMarkdownContent>
|
|
44
|
+
{showTranslation && <AIDisclosureNotice kind="translation" lang={route.lang} sourceHref={sourceHref} />}
|
|
45
|
+
{showHagicodePromotion && <ArticlePromotion lang={route.lang} randomAvatar={hagicodePromotionRandomAvatar} />}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DefaultHero from '@astrojs/starlight/components/Hero.astro';
|
|
3
|
+
import context from 'virtual:starlight/project-context';
|
|
4
|
+
import {
|
|
5
|
+
getNotFoundHomeHref,
|
|
6
|
+
getNotFoundHomeLabel,
|
|
7
|
+
isNotFoundEntry,
|
|
8
|
+
} from './dist/not-found.js';
|
|
9
|
+
|
|
10
|
+
const route = Astro.locals.starlightRoute;
|
|
11
|
+
const isNotFound = isNotFoundEntry(route.entry.id);
|
|
12
|
+
const homeHref = getNotFoundHomeHref({
|
|
13
|
+
basePath: import.meta.env.BASE_URL,
|
|
14
|
+
locale: route.locale,
|
|
15
|
+
trailingSlash: context.trailingSlash,
|
|
16
|
+
});
|
|
17
|
+
const homeLabel = getNotFoundHomeLabel(route.lang);
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
isNotFound ? (
|
|
22
|
+
<div class="not-found-hero">
|
|
23
|
+
<DefaultHero />
|
|
24
|
+
<div class="recovery-action">
|
|
25
|
+
<a class="sl-link-button primary" href={homeHref}>{homeLabel}</a>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
) : (
|
|
29
|
+
<DefaultHero />
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.not-found-hero {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
align-items: center;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.recovery-action {
|
|
41
|
+
margin-block: 0.5rem 2rem;
|
|
42
|
+
text-align: center;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
package/PageTitle.astro
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# @hagicode/hagilight-starlight
|
|
2
|
+
|
|
3
|
+
A Starlight plugin for HagiCode documentation sites. Adds a localized header and footer, language chooser, content-width toggle, theme picker, custom 404 page, SEO metadata, RSS feeds, analytics, and AI-content disclosures.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @hagicode/hagilight-starlight @astrojs/starlight astro
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Use Starlight `^0.42.4` and Astro `^7.3.5`. The matching `@hagicode/hagilight-core` version is installed as a dependency.
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// astro.config.mjs
|
|
17
|
+
import { defineConfig } from 'astro/config';
|
|
18
|
+
import starlight from '@astrojs/starlight';
|
|
19
|
+
import hagilight from '@hagicode/hagilight-starlight';
|
|
20
|
+
import { locales } from '@hagicode/hagilight-starlight/locales';
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
site: 'https://docs.example.com',
|
|
24
|
+
integrations: [
|
|
25
|
+
starlight({ title: 'Docs', locales, plugins: [hagilight()] }),
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The plugin also exposes `@hagicode/hagilight-starlight/schema` for optional SEO frontmatter and individual Astro components such as `/Header` and `/MarkdownContent` for custom Starlight overrides. Configure features through the typed `HagilightStarlightOptions` passed to `hagilight()`.
|
|
31
|
+
|
|
32
|
+
### Analytics
|
|
33
|
+
|
|
34
|
+
`hagilight()` enables PostHog without analytics configuration, an enabled flag, or a consumer project key. It uses `phc_qiVVihsL925ovhPYNdLJWz4k7aoKVMwYCevgZPE6PER5`, API host `https://us.i.posthog.com`, and SDK defaults `2026-05-30`. Google Analytics and PostHog render together in the generated global Head; 51LA stays in the Footer. PostHog covers all production pages, including localized and 404 pages, independently of Google's existing 404 exclusion. Development pages emit no PostHog loader.
|
|
35
|
+
|
|
36
|
+
PostHog options follow the Google Analytics provider convention:
|
|
37
|
+
|
|
38
|
+
| PostHog input | Behavior |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| Plugin options omitted, `analytics: {}`, or `posthog` omitted | Enabled with the supplied default key |
|
|
41
|
+
| `posthog: {}` | Disabled, like an empty Google Analytics provider |
|
|
42
|
+
| `posthog: { apiKey: 'phc_Custom123' }` | Enabled with the custom key |
|
|
43
|
+
| `posthog: { enabled: true, apiKey: 'phc_Custom123' }` | Enabled with the custom key |
|
|
44
|
+
| `posthog: { enabled: true }` | Configuration error: `apiKey` required |
|
|
45
|
+
| `posthog: { enabled: false }` | Disabled independently of Google Analytics and 51LA |
|
|
46
|
+
|
|
47
|
+
Keys must match `^phc_[A-Za-z0-9]+$`; enabled providers reject missing or malformed keys. Provider options must be objects and `enabled` must be boolean.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
hagilight(); // shared analytics defaults
|
|
51
|
+
hagilight({ analytics: { posthog: { apiKey: 'phc_Custom123' } } });
|
|
52
|
+
hagilight({ analytics: { posthog: { enabled: false } } });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A site-owned `components.Head` conflicts with enabled SEO, Google Analytics, or PostHog. To retain it, disable all three head features and explicitly compose the desired metadata and `@hagicode/hagilight-core/GoogleAnalytics` / `@hagicode/hagilight-core/PostHog` components in that Head:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
hagilight({
|
|
59
|
+
seo: { enabled: false },
|
|
60
|
+
analytics: {
|
|
61
|
+
googleAnalytics: { enabled: false },
|
|
62
|
+
posthog: { enabled: false },
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The shared component initializes once per document; SDK defaults own PostHog navigation behavior. Choose one initialization owner and disable this provider when the site already initializes PostHog. Google Analytics and 51LA keep their configured behavior when PostHog is disabled.
|
|
68
|
+
|
|
69
|
+
### Theme picker
|
|
70
|
+
|
|
71
|
+
By default the plugin replaces Starlight's light/dark toggle with a theme picker offering nine choices: the default theme plus the Ocean, Sakura, and Forest themes in light and dark, and a "Default" choice that follows the visitor's system color scheme and renders the Forest theme. First-time visitors, and visitors with an invalid saved choice, therefore see Forest; the other themes appear only after an explicit selection, which is remembered in `localStorage`.
|
|
72
|
+
|
|
73
|
+
Earlier versions assigned each first-time visitor a random theme and stored it under `hagilight-theme-random`. That value is now ignored: returning visitors who never picked a theme explicitly see Forest after upgrading, while visitors with an explicit choice keep it.
|
|
74
|
+
|
|
75
|
+
Disable the picker to keep Starlight's built-in toggle or compose `@hagicode/hagilight-starlight/ThemeSelect` yourself:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
hagilight({ themes: { enabled: false } })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Build line
|
|
82
|
+
|
|
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
|
+
|
|
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
|
+
|
|
94
|
+
### Body images
|
|
95
|
+
|
|
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.
|
|
97
|
+
|
|
98
|
+
Local body images in `.md` and `.mdx` files are also wrapped in a `<picture>` element with AVIF and WebP `<source>` elements, produced by Astro's image service. The rule is the same for both file types: a relative local image that is not `.svg` or `.gif` and does not resolve into `public/` is wrapped. A Markdown image in an `.mdx` page therefore renders as `picture` with an inner `<img>`, the same as in `.md`. The default sharp service encodes AVIF. With the passthrough no-op service, the wrapper is skipped and one warning is logged. Site-root and remote images, and raw HTML `<img>` tags, are not wrapped. Encoding adds build time, which Astro caches. In `.md`, each `<source>` also carries `src`, `loading`, `decoding`, `width`, and `height` from Astro's output; in `.mdx`, `<source>` carries only `srcset` and `type`. Browsers ignore the extra attributes on `source`, but they are not valid HTML there. The opt-out below turns this step off too. The step adds no option of its own.
|
|
99
|
+
|
|
100
|
+
Turn it off for the site:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
hagilight({ contentImages: { enabled: false } })
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### HagiCode showcase
|
|
107
|
+
|
|
108
|
+
After the body (and any translation notice) of every documentation article, the plugin renders a static HagiCode showcase: a headline, lead, and tagline with calls to action to the website and the documentation, three pillars (Smart, Efficient, Fun), a gallery with captions (three audited screenshots and one hero-avatar illustration), and a closing row with the Windows download button beside a selectable, copy-ready share sentence. The layout follows the showcase's own width: one column when narrow, a three-up pillar row, a gallery mosaic, and a two-column closing row from 36rem, and the pillars beside the pitch from 56rem (for example in the wide content mode). All text is localized for the ten published locales (`en-US`, `zh-CN`, `zh-Hant`, `ja-JP`, `ko-KR`, `de-DE`, `fr-FR`, `es-ES`, `pt-BR`, `ru-RU`) and falls back to English for any other language. Links come from the same locale-aware catalog as the footer.
|
|
109
|
+
|
|
110
|
+
Turn it off for the whole site, or override it per article:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
hagilight({ hagicodePromotion: { enabled: false } })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```md
|
|
117
|
+
---
|
|
118
|
+
title: An article without the showcase
|
|
119
|
+
hagicodePromotion: false
|
|
120
|
+
---
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The floating banner is a separate option (`promoto`).
|
|
124
|
+
|
|
125
|
+
#### Reuse the copy
|
|
126
|
+
|
|
127
|
+
`@hagicode/hagilight-starlight/showcase` returns the same read-only copy, including the share sentence, so sibling sites can show identical wording:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import { getShowcaseCopy, showcaseLocales } from '@hagicode/hagilight-starlight/showcase';
|
|
131
|
+
|
|
132
|
+
const copy = getShowcaseCopy('ja-JP'); // English copy for any unsupported locale
|
|
133
|
+
copy.shareText; // one sentence that ends with https://www.hagicode.com/
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The catalog is frozen; changing a returned object throws.
|
|
137
|
+
|
|
138
|
+
#### Windows download button and the Microsoft badge script
|
|
139
|
+
|
|
140
|
+
The Windows button follows the official HagiCode website and documentation: the `<ms-store-badge>` element for product `9N3PM0N3SVDW` (`window-mode="direct"`, `theme="auto"`, `size="large"`, badge language matching the page locale). Inside it sits a plain "Get it from Microsoft Store" link to the Store listing, and next to it an "All downloads" link to the localized downloads page for other platforms. The store URL and the downloads URL come from `@hagicode/hagilight-core` (`microsoftStore`, `downloadClient`).
|
|
141
|
+
|
|
142
|
+
This is the only script the showcase loads: `https://get.microsoft.com/badge/ms-store-badge.bundled.js`, as a module script. It only draws the badge. If your content security policy blocks it, or the reader is offline or blocks third-party scripts, the plain Store link stays visible and works, and nothing else changes. To use the badge, allow `get.microsoft.com` in `script-src`. The loader is emitted once per page, and only when the showcase is enabled. There is no first-party script, no operating-system detection, and no Steam link (the Steam app is retired).
|
|
143
|
+
|
|
144
|
+
#### Assets and image traffic
|
|
145
|
+
|
|
146
|
+
The gallery images and pillar icons live in `assets/showcase/`. The illustration `hero-avatar.webp` is the R2 object `hero-avatars/ember-01.avif` (1024 px) upscaled to the 1200 px minimum; its manifest entry records the source and this transform, with a provenance record in `assets/showcase/manifest.json`. Each entry states its kind (`sourced` from the HagiCode web app or documentation, a hand-authored `svg`, or a `generated` illustration), its source path or generation record (tool, prompt, parameters, date), the crop and conversion applied, its size, and the audit verdict. Nothing is fetched from the product sites at build or read time.
|
|
147
|
+
|
|
148
|
+
Every asset passes the same audit before it is committed: the current `HagiCode` brand, no Steam or other retired-channel content, no third-party promotion, no credentials, private repository names, or personal data, and rasters at least 1200 px wide. A removable offending region may be cropped out, and the crop is recorded. Pick images in this order to keep traffic low: an audited existing image converted to WebP, then a hand-authored SVG, then a newly generated raster only when neither fits. Generated art is an illustration, never a replica of the product UI, contains no text or logos, is captioned as an illustration, is reviewed by a person, and records its prompt and parameters (never credentials). Generation is an authoring step; the package never calls a generator.
|
|
149
|
+
|
|
150
|
+
Budgets for the shipped sources, enforced by `npm test`: 768 KiB for all showcase sources together, 200 KiB per raster source (WebP only), and 8 KiB per SVG. `npm run build:example` also checks the built output: each delivered raster variant at most 200 KiB, the variants of one format at most 768 KiB together, and each AVIF variant smaller than the WebP variant of the same width.
|
|
151
|
+
|
|
152
|
+
Each gallery image renders as a `<picture>` with an AVIF source, a WebP source, and a JPEG `<img>` fallback, at widths 480, 800, and the native width. The tiers are derived at the consumer's build from the shipped WebP, with the `sharp` service that Astro already bundles, so no conversion runs outside the site's own build and no external service is involved. The chain needs a transforming image service: with the passthrough `astro/assets/services/noop` service the tiers serve the original bytes, and the plugin logs one warning. Opaque images use the JPEG tier. The JPEG tier cannot carry transparency, so an image that needs alpha needs a PNG tier, which the component does not offer yet, and a budget review before it is added.
|
|
153
|
+
|
|
154
|
+
Images are served through Astro's image pipeline as responsive, content-hashed files, load lazily with low priority, and sit after the article body, so a reader who does not reach the end downloads none of them.
|
|
155
|
+
|
|
156
|
+
Measured on `examples/demo-starlight-web` with one warm build each: the nine AVIF variants total 139.0 KiB (largest 41.4 KiB) and the nine JPEG variants total 387.4 KiB (largest 118.2 KiB). The shipped WebP sources are unchanged. The build took 21.6 s without the tiers and 20.4 s with them, a difference within run-to-run noise. A browser that supports AVIF downloads one variant per image, the AVIF one.
|
|
157
|
+
|
|
158
|
+
#### Adopting and verifying
|
|
159
|
+
|
|
160
|
+
Upgrading `@hagicode/hagilight`, `@hagicode/hagilight-starlight`, and `@hagicode/hagilight-core` together is the whole change. It needs no configuration, dependency, script, or CI step: the consumer's existing build derives the tiers. If the build logs the passthrough image service warning, remove the `image` service override or use the default `sharp` service.
|
|
161
|
+
|
|
162
|
+
To check a deployed site, run `curl -sI https://<site>/_astro/<file>.avif` with a real file name from the built `dist/_astro`; it should report `Content-Type: image/avif`. `docs` also previews on Azure Static Web Apps, so check that preview as well. Any host header fix belongs to the owning repository, not to this package.
|
|
163
|
+
|
|
164
|
+
#### Keep it in sync
|
|
165
|
+
|
|
166
|
+
The copy and assets mirror `repos/web` and `repos/docs` in the HagiCode monorepo, which this package never edits.
|
|
167
|
+
|
|
168
|
+
- **Copy:** edit `src/article-promotion-copy/<locale>.ts` (every locale must keep the same fields, feature ids, and gallery ids). Terminology is pinned in `GLOSSARY` in `src/article-promotion-copy/index.ts`, seeded from the product locale files in `repos/web/src/locales/<locale>/`. `HagiCode`, `OpenSpec`, `Hero Scenario`, `Microsoft Store`, and `Windows` are never translated.
|
|
169
|
+
- **Images:** audit the candidate, crop and convert it to WebP (the AVIF and JPEG tiers are derived at build time, so an opaque image needs no extra file), add or update its `manifest.json` entry, register it in the `galleryImages` map in `ArticlePromotion.astro`, and add a caption and text alternative in every locale. The gallery mosaic places the first, second, and third catalog entries as the left top, left bottom, and right tall slot, so keep the three screenshots first. The illustration is the fourth entry: from 36rem it takes a full-width row with a 12 rem square and its caption, and it is the only gallery image that may change. To add a screenshot, revisit the placement in the component's `@container (min-width: 36rem)` block. `npm test` checks that the manifest, the files, the component registry, and the copy agree.
|
|
170
|
+
- **Random illustration (R2 pool):** the `hero-avatar` entry in `assets/showcase/manifest.json` carries `source` (`repo: img-gen`, `album`, `objectKey`), its audit, and `pool` (`album`, `maxBytes` 200 KiB, `entries` with `key` and `bytes`). `ArticlePromotion.astro` builds the pool attribute at build time and renders `ArticlePromotionPicker.astro` only when the option is on. Refresh: take keys from the `hero-avatars` album `index.json`, keep 12 to 200 unique keys each within `maxBytes`, audit them text-free and current, update `pool.entries`, and keep the footer pool in `repos/hagilight/packages/core/assets/footer/manifest.json` in step. To rebuild the WebP, resize the source to 1200 px (lanczos3) and encode at quality 88. `npm test` (`test/showcase-assets.test.mjs`) checks the entry, the pool, the byte caps and the budgets.
|
|
171
|
+
- **Options:** `hagicodePromotion: { randomAvatar: false }` keeps the default illustration and loads no picker; `footer: { avatar: false }` removes the footer avatar, and `footer: { randomAvatar: false }` keeps its default. A site whose `img-src` policy blocks `https://img-gen.hagicode.com` keeps the defaults.
|
|
172
|
+
- **Badge:** the product id is parsed from the Store URL and the badge language map lives in `src/windows-download.ts`. Re-check both against `repos/docs/src/components/MicrosoftStoreBadge.tsx` and the official site when the Store listing or the supported languages change.
|
|
173
|
+
|
|
174
|
+
The plugin footer and direct `/Footer` and `/PromotoFooter` components append
|
|
175
|
+
the literal `Powered By hagilight-starlight@<version>`. The version comes from
|
|
176
|
+
this package's own manifest at build time, not from Astro or Starlight.
|
|
177
|
+
|
|
178
|
+
See the [repository documentation](https://github.com/HagiCode-org/hagilight#readme) for schema setup and feature options.
|
package/SEOHead.astro
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import { composeSeoHead, resolveSeoMetadata, serializeJsonLd } from '@hagicode/hagilight-core/seo';
|
|
4
|
+
import {
|
|
5
|
+
buildStructuredData,
|
|
6
|
+
createPublishedDocsLookup,
|
|
7
|
+
filterPublishedLocaleAlternates,
|
|
8
|
+
resolveSeoPage,
|
|
9
|
+
} from './dist/seo-utils.js';
|
|
10
|
+
|
|
11
|
+
const { options } = Astro.props;
|
|
12
|
+
const route = Astro.locals.starlightRoute;
|
|
13
|
+
const routeId = route?.id ?? route?.entry?.id ?? '';
|
|
14
|
+
const isNotFound = /(?:^|\/)404(?:\.html)?\/?$/u.test(Astro.url.pathname)
|
|
15
|
+
|| routeId === '404'
|
|
16
|
+
|| routeId.endsWith('/404');
|
|
17
|
+
let head = route?.head ?? [];
|
|
18
|
+
let structuredData = [];
|
|
19
|
+
|
|
20
|
+
if (route?.entry?.collection === 'docs' && !isNotFound && route.entry.data?.draft !== true) {
|
|
21
|
+
const entries = await getCollection('docs', ({ data }) => data.draft !== true);
|
|
22
|
+
const lookup = createPublishedDocsLookup(entries, options.seoLocales);
|
|
23
|
+
const page = resolveSeoPage(routeId, options.seoLocales);
|
|
24
|
+
const sourceEntry = lookup.get(page.route, page.slug);
|
|
25
|
+
const explicitHead = [options.consumerHead, route.entry.data.head];
|
|
26
|
+
head = filterPublishedLocaleAlternates(head, {
|
|
27
|
+
entryId: routeId,
|
|
28
|
+
locales: options.seoLocales,
|
|
29
|
+
lookup,
|
|
30
|
+
basePath: options.seoBasePath,
|
|
31
|
+
explicitHead,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const seoMetadata = resolveSeoMetadata({
|
|
35
|
+
entry: route.entry,
|
|
36
|
+
head,
|
|
37
|
+
pageSeo: route.entry.data.seo,
|
|
38
|
+
siteSeo: options.seo,
|
|
39
|
+
site: options.seoSite,
|
|
40
|
+
basePath: options.seoBasePath,
|
|
41
|
+
explicitHead,
|
|
42
|
+
body: sourceEntry?.body,
|
|
43
|
+
});
|
|
44
|
+
head = composeSeoHead(head, seoMetadata, explicitHead);
|
|
45
|
+
const canonicalUrl = head.find(({ tag, attrs }) => tag === 'link' && attrs?.rel === 'canonical')?.attrs.href;
|
|
46
|
+
structuredData = buildStructuredData({
|
|
47
|
+
entry: route.entry,
|
|
48
|
+
entryId: routeId,
|
|
49
|
+
canonicalUrl,
|
|
50
|
+
site: options.seoSite,
|
|
51
|
+
siteTitle: route.siteTitle,
|
|
52
|
+
pageSeo: { ...route.entry.data.seo, description: seoMetadata.description },
|
|
53
|
+
organization: options.seo.organization,
|
|
54
|
+
locales: options.seoLocales,
|
|
55
|
+
defaultLocale: options.seoDefaultLocale,
|
|
56
|
+
lookup,
|
|
57
|
+
basePath: options.seoBasePath,
|
|
58
|
+
format: options.seoFormat,
|
|
59
|
+
trailingSlash: options.seoTrailingSlash,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
{head.map(({ tag: Tag, attrs, content }) => <Tag {...attrs} set:html={content} />)}
|
|
65
|
+
{structuredData.map((value) => (
|
|
66
|
+
<script type="application/ld+json" set:html={serializeJsonLd(value)} />
|
|
67
|
+
))}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
applyThemeChoice,
|
|
4
|
+
getThemePickerLabels,
|
|
5
|
+
parseThemeChoice,
|
|
6
|
+
readStoredThemeChoice,
|
|
7
|
+
type ThemeChoice,
|
|
8
|
+
} from './dist/themes.js';
|
|
9
|
+
|
|
10
|
+
const { lang } = Astro.locals.starlightRoute;
|
|
11
|
+
const labels = getThemePickerLabels(lang);
|
|
12
|
+
const accessibleLabel = Astro.locals.t('themeSelect.accessibleLabel');
|
|
13
|
+
const lightLabel = Astro.locals.t('themeSelect.light');
|
|
14
|
+
const darkLabel = Astro.locals.t('themeSelect.dark');
|
|
15
|
+
|
|
16
|
+
/* Inlined copies of Starlight's laptop, sun, moon, and down-caret icons; the Starlight
|
|
17
|
+
* Icon component is not part of the package's public exports. */
|
|
18
|
+
const icons = {
|
|
19
|
+
laptop: `<path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/>`,
|
|
20
|
+
sun: `<path d="M5 12a1 1 0 0 0-1-1H3a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Zm.64 5-.71.71a1 1 0 0 0 0 1.41 1 1 0 0 0 1.41 0l.71-.71A1 1 0 0 0 5.64 17ZM12 5a1 1 0 0 0 1-1V3a1 1 0 0 0-2 0v1a1 1 0 0 0 1 1Zm5.66 2.34a1 1 0 0 0 .7-.29l.71-.71a1 1 0 1 0-1.41-1.41l-.66.71a1 1 0 0 0 0 1.41 1 1 0 0 0 .66.29Zm-12-.29a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.71-.71a1.004 1.004 0 1 0-1.43 1.41l.73.71ZM21 11h-1a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2Zm-2.64 6A1 1 0 0 0 17 18.36l.71.71a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.76-.66ZM12 6.5a5.5 5.5 0 1 0 5.5 5.5A5.51 5.51 0 0 0 12 6.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0 3.5a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1a1 1 0 0 0-1-1Z"/>`,
|
|
21
|
+
moon: `<path d="M21.64 13a1 1 0 0 0-1.05-.14 8.049 8.049 0 0 1-3.37.73 8.15 8.15 0 0 1-8.14-8.1 8.59 8.59 0 0 1 .25-2A1 1 0 0 0 8 2.36a10.14 10.14 0 1 0 14 11.69 1 1 0 0 0-.36-1.05Zm-9.5 6.69A8.14 8.14 0 0 1 7.08 5.22v.27a10.15 10.15 0 0 0 10.14 10.14 9.784 9.784 0 0 0 2.1-.22 8.11 8.11 0 0 1-7.18 4.32v-.04Z"/>`,
|
|
22
|
+
caret: `<path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/>`,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const groups = [
|
|
26
|
+
{
|
|
27
|
+
label: labels.defaultThemeGroup,
|
|
28
|
+
prefix: labels.defaultOption,
|
|
29
|
+
choices: ['default-light', 'default-dark'],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: labels.themes.ocean,
|
|
33
|
+
prefix: labels.themes.ocean,
|
|
34
|
+
choices: ['ocean-light', 'ocean-dark'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: labels.themes.sakura,
|
|
38
|
+
prefix: labels.themes.sakura,
|
|
39
|
+
choices: ['sakura-light', 'sakura-dark'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: labels.themes.forest,
|
|
43
|
+
prefix: labels.themes.forest,
|
|
44
|
+
choices: ['forest-light', 'forest-dark'],
|
|
45
|
+
},
|
|
46
|
+
] as const satisfies ReadonlyArray<{
|
|
47
|
+
label: string;
|
|
48
|
+
prefix: string;
|
|
49
|
+
choices: readonly ThemeChoice[];
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
const groupOptionLabel = (prefix: string, choice: ThemeChoice): string =>
|
|
53
|
+
`${prefix} · ${choice.endsWith('-light') ? lightLabel : darkLabel}`;
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<hagilight-theme-select class="theme-picker">
|
|
57
|
+
<label style="--sl-select-width: 12.5em">
|
|
58
|
+
<span class="sr-only">{accessibleLabel}</span>
|
|
59
|
+
<svg class="icon label-icon" aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" set:html={icons.laptop} />
|
|
60
|
+
<select autocomplete="off">
|
|
61
|
+
<option value="default" selected>{labels.defaultOption}</option>
|
|
62
|
+
{groups.map((group) => (
|
|
63
|
+
<optgroup label={group.label}>
|
|
64
|
+
{group.choices.map((choice) => (
|
|
65
|
+
<option value={choice}>{groupOptionLabel(group.prefix, choice)}</option>
|
|
66
|
+
))}
|
|
67
|
+
</optgroup>
|
|
68
|
+
))}
|
|
69
|
+
</select>
|
|
70
|
+
<svg class="icon caret" aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" set:html={icons.caret} />
|
|
71
|
+
</label>
|
|
72
|
+
<template class="mode-icons">
|
|
73
|
+
<svg class="mode-auto" aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" set:html={icons.laptop} />
|
|
74
|
+
<svg class="mode-light" aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" set:html={icons.sun} />
|
|
75
|
+
<svg class="mode-dark" aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" set:html={icons.moon} />
|
|
76
|
+
</template>
|
|
77
|
+
</hagilight-theme-select>
|
|
78
|
+
|
|
79
|
+
<script>
|
|
80
|
+
import { applyThemeChoice, parseThemeChoice, readStoredThemeChoice, type ThemeChoice } from './dist/themes.js';
|
|
81
|
+
|
|
82
|
+
const modeClass = (choice: ThemeChoice): string =>
|
|
83
|
+
choice.endsWith('-light') ? 'mode-light' : choice.endsWith('-dark') ? 'mode-dark' : 'mode-auto';
|
|
84
|
+
|
|
85
|
+
class HagilightThemeSelect extends HTMLElement {
|
|
86
|
+
connectedCallback() {
|
|
87
|
+
if (this.dataset.initialized) return;
|
|
88
|
+
this.dataset.initialized = 'true';
|
|
89
|
+
|
|
90
|
+
const select = this.querySelector('select');
|
|
91
|
+
if (!(select instanceof HTMLSelectElement)) return;
|
|
92
|
+
|
|
93
|
+
const sync = (choice: ThemeChoice) => {
|
|
94
|
+
select.value = choice;
|
|
95
|
+
const icon = this.querySelector('svg.label-icon');
|
|
96
|
+
const next = this.querySelector('template.mode-icons')?.content.querySelector(`.${modeClass(choice)}`);
|
|
97
|
+
if (icon && next) icon.replaceChildren(...next.cloneNode(true).childNodes);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
sync(readStoredThemeChoice());
|
|
101
|
+
|
|
102
|
+
select.addEventListener('change', () => {
|
|
103
|
+
const choice = parseThemeChoice(select.value);
|
|
104
|
+
applyThemeChoice(choice);
|
|
105
|
+
sync(choice);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
matchMedia('(prefers-color-scheme: light)').addEventListener('change', (event) => {
|
|
109
|
+
if (readStoredThemeChoice() !== 'default') return;
|
|
110
|
+
document.documentElement.dataset.theme = event.matches ? 'light' : 'dark';
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!customElements.get('hagilight-theme-select')) {
|
|
116
|
+
customElements.define('hagilight-theme-select', HagilightThemeSelect);
|
|
117
|
+
}
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<style>
|
|
121
|
+
.theme-picker {
|
|
122
|
+
position: relative;
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 0.25rem;
|
|
126
|
+
color: var(--sl-color-gray-2);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.theme-picker:hover {
|
|
130
|
+
color: var(--sl-color-white);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.theme-picker .icon {
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 50%;
|
|
136
|
+
transform: translateY(-50%);
|
|
137
|
+
width: 1em;
|
|
138
|
+
height: 1em;
|
|
139
|
+
pointer-events: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.theme-picker select {
|
|
143
|
+
width: calc(var(--sl-select-width) + 1rem);
|
|
144
|
+
border: 0;
|
|
145
|
+
margin-inline: -0.5rem;
|
|
146
|
+
padding-block: 0.625rem;
|
|
147
|
+
padding-inline: calc(0.875rem + 0.75rem) calc(1.25rem + 0.75rem);
|
|
148
|
+
background-color: transparent;
|
|
149
|
+
color: inherit;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
text-overflow: ellipsis;
|
|
152
|
+
appearance: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.theme-picker option,
|
|
156
|
+
.theme-picker optgroup {
|
|
157
|
+
background-color: var(--sl-color-bg-nav);
|
|
158
|
+
color: var(--sl-color-gray-1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.theme-picker .label-icon {
|
|
162
|
+
inset-inline-start: 0;
|
|
163
|
+
font-size: 0.875rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.theme-picker .caret {
|
|
167
|
+
inset-inline-end: 0;
|
|
168
|
+
font-size: 1.25rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@media (min-width: 50rem) {
|
|
172
|
+
.theme-picker select {
|
|
173
|
+
font-size: var(--sl-text-sm);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
Binary file
|
|
Binary file
|