@hagicode/hagilight-starlight 0.1.2-dev.5.1.c41051d → 0.1.2-dev.6.1.f3f7b3d

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.
Files changed (3) hide show
  1. package/Footer.astro +70 -0
  2. package/index.mjs +123 -3
  3. package/package.json +2 -2
package/Footer.astro CHANGED
@@ -1,12 +1,82 @@
1
1
  ---
2
2
  import Copyright from '@hagicode/hagilight/Copyright';
3
3
  import DefaultFooter from '@astrojs/starlight/components/Footer.astro';
4
+ import { resolveSiteLinks, type SiteLinksOptions } from '@hagicode/hagilight/site-links';
5
+
6
+ interface Props {
7
+ locale?: string;
8
+ links?: SiteLinksOptions;
9
+ }
10
+
11
+ const { locale = Astro.locals?.starlightRoute?.locale, links } = Astro.props;
12
+ const resolvedLinks = resolveSiteLinks(locale, links);
4
13
  ---
5
14
 
6
15
  <DefaultFooter />
16
+ <nav class="hagilight-site-links" aria-label={resolvedLinks.labels.quickLinks}>
17
+ {resolvedLinks.relatedSites.length > 0 && (
18
+ <section>
19
+ <h2>{resolvedLinks.labels.relatedSites}</h2>
20
+ <ul>
21
+ {resolvedLinks.relatedSites.map((site) => (
22
+ <li><a href={site.href}>{site.name}</a></li>
23
+ ))}
24
+ </ul>
25
+ </section>
26
+ )}
27
+ <section>
28
+ <h2>{resolvedLinks.labels.quickLinks}</h2>
29
+ <ul>
30
+ {resolvedLinks.quick.map((link) => (
31
+ <li><a href={link.href} target={link.target} rel={link.rel}>{link.label}</a></li>
32
+ ))}
33
+ </ul>
34
+ </section>
35
+ <section>
36
+ <h2>{resolvedLinks.labels.community}</h2>
37
+ <ul>
38
+ {resolvedLinks.community.map((link) => (
39
+ <li><a href={link.href} target={link.target} rel={link.rel}>{link.label}</a></li>
40
+ ))}
41
+ </ul>
42
+ </section>
43
+ </nav>
7
44
  <p class="hagilight-copyright"><Copyright name="HagiCode" /></p>
8
45
 
9
46
  <style>
47
+ .hagilight-site-links {
48
+ display: grid;
49
+ grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
50
+ gap: 1.5rem;
51
+ margin: 2rem auto 1rem;
52
+ max-width: 72rem;
53
+ padding-inline: 1rem;
54
+ }
55
+
56
+ .hagilight-site-links h2 {
57
+ margin: 0 0 0.75rem;
58
+ color: var(--sl-color-white);
59
+ font-size: var(--sl-text-sm);
60
+ }
61
+
62
+ .hagilight-site-links ul {
63
+ display: grid;
64
+ gap: 0.5rem;
65
+ margin: 0;
66
+ padding: 0;
67
+ list-style: none;
68
+ }
69
+
70
+ .hagilight-site-links a {
71
+ color: var(--sl-color-gray-3);
72
+ font-size: var(--sl-text-sm);
73
+ text-decoration: none;
74
+ }
75
+
76
+ .hagilight-site-links a:hover {
77
+ color: var(--sl-color-white);
78
+ }
79
+
10
80
  .hagilight-copyright {
11
81
  margin-block: 1rem;
12
82
  color: var(--sl-color-gray-3);
package/index.mjs CHANGED
@@ -1,19 +1,139 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { fileURLToPath } from 'node:url';
2
3
 
4
+ const GOOGLE_ID_PATTERN = /^G-[A-Z0-9]+$/u;
5
+ const FIFTY_ONE_LA_ID_PATTERN = /^[A-Za-z0-9_-]+$/u;
6
+
7
+ function resolveProvider(config, { name, idKey, pattern }) {
8
+ if (config === undefined) return undefined;
9
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
10
+ throw new TypeError(`Hagilight ${name} analytics options must be an object.`);
11
+ }
12
+
13
+ const enabled = config.enabled ?? Boolean(config[idKey]);
14
+ if (!enabled) return undefined;
15
+
16
+ const id = config[idKey];
17
+ if (typeof id !== 'string' || !id.trim()) {
18
+ throw new Error(`Hagilight ${name} analytics is enabled but its ${idKey} is missing.`);
19
+ }
20
+ if (!pattern.test(id)) {
21
+ throw new Error(`Hagilight ${name} analytics ${idKey} has an invalid format.`);
22
+ }
23
+ return id;
24
+ }
25
+
26
+ function createConfiguredIntegration(instanceId, serializedOptions, componentIds) {
27
+ const optionsId = `virtual:hagilight-starlight/${instanceId}/options`;
28
+ const footerPath = fileURLToPath(new URL('./Footer.astro', import.meta.url));
29
+ const promotionImport = serializedOptions.promotoEnabled
30
+ ? "import PromotoBanner from '@hagicode/hagilight/PromotoBanner';"
31
+ : '';
32
+ const promotionRender = serializedOptions.promotoEnabled ? '<PromotoBanner />' : '';
33
+ const analyticsImport = serializedOptions.fiftyOneLaId
34
+ ? "import Analytics51LA from '@hagicode/hagilight/Analytics51LA';"
35
+ : '';
36
+ const analyticsRender = serializedOptions.fiftyOneLaId
37
+ ? '<Analytics51LA siteId={options.fiftyOneLaId} />'
38
+ : '';
39
+ const footerSource = `---
40
+ import Footer from ${JSON.stringify(footerPath)};
41
+ ${promotionImport}
42
+ ${analyticsImport}
43
+ import options from '${optionsId}';
44
+ ---
45
+
46
+ <Footer locale={Astro.locals?.starlightRoute?.locale} links={options.links} />
47
+ ${promotionRender}
48
+ ${analyticsRender}
49
+ `;
50
+ const headSource = `---
51
+ import DefaultHead from '@astrojs/starlight/components/Head.astro';
52
+ import GoogleAnalytics from '@hagicode/hagilight/GoogleAnalytics';
53
+ import options from '${optionsId}';
54
+ const isNotFound = /(?:^|\\/)404(?:\\.html)?\\/?$/u.test(Astro.url.pathname);
55
+ ---
56
+
57
+ <DefaultHead />
58
+ {!isNotFound && options.googleAnalyticsMeasurementId && (
59
+ <GoogleAnalytics measurementId={options.googleAnalyticsMeasurementId} />
60
+ )}
61
+ `;
62
+ const modules = new Map([
63
+ [componentIds.footer, footerSource],
64
+ ...(componentIds.head ? [[componentIds.head, headSource]] : []),
65
+ [optionsId, `export default ${JSON.stringify(serializedOptions)};`],
66
+ ]);
67
+ const vitePlugin = {
68
+ name: `@hagicode/hagilight-starlight:${instanceId}`,
69
+ resolveId(id) {
70
+ return modules.has(id) ? `\0${id}` : null;
71
+ },
72
+ load(id) {
73
+ return id.startsWith('\0') ? modules.get(id.slice(1)) ?? null : null;
74
+ },
75
+ };
76
+
77
+ return {
78
+ name: `@hagicode/hagilight-starlight:${instanceId}`,
79
+ hooks: {
80
+ 'astro:config:setup'({ updateConfig }) {
81
+ updateConfig({ vite: { plugins: [vitePlugin] } });
82
+ },
83
+ },
84
+ };
85
+ }
86
+
3
87
  export default function hagilight(options = {}) {
4
- const footer = options.promoto?.enabled === false ? 'Footer.astro' : 'PromotoFooter.astro';
88
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
89
+ throw new TypeError('Hagilight Starlight options must be an object.');
90
+ }
91
+ if (options.analytics !== undefined
92
+ && (!options.analytics || typeof options.analytics !== 'object' || Array.isArray(options.analytics))) {
93
+ throw new TypeError('Hagilight analytics options must be an object.');
94
+ }
95
+ const analytics = options.analytics ?? {};
96
+ const googleAnalyticsMeasurementId = resolveProvider(analytics.googleAnalytics, {
97
+ name: 'Google Analytics',
98
+ idKey: 'measurementId',
99
+ pattern: GOOGLE_ID_PATTERN,
100
+ });
101
+ const fiftyOneLaId = resolveProvider(analytics.fiftyOneLa, {
102
+ name: '51LA',
103
+ idKey: 'siteId',
104
+ pattern: FIFTY_ONE_LA_ID_PATTERN,
105
+ });
106
+ const instanceId = randomUUID();
107
+ const componentIds = {
108
+ footer: `virtual:hagilight-starlight/${instanceId}/Footer.astro`,
109
+ head: googleAnalyticsMeasurementId
110
+ ? `virtual:hagilight-starlight/${instanceId}/Head.astro`
111
+ : undefined,
112
+ };
113
+ const serializedOptions = {
114
+ promotoEnabled: options.promoto?.enabled !== false,
115
+ links: options.links ?? {},
116
+ googleAnalyticsMeasurementId,
117
+ fiftyOneLaId,
118
+ };
119
+
5
120
  return {
6
121
  name: '@hagicode/hagilight-starlight',
7
122
  hooks: {
8
- 'config:setup'({ config, updateConfig }) {
123
+ 'config:setup'({ config, updateConfig, addIntegration }) {
9
124
  if (config.components?.Footer) {
10
125
  throw new Error('Hagilight cannot replace an existing Starlight Footer override.');
11
126
  }
127
+ if (googleAnalyticsMeasurementId && config.components?.Head) {
128
+ throw new Error('Hagilight Google Analytics cannot replace an existing Starlight Head override. Compose @hagicode/hagilight/GoogleAnalytics in your Head instead.');
129
+ }
12
130
 
131
+ addIntegration(createConfiguredIntegration(instanceId, serializedOptions, componentIds));
13
132
  updateConfig({
14
133
  components: {
15
134
  ...config.components,
16
- Footer: fileURLToPath(new URL(`./${footer}`, import.meta.url)),
135
+ Footer: componentIds.footer,
136
+ ...(componentIds.head ? { Head: componentIds.head } : {}),
17
137
  },
18
138
  });
19
139
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagilight-starlight",
3
- "version": "0.1.2-dev.5.1.c41051d",
3
+ "version": "0.1.2-dev.6.1.f3f7b3d",
4
4
  "type": "module",
5
5
  "description": "Starlight customizations for HagiCode documentation",
6
6
  "repository": {
@@ -22,7 +22,7 @@
22
22
  "*.astro"
23
23
  ],
24
24
  "dependencies": {
25
- "@hagicode/hagilight": "0.1.2-dev.5.1.c41051d"
25
+ "@hagicode/hagilight": "0.1.2-dev.6.1.f3f7b3d"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@astrojs/starlight": "^0.42.4",