Reusable Astro components, integrations, a Starlight plugin, and a Docusaurus
plugin for HagiCode sites. This repository publishes four npm packages, all authored in
TypeScript and shipped as ESM with generated .d.ts declarations. Astro
components, Astro route entry points, styles, JSON, and images are shipped in
their native formats.
flowchart LR
A["@hagicode/hagilight"] --> C["@hagicode/hagilight-core"]
S["@hagicode/hagilight-starlight"] --> C
D["@hagicode/hagilight-docusaurus"] --> C
Every feature package depends on the matching version of the shared core and never on another feature package. Install core directly when your site imports a core entry point.
| Package | npm | Downloads |
|---|---|---|
@hagicode/hagilight-core |
||
@hagicode/hagilight |
||
@hagicode/hagilight-starlight |
||
@hagicode/hagilight-docusaurus |
Shared building blocks for plain Astro, Starlight, and Docusaurus sites. Its Astro components require astro
>=7; it has no Starlight dependency.
| Import | Provides |
|---|---|
@hagicode/hagilight-core/links |
resolveSiteLinks() and the localized header/footer link catalog types |
@hagicode/hagilight-core/favicon |
resolveFaviconHeadEntry() and getHagilightFaviconDataUri() |
@hagicode/hagilight-core/seo |
Metadata, canonical head composition, and JSON-LD builders |
@hagicode/hagilight-core/seo-schema |
seoSchema for optional seo frontmatter |
@hagicode/hagilight-core/rss |
generateRssFeed() and resolveRssLocales() |
@hagicode/hagilight-core/rss-ownership |
Coordination that keeps one integration as the RSS route owner |
@hagicode/hagilight-core/promotions |
Typed promotion campaign loader |
@hagicode/hagilight-core/promoto-banner |
definePromotoBannerElement(), the promotion banner custom element used by the Docusaurus package |
@hagicode/hagilight-core/analytics-events |
Google Analytics click-event tags (gaEventAttributes(), siteLinkGaAttributes()) and the tracked-link table |
@hagicode/hagilight-core/Footer |
Localized footer with generated RSS and sitemap links |
@hagicode/hagilight-core/Copyright, /PromotoBanner, /GoogleAnalytics, /Analytics51LA |
Astro components |
@hagicode/hagilight-core/logo.png, /favicon.ico |
Brand assets |
Plain Astro integrations and components (astro >=7):
| Import | Provides |
|---|---|
@hagicode/hagilight/integration |
hagilight() (sitemap, robots.txt, and RSS feeds by default), hagilightFavicon(), and their option types |
@hagicode/hagilight/SEOHead |
Canonical, Open Graph, Twitter, and JSON-LD head entries |
@hagicode/hagilight/Footer |
Shared footer with Powered By hagilight@<version> attribution |
The plain-Astro Footer reads the exact build-time version from its own
@hagicode/hagilight package manifest; it does not report an astro framework
version. Existing sites that import @hagicode/hagilight-core/Footer can keep
that core-only footer (with no feature-package attribution) or switch to
@hagicode/hagilight/Footer to display the Hagilight version.
A Starlight plugin (@astrojs/starlight ^0.42.4, astro ^7.3.5) that adds
the localized header, language chooser, and footer links; content-width toggle;
custom 404 page; end-of-article HagiCode introduction; floating promotion
banner; AI translation or authorship disclosures; SEO metadata and multilingual
page discovery; RSS feeds; Google Analytics and 51LA; and the shared favicon.
Its Footer appends Powered By hagilight-starlight@<version> using the exact
build-time version from the @hagicode/hagilight-starlight manifest, not the
Astro or Starlight dependency version.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
import hagilight from '@hagicode/hagilight-starlight';
import { locales } from '@hagicode/hagilight-starlight/locales';
export default defineConfig({
site: 'https://docs.example.test',
integrations: [starlight({ title: 'Docs', locales, plugins: [hagilight({ rss: { includeDocs: false } })] })],
});// src/content.config.ts
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
import { hagilightSchema } from '@hagicode/hagilight-starlight/schema';
export const collections = { docs: defineCollection({ loader: docsLoader(), schema: docsSchema({ extend: hagilightSchema }) }) };HagilightStarlightOptions types every plugin option, so misspelled or
mistyped options fail type-checking in astro.config.ts or JSDoc-checked
configs; the plugin still validates options at runtime. Components such as
@hagicode/hagilight-starlight/Header and /MarkdownContent remain importable
for sites that compose their own Starlight overrides.
A Docusaurus 3 plugin that adds the Hagilight footer, promotion banner, analytics, SEO metadata, and discovery files (sitemap.xml, robots.txt, rss.xml) to a Docusaurus site. It peers on @docusaurus/core and @docusaurus/theme-classic ^3.9.2, depends only on the shared core, and exports the locale catalog from @hagicode/hagilight-docusaurus/locales.
// docusaurus.config.mjs
import { hagilightDocusaurus } from '@hagicode/hagilight-docusaurus';
import { i18n } from '@hagicode/hagilight-docusaurus/locales';
export default {
title: 'My docs',
url: 'https://docs.example.com',
baseUrl: '/',
i18n,
plugins: [
hagilightDocusaurus({
analytics: {
googleAnalytics: { measurementId: 'G-XXXXXXXXXX' },
fiftyOneLa: { enabled: false },
},
}),
],
themeConfig: {
navbar: { items: [{ type: 'localeDropdown' }] },
},
};The built-in analytics IDs belong to HagiCode, so set your own IDs or disable each provider. Every option, its default, and the sitemap, robots.txt, and RSS rules are in the package README. The demo in examples/demo-docusaurus-web is published at https://docusaurus.hagicode.com/.
Shared links classify each destination as same-origin, first-party (hagicode.com and its subdomains, plus github.com/HagiCode-org), or third-party. First-party and same-origin new-tab links use rel="noopener"; third-party links keep rel="noopener noreferrer". Setting links.siteId adds utm_source=<siteId>&utm_medium=footer (or promoto for the banner) to first-party hagicode.com footer and banner destinations, after deduplication and never on GitHub, third-party, or same-origin links. The Google Analytics cross-domain linker is not configured, because every first-party host that runs the shared gtag shares the hagicode.com cookie domain. The full rules, options and the linker decision are in the core package README.
GoogleAnalytics (included by the Starlight plugin) reports clicks on key links and download buttons as GA4 events, with no configuration. Events are sent only on production pages where Google Analytics is enabled, never on the 404 page, and are skipped when gtag is missing.
- Event name (action):
download_clickfor thedownloadcategory,store_visitforstore, andlink_clickfornavigation,community, andpromotion. - Categories: use
downloadfor installers and download-hub entry points, andstorefor store listing pages (Microsoft Store, Steam). A single click reports under one category only. - Parameters:
event_category,event_label(the link's stable id, not its localized text),link_location(header,footer,article_promotion,promoto_banner, or your own value),link_url, andtransport_type: 'beacon'. No link text, form values, or visitor identifiers are sent. - Tracked links: header
home,blog,support; footerdownloadClient(download),microsoftStore(store),dockerCompose,productDocs,blogPosts(navigation),github,discord,issueFeedback(community); the HagiCode showcase calls to action, Microsoft Store badge (store, and its fallback link), and all-downloads link (download); and the floating promotion banner call to action, labelled with the promotion id.TRACKED_SITE_LINKSinpackages/core/src/analytics-events.tsis authoritative; renaming a link id changes its GA label. Related sites, RSS, sitemap, about, contact, filings, andextraLinksare not tracked. - Your own links: spread
gaEventAttributes({ category, label, location })from@hagicode/hagilight-core/analytics-eventsonto an anchor. Usecategory: 'store'for a store listing andcategory: 'download'for an installer. Unknown categories (including action names such asstore_visit) throw at build time and are ignored at click time. - Reports: register
event_category,event_label,link_location, andlink_urlas GA4 custom dimensions to see them in standard reports.
See the core package README for the full contract.
Every JavaScript entry point has a types export condition, so TypeScript
resolves declarations with moduleResolution NodeNext, Node16, or
Bundler without application-level module declarations. For example, type the
module passed to hagilight({ rss: { getFeed } }):
// src/rss-feed.ts
import type { RssFeedCallback } from '@hagicode/hagilight/integration';
const getFeed: RssFeedCallback = async ({ lang }) => ({
title: 'Updates',
description: `Updates in ${lang}`,
items: [{ title: 'Release notes', link: '/blog/release/', date: new Date() }],
});
export default getFeed;Run commands from the repository root:
npm install
npm run build # tsc -b: core, then the Docusaurus, plain Astro, and Starlight packages
npm run typecheck # build, check Astro route entry points, and check test/types fixtures
npm test # builds first, then runs node --test
npm run build:example # builds packages, the core-footer demo, and the Starlight demo variants, then verifies their output (the Docusaurus demo is built and checked by npm test)
npm run build:starlight-example # builds only the default Starlight demo, without the verification variants
npm run pack:check # verifies all four tarballs, exports, and version alignment
npm run integration:installed # installs tarballs into isolated core, Docusaurus, Astro, and Starlight consumersSources live in packages/*/src/*.ts and compile to the ignored
packages/*/dist/ directories; .astro components and route files import the
built dist/*.js modules, so build before running examples from a clean
checkout. The release workflow stamps one version into all four packages and the core
dependency of every feature package (scripts/release.mjs stamp), then
scripts/publish.mjs publishes core first, followed by the Docusaurus,
plain-Astro, and Starlight packages.
Register the plain-Astro integration in astro.config.mjs (importing a component alone
cannot register an Astro integration):
import { defineConfig } from 'astro/config';
import { hagilight } from '@hagicode/hagilight/integration';
export default defineConfig({
site: 'https://example.test',
integrations: [hagilight()],
});With an absolute HTTP(S) site, Astro's sitemap integration generates
sitemap-index.xml and Hagilight generates robots.txt with an absolute
Sitemap: URL to that index. For base: '/manual/', verify
https://example.test/manual/sitemap-index.xml and the sitemap entries under
https://example.test/manual/; the localized Footer sitemap link uses that
same base-aware path. Set hagilight({ enabled: false }) to disable both
generated outputs. An existing @astrojs/sitemap integration or Starlight owns
sitemap generation instead; a consumer-owned public/robots.txt or
src/pages/robots.txt.* remains authoritative.
If you need a site-specific crawl policy, own public/robots.txt and include
the absolute URL of the deployed sitemap index:
User-agent: *
Allow: /
Disallow: /private/
Sitemap: https://example.test/manual/sitemap-index.xml
Hagilight does not generate llms.txt. To publish AI discovery links, add your
own public/llms.txt with only the localized pages you want to expose:
# Example documentation
- [English](https://example.test/manual/)
- [简体中文](https://example.test/manual/zh-CN/)
If you do not want AI discovery, do not add an llms.txt file. Deployments
under a base path must still serve /robots.txt at the origin root, because
crawlers look for that exact URL.
hagilight() from @hagicode/hagilight/integration generates localized RSS feeds
by default, so integrating it is enough to publish /rss.xml and
/rss.<language>.xml. Sites can keep owning RSS routes and call
generateRssFeed from @hagicode/hagilight-core/rss, or customize the generated
feeds with the rss option:
import { defineConfig } from 'astro/config';
import { hagilight } from '@hagicode/hagilight/integration';
export default defineConfig({
site: 'https://example.test',
integrations: [
hagilight({
rss: { getFeed: './src/rss-feed.ts' },
}),
],
});When rss is omitted (the default) or rss: true, the integration derives the
feed locales from the Astro i18n config and serves a built-in empty feed.
Pass rss: false to disable RSS entirely. The getFeed path is resolved
relative to the Astro project root. Its module must default-export an
RssFeedCallback that receives { route, lang } for each configured locale and
returns { title, description, items }. Items use the same title, link,
optional description, and optional date/pubDate shape accepted by
generateRssFeed. The integration requires an absolute HTTP(S) site URL and a
valid callback module and result when getFeed is supplied.
The integration prerenders /rss.xml, /rss.en.xml, and
/rss.<language>.xml for each configured non-English language. /rss.xml and
/rss.en.xml use the configured English callback result. If no English locale
is configured, both are valid empty English feeds; Hagilight does not invoke a
different language's callback or borrow its content. A generated filename
conflicting with a page or public file fails the build rather than replacing
consumer-owned output. Setting rss: false leaves route and Footer behavior
unchanged.
Generated route and item URLs honor Astro's base, for example
base: '/manual/' generates /manual/rss.xml and resolves relative item links
under https://example.test/manual/. The core Footer gets the default feed
link and, on configured non-English pages, a current-language link from
request-local integration context. Explicit links.rssFeedUrl,
links.rssLocaleFeedUrl, link overrides, and existing removal options remain
authoritative.
When both Hagilight integrations are active, Starlight remains the sole RSS
owner if its RSS generation is enabled; the plain-Astro hagilight() then adds
neither routes nor Footer URLs. Both packages coordinate through
@hagicode/hagilight-core/rss-ownership. Having both packages installed does not enable
either integration. If the Starlight integration is present with RSS explicitly
disabled, the plain-Astro hagilight() owns the RSS routes instead.
Install the Chromium browser once with npx playwright install chromium, then run npm run test:viewport from the Hagilight repository root. The command builds and serves both example sites locally before checking these routes at 1536 x 864, 1920 x 1080, and 2560 x 1440 CSS viewport pixels:
| Example | Routes | Layout states |
|---|---|---|
| Core Astro | /, /zh-CN/ |
Header actions, bounded feature cards, internal code-block scrolling, and document overflow |
| Starlight | /zh-CN/, /zh-Hant/ |
Header, sidebar, populated page outline, narrow and wide reading widths, and the open language chooser |
The suite blocks external requests, disables optional analytics only for its test build, waits for fonts, and disables animation. On failure it reports the route, viewport, and affected region, then saves a screenshot; CI retains these under .ci-artifacts/viewport/.
These dimensions are Chromium CSS viewport pixels at device scale factor 1. They do not emulate macOS display scaling, physical device pixels, Safari, or other browser typography; a real-device/browser pass remains complementary.