@hagicode/hagilight-core 0.6.1-dev.74.1.7df157f → 0.6.1

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/Footer.astro CHANGED
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import Copyright from './Copyright.astro';
3
+ import { siteLinkGaAttributes } from './dist/analytics-events.js';
3
4
  import { resolveSiteLinks, type SiteLinksOptions } from './dist/links.js';
4
5
  import { resolveRssFooterLinks } from './dist/rss-footer-context.js';
5
6
 
@@ -51,6 +52,7 @@ const resolvedLinks = resolveSiteLinks(locale, {
51
52
  target={link.target}
52
53
  rel={link.rel}
53
54
  aria-label={link.ariaLabel}
55
+ {...siteLinkGaAttributes(link, 'footer')}
54
56
  >{link.label}</a>
55
57
  </li>
56
58
  ))}
@@ -68,6 +70,7 @@ const resolvedLinks = resolveSiteLinks(locale, {
68
70
  target={link.target}
69
71
  rel={link.rel}
70
72
  aria-label={link.ariaLabel}
73
+ {...siteLinkGaAttributes(link, 'footer')}
71
74
  >{link.label}</a>
72
75
  </li>
73
76
  ))}
@@ -19,3 +19,9 @@ const scriptUrl = `https://www.googletagmanager.com/gtag/js?id=${encodeURICompon
19
19
  </script>
20
20
  </>
21
21
  )}
22
+
23
+ <script>
24
+ import { installGaEventTracking } from './dist/analytics-events.js';
25
+
26
+ installGaEventTracking();
27
+ </script>
package/README.md CHANGED
@@ -27,7 +27,7 @@ attribution by itself. Plain-Astro sites that want the literal
27
27
  `@hagicode/hagilight/Footer`; that component reads the exact build-time version
28
28
  from `@hagicode/hagilight`'s own manifest, not from Astro.
29
29
 
30
- Other entry points include `Copyright`, `PromotoBanner`, `GoogleAnalytics`, and `Analytics51LA` Astro components; `links`, `favicon`, `seo`, `seo-schema`, `rss`, `rss-ownership`, and `promotions` utilities; and `logo.png` and `favicon.ico` assets. For example:
30
+ Other entry points include `Copyright`, `PromotoBanner`, `GoogleAnalytics`, and `Analytics51LA` Astro components; `links`, `favicon`, `seo`, `seo-schema`, `rss`, `rss-ownership`, `promotions`, and `analytics-events` utilities; and `logo.png` and `favicon.ico` assets. For example:
31
31
 
32
32
  ```ts
33
33
  import { resolveSiteLinks } from '@hagicode/hagilight-core/links';
@@ -35,4 +35,55 @@ import { resolveSiteLinks } from '@hagicode/hagilight-core/links';
35
35
  const links = resolveSiteLinks('en-US');
36
36
  ```
37
37
 
38
+ ## Google Analytics click events
39
+
40
+ `GoogleAnalytics` also reports clicks on the key links and download buttons that the shared components render. There is nothing to configure: events follow the same rules as page views, so they are sent only on production pages where Google Analytics is enabled, never on the Starlight 404 page, and silently skipped when `gtag` is missing (development builds, blocked scripts).
41
+
42
+ A click on a tagged link calls `gtag('event', <action>, …)` once:
43
+
44
+ | Field | Value |
45
+ | --- | --- |
46
+ | Event name (action) | `download_click` for the `download` category, `link_click` for every other category |
47
+ | `event_category` | `download`, `navigation`, `community`, or `promotion` |
48
+ | `event_label` | The link's stable id (for example `downloadClient`), never its localized text |
49
+ | `link_location` | Where the link sits: `header`, `footer`, `article_promotion`, `promoto_banner`, or your own value |
50
+ | `link_url` | The destination at click time |
51
+ | `transport_type` | `beacon`, so same-tab navigations still deliver |
52
+
53
+ Reporting never delays, cancels, or changes navigation, and carries no link text, form values, or visitor identifiers.
54
+
55
+ ### Tracked links
56
+
57
+ The shared components tag exactly these links. The authoritative list is `TRACKED_SITE_LINKS` in `src/analytics-events.ts`; renaming a link id changes its GA label.
58
+
59
+ | `event_label` | `event_category` | Reported from |
60
+ | --- | --- | --- |
61
+ | `home`, `blog`, `support` | `navigation` | header (`home` also in the HagiCode showcase) |
62
+ | `dockerCompose`, `blogPosts` | `navigation` | footer quick links |
63
+ | `productDocs` | `navigation` | footer quick links, HagiCode showcase |
64
+ | `downloadClient`, `microsoftStore` | `download` | footer quick links, HagiCode showcase (the Microsoft Store badge and its fallback link report `microsoftStore`) |
65
+ | `github`, `discord`, `issueFeedback` | `community` | footer community links |
66
+ | the promotion's id | `promotion` | floating promotion banner call to action, including rotated slides |
67
+
68
+ Related-site links, RSS, sitemap, about, contact, filings, the language chooser, and links you add through `extraLinks` do not report events. A consumer that overrides a tracked link's destination keeps the original label and reports the new destination as `link_url`.
69
+
70
+ ### Tag your own links
71
+
72
+ ```astro
73
+ ---
74
+ import { gaEventAttributes } from '@hagicode/hagilight-core/analytics-events';
75
+ ---
76
+
77
+ <a
78
+ href="/downloads/"
79
+ {...gaEventAttributes({ category: 'download', label: 'pricingDownload', location: 'pricing' })}
80
+ >Download</a>
81
+ ```
82
+
83
+ `gaEventAttributes()` returns the `data-ga-category`, `data-ga-label`, `data-ga-location`, and optional `data-ga-url` attributes (pass `url` when the tagged element has no `href` of its own). It throws a `TypeError` for an unknown category or an empty label or location. A hand-written `data-ga-*` tag with an invalid category is ignored at click time. The page must render `GoogleAnalytics` for the click to be reported. `siteLinkGaAttributes(link, location)` tags a resolved site link and returns `{}` for ids outside the tracked list.
84
+
85
+ ### Reports
86
+
87
+ `event_category`, `event_label`, `link_location`, and `link_url` are custom event parameters. The property owner must register them as GA4 custom dimensions before they appear in standard reports; until then they are visible in DebugView and exports only. Ad blockers and browsers that block Google Analytics drop these events like any other, so treat the counts as directional.
88
+
38
89
  See the [repository documentation](https://github.com/HagiCode-org/hagilight#readme) for the full list of entry points and usage.
@@ -0,0 +1,49 @@
1
+ import type { SiteLink, SiteLinkKey } from './links.js';
2
+ export declare const GA_CATEGORIES: readonly ["download", "navigation", "community", "promotion"];
3
+ export type GaCategory = (typeof GA_CATEGORIES)[number];
4
+ export type GaAction = 'download_click' | 'link_click';
5
+ export declare const GA_CATEGORY_ACTIONS: Readonly<Record<GaCategory, GaAction>>;
6
+ export declare const GA_LOCATIONS: readonly ["header", "footer", "article_promotion", "promoto_banner"];
7
+ export type GaLocation = (typeof GA_LOCATIONS)[number];
8
+ /** Shared links that report click events, keyed by `SiteLink.id`. Renaming a key changes its GA label. */
9
+ export declare const TRACKED_SITE_LINKS: Readonly<Partial<Record<SiteLinkKey, GaCategory>>>;
10
+ export interface GaEventTag {
11
+ category: GaCategory;
12
+ /** Stable identifier, never localized text. */
13
+ label: string;
14
+ /** Where the link sits, for example one of `GA_LOCATIONS`. */
15
+ location: string;
16
+ /** Destination to report when the tagged element has no `href` of its own. */
17
+ url?: string;
18
+ }
19
+ export interface GaEventAttributes {
20
+ 'data-ga-category': GaCategory;
21
+ 'data-ga-label': string;
22
+ 'data-ga-location': string;
23
+ 'data-ga-url'?: string;
24
+ }
25
+ export interface GaEventParams {
26
+ event_category: GaCategory;
27
+ event_label: string;
28
+ link_location: string;
29
+ link_url?: string;
30
+ transport_type: 'beacon';
31
+ }
32
+ export type GaSend = (action: GaAction, params: GaEventParams) => void;
33
+ /** Builds the `data-ga-*` attributes that make a link report a click event. */
34
+ export declare function gaEventAttributes(tag: GaEventTag): GaEventAttributes;
35
+ /** Returns tag attributes for catalog links in `TRACKED_SITE_LINKS` and `{}` for every other link. */
36
+ export declare function siteLinkGaAttributes(link: Pick<SiteLink, 'id'>, location: string): Partial<GaEventAttributes>;
37
+ /**
38
+ * Reports the first tagged element in the click's composed path. Never throws and never
39
+ * touches the event, so navigation is unaffected.
40
+ */
41
+ export declare function handleGaClick(event: {
42
+ composedPath(): readonly unknown[];
43
+ }, send: GaSend): void;
44
+ /**
45
+ * Installs one capture-phase click listener per document. Events are sent only while
46
+ * `gtag` exists, so development builds, disabled sites, and the 404 page stay silent.
47
+ * Returns `false` when the document was already instrumented.
48
+ */
49
+ export declare function installGaEventTracking(target?: Pick<EventTarget, 'addEventListener'>, getGtag?: () => unknown): boolean;
@@ -0,0 +1,112 @@
1
+ export const GA_CATEGORIES = ['download', 'navigation', 'community', 'promotion'];
2
+ // GA4 has no native category/action/label: the action is the event name.
3
+ export const GA_CATEGORY_ACTIONS = Object.freeze({
4
+ download: 'download_click',
5
+ navigation: 'link_click',
6
+ community: 'link_click',
7
+ promotion: 'link_click',
8
+ });
9
+ export const GA_LOCATIONS = ['header', 'footer', 'article_promotion', 'promoto_banner'];
10
+ /** Shared links that report click events, keyed by `SiteLink.id`. Renaming a key changes its GA label. */
11
+ export const TRACKED_SITE_LINKS = Object.freeze({
12
+ home: 'navigation',
13
+ blog: 'navigation',
14
+ support: 'navigation',
15
+ downloadClient: 'download',
16
+ microsoftStore: 'download',
17
+ dockerCompose: 'navigation',
18
+ productDocs: 'navigation',
19
+ blogPosts: 'navigation',
20
+ github: 'community',
21
+ discord: 'community',
22
+ issueFeedback: 'community',
23
+ });
24
+ function isCategory(value) {
25
+ return typeof value === 'string' && GA_CATEGORIES.includes(value);
26
+ }
27
+ function requireText(name, value) {
28
+ if (typeof value !== 'string' || !value.trim()) {
29
+ throw new TypeError(`Analytics event ${name} must be a non-empty string.`);
30
+ }
31
+ return value;
32
+ }
33
+ /** Builds the `data-ga-*` attributes that make a link report a click event. */
34
+ export function gaEventAttributes(tag) {
35
+ if (!isCategory(tag.category)) {
36
+ throw new TypeError(`Analytics event category must be one of: ${GA_CATEGORIES.join(', ')}.`);
37
+ }
38
+ return {
39
+ 'data-ga-category': tag.category,
40
+ 'data-ga-label': requireText('label', tag.label),
41
+ 'data-ga-location': requireText('location', tag.location),
42
+ ...(tag.url === undefined ? {} : { 'data-ga-url': requireText('url', tag.url) }),
43
+ };
44
+ }
45
+ /** Returns tag attributes for catalog links in `TRACKED_SITE_LINKS` and `{}` for every other link. */
46
+ export function siteLinkGaAttributes(link, location) {
47
+ if (!Object.hasOwn(TRACKED_SITE_LINKS, link.id))
48
+ return {};
49
+ return gaEventAttributes({
50
+ category: TRACKED_SITE_LINKS[link.id],
51
+ label: link.id,
52
+ location,
53
+ });
54
+ }
55
+ function isTaggedElement(value) {
56
+ return typeof value === 'object' && value !== null
57
+ && typeof value.getAttribute === 'function'
58
+ && (value.getAttribute('data-ga-category') !== null
59
+ || value.getAttribute('data-ga-label') !== null);
60
+ }
61
+ function nonEmpty(value) {
62
+ return typeof value === 'string' && value.trim() ? value : undefined;
63
+ }
64
+ /**
65
+ * Reports the first tagged element in the click's composed path. Never throws and never
66
+ * touches the event, so navigation is unaffected.
67
+ */
68
+ export function handleGaClick(event, send) {
69
+ try {
70
+ const element = event.composedPath().find(isTaggedElement);
71
+ if (!element)
72
+ return;
73
+ const category = element.getAttribute('data-ga-category');
74
+ const label = nonEmpty(element.getAttribute('data-ga-label'));
75
+ const location = nonEmpty(element.getAttribute('data-ga-location'));
76
+ if (!isCategory(category) || !label || !location)
77
+ return;
78
+ const linkUrl = nonEmpty(element.getAttribute('data-ga-url'))
79
+ ?? nonEmpty(element.href)
80
+ ?? nonEmpty(element.getAttribute('href'));
81
+ send(GA_CATEGORY_ACTIONS[category], {
82
+ event_category: category,
83
+ event_label: label,
84
+ link_location: location,
85
+ ...(linkUrl === undefined ? {} : { link_url: linkUrl }),
86
+ transport_type: 'beacon',
87
+ });
88
+ }
89
+ catch {
90
+ // Analytics must never interfere with the click.
91
+ }
92
+ }
93
+ const INSTALLED = Symbol.for('hagilight.ga-event-tracking');
94
+ /**
95
+ * Installs one capture-phase click listener per document. Events are sent only while
96
+ * `gtag` exists, so development builds, disabled sites, and the 404 page stay silent.
97
+ * Returns `false` when the document was already instrumented.
98
+ */
99
+ export function installGaEventTracking(target = document, getGtag = () => globalThis.gtag) {
100
+ const marked = target;
101
+ if (marked[INSTALLED])
102
+ return false;
103
+ marked[INSTALLED] = true;
104
+ target.addEventListener('click', (event) => {
105
+ handleGaClick(event, (action, params) => {
106
+ const gtag = getGtag();
107
+ if (typeof gtag === 'function')
108
+ gtag('event', action, params);
109
+ });
110
+ }, true);
111
+ return true;
112
+ }
@@ -1,3 +1,4 @@
1
+ import { gaEventAttributes } from './analytics-events.js';
1
2
  import { loadActivePromotions } from './promotions.js';
2
3
  const ELEMENT_NAME = 'hagilight-promoto-banner';
3
4
  const DISMISSED_SIGNATURE_KEY = 'hagilight:promoto-banner:dismissed-signature';
@@ -228,6 +229,12 @@ export function definePromotoBannerElement() {
228
229
  link.target = '_blank';
229
230
  link.rel = 'noopener noreferrer';
230
231
  link.textContent = card.ctaLabel;
232
+ for (const [name, value] of Object.entries(gaEventAttributes({
233
+ category: 'promotion',
234
+ label: card.id,
235
+ location: 'promoto_banner',
236
+ })))
237
+ link.setAttribute(name, value);
231
238
  slide.append(copy);
232
239
  if (card.image) {
233
240
  const media = document.createElement('span');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagilight-core",
3
- "version": "0.6.1-dev.74.1.7df157f",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Shared TypeScript utilities, components, and assets for HagiCode Astro and Starlight sites",
6
6
  "repository": {
@@ -46,6 +46,10 @@
46
46
  "types": "./dist/promotions.d.ts",
47
47
  "default": "./dist/promotions.js"
48
48
  },
49
+ "./analytics-events": {
50
+ "types": "./dist/analytics-events.d.ts",
51
+ "default": "./dist/analytics-events.js"
52
+ },
49
53
  "./Copyright": "./Copyright.astro",
50
54
  "./PromotoBanner": "./PromotoBanner.astro",
51
55
  "./GoogleAnalytics": "./GoogleAnalytics.astro",