Skip to content

A Complete Practical Guide to Integrating Microsoft Clarity into a Starlight Documentation Site

火山引擎 Coding Plan
火山引擎提供 Claude API 兼容服务,稳定可靠。订阅折上9折,低至8.9元,订阅越多越划算!
立即订阅
智谱 GLM Coding: 20+ 大编程工具无缝支持 推荐
Claude Code、Cline 等 20+ 大编程工具无缝支持,"码力"全开,越拼越爽!
立即开拼
MiniMax Claude API 兼容服务
MiniMax 提供 Claude API 兼容服务,支持多种模型接入,稳定可靠。
了解更多
阿里云千问 Coding Plan 上线
阿里云千问 Coding Plan 已上线,满足开发日常需求。推荐 + Hagicode,完美实现开发过程中的各项需求。
立即订阅

From Data Insight to User Growth: A Complete Guide to Integrating Clarity Analytics into the HagiCode Blog

Section titled “From Data Insight to User Growth: A Complete Guide to Integrating Clarity Analytics into the HagiCode Blog”

This article shares how to elegantly integrate Microsoft Clarity into a Starlight documentation site so you can clearly understand user behavior while still staying privacy-compliant. This solution is distilled from our implementation experience in the HagiCode project, and we hope it gives some useful reference to anyone else wrestling with analytics.

The following code shows how to dynamically inject the Microsoft Clarity script in an Astro integration based on environment variables, loading it in production only when it is actually enabled.

105 | interface Props {
106 | // Future extension: allow manually overriding the Project ID
107 | projectId?: string;
108 | }
109 |
110 | const {
111 | projectId = import.meta.env.CLARITY_PROJECT_ID,
112 | } = Astro.props;
113 |
114 | const isProduction = import.meta.env.PROD;
115 | ---
116 |
117 | {isProduction && projectId && (
118 | <script is:inline define:vars={{projectId}}>
119 | (function(c,l,a,r,i,t,y){

File: openspec/changes/archive/2026-01-30-microsoft-clarity-integration/design.md

While operating HagiCode, we kept running into a “black box” problem: we were producing content, but we had no clear view of how users were actually reading it. GitHub Stars can show some signal, but that feedback comes far too late. What we really needed to know was:

  • Do users actually finish reading our tutorials?
  • At which step do those complicated configuration docs drive people away?
  • Is our SEO optimization really bringing in meaningful traffic?

There are many analytics tools on the market, such as Google Analytics (GA) and Microsoft Clarity. GA is powerful but more complex to configure, and it is also more tightly constrained by privacy regulations such as GDPR. Clarity, as Microsoft’s free heatmap tool, is not only intuitive to use but also relatively easier to work with from a privacy-compliance perspective, making it a great fit for technical documentation sites.

Our goal was very clear: seamlessly integrate Clarity into the HagiCode documentation site so that it works across all pages while still giving users the right to opt out for privacy compliance.

HagiCode theme initialization logic: first read from local storage, then fall back to system preference, with dark mode as the default.

67 | function getInitialTheme(): Theme {
68 | // 1. Check localStorage
69 | const stored = localStorage.getItem('hagicode-theme');
70 | if (stored) return stored as Theme;
71 |
72 | // 2. Detect system preference
73 | const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
74 | if (systemDark) return 'dark';
75 |
76 | // 3. Default to dark mode
77 | return 'dark';
78 | }
79 | ```
80 |
81 | ### Decision 3: Theme application method
82 |
83 | **Choice**: set the `data-theme` attribute on the `<html>` root element
84 |
85 | **Alternative options**:
86 |

File: openspec/changes/archive/2026-01-29-theme-toggle-implementation/design.md

The approach shared in this article comes from our hands-on experience in the HagiCode project. HagiCode is an AI-based coding assistant tool, and during development we need to maintain a large body of technical documentation and blog content. To better understand user needs, we explored and implemented this analytics integration solution.

At first, we discussed several integration approaches during the Proposal phase. Since we were using Starlight, a documentation framework built on Astro, the most obvious idea was to use Astro Hooks.

We first tried modifying astro.config.mjs and planned to inject the Clarity script during the build. Although that approach could guarantee global coverage, it lacked flexibility: we could not dynamically load or unload the script based on user preference.

Taking user experience and privacy control into account, we ultimately chose a component override approach. Starlight allows developers to override its internal components, which means we could take over the rendering logic for <footer> or <head> and precisely control when Clarity is loaded.

There was also a small detour here: originally, we wanted to create a layout wrapper named StarlightWrapper.astro. But during real debugging, we found that Starlight’s routing mechanism does not automatically invoke this custom wrapper, causing the script to fail on some pages. It was a classic “obvious assumption” pitfall and reminded us that we must deeply understand the framework’s rendering flow instead of blindly applying generic framework patterns.

Section titled “Core Solution: Overriding the Footer Component”

To ensure the Clarity script loads on all pages, including docs and blog posts, without breaking the original page structure, we chose to override Starlight’s Footer component.

  1. Global presence: the Footer appears on almost all standard pages.
  2. Non-intrusive: placing the script in the Footer area, which is actually rendered near the bottom of the body, does not block the page’s critical rendering path (LCP), so the performance impact is minimal.
  3. Centralized logic: cookie consent logic can be handled in one place inside the component.

First, register at Microsoft Clarity and create a new project. Then get your Project ID, which looks like a string such as k8z2ab3xxx.

The following demonstrates environment variable configuration and date-checking logic to implement conditional behavior during the Lunar New Year period. Please refer to the concrete implementation.

46 | function isLunarNewYearPeriod() {
47 | const now = new Date();
48 | const year = now.getFullYear();
49 | const month = now.getMonth() + 1; // 1-12
50 | const day = now.getDate();
51 |
52 | // Lunar New Year period in 2025, Year of the Snake (January 29 - February 12)
53 | if (year === 2025) {
54 | if (month === 1 && day >= 29) return true;
55 | if (month === 2 && day <= 12) return true;
56 | }
57 | // Lunar New Year period in 2026, Year of the Horse (February 17 - March 3)
58 | if (year === 2026) {
59 | if (month === 2 && day >= 17) return true;
60 | if (month === 3 && day <= 3) return true;
61 | }
62 | return false;
63 | }
64 |
65 | const stored = localStorage.getItem('starlight-theme');

File: src/pages/index.astro

For safety, do not hardcode the ID. It is recommended to store it in an environment variable.

Create a .env file in the project root:

Terminal window
# Microsoft Clarity ID
PUBLIC_CLARITY_ID="your_Clarity_ID"

The following is an implementation that listens for system theme changes, showing how to follow the system theme only when the user has not set one manually.

445 | const handleChange = (e: MediaQueryListEvent) => {
446 | // Only follow the system when the user has not manually set a theme
447 | if (!localStorage.getItem(THEME_KEY)) {
448 | setThemeState(e.matches ? 'dark' : 'light');
449 | }
450 | };
451 |
452 | mediaQuery.addEventListener('change', handleChange);
453 | return () => mediaQuery.removeEventListener('change', handleChange);
454 | }, []);
455 |
456 | return { theme, toggleTheme, setTheme: manuallySetTheme };
457 | }
458 | ```
459 |
460 | #### 3. `src/components/ThemeButton.tsx` - Button component
461 |
462 | **Responsibility**: render the theme toggle button and handle user interaction
463 |
464 | **Component interface**:

File: openspec/changes/archive/2026-01-29-theme-toggle-implementation/design.md

Create a file named StarlightFooter.astro under src/components/. Starlight will automatically recognize it and override the default Footer.

The core code logic is as follows:

src/components/StarlightFooter.astro
---
// 1. Import the original component to preserve its default behavior
import DefaultFooter from '@astrojs/starlight/components/StarlightFooter.astro';
// 2. Read the environment variable
const clarityId = import.meta.env.PUBLIC_CLARITY_ID;
// 3. Define a simple injection script (inline approach)
// Note: in production, it is recommended to move this logic into a separate .js file to take advantage of caching
const initScript = `
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${clarityId}");
`;
---
<DefaultFooter {...Astro.props} />
{/* Inject the script only in production and only when the ID exists */}
{import.meta.env.PROD && clarityId && (
<script is:inline define:vars={{ clarityId }}>
{initScript}
</script>
)}

Key points explained:

  • is:inline: tells Astro not to process the contents of this script tag and to output it directly into the HTML. This is critical for third-party analytics scripts; otherwise, Astro’s bundling optimization may cause the script to stop working.
  • define:vars: an Astro 3+ feature that allows variables to be injected safely within scope.
  • import.meta.env.PROD: ensures no meaningless analytics are generated during local development unless explicitly needed for debugging, keeping your data clean.
Section titled “Advanced Topic: Privacy Compliance and Cookie Control”

Simply adding the code is not enough, especially in GDPR-regulated regions. We need to respect user choice.

HagiCode’s approach is to provide a simple toggle. While this is not a full-featured cookie banner, for a technical documentation site that mainly serves content, such cookies are often categorized as “necessary” or “analytics” cookies and can be disclosed in the privacy statement and enabled by default, or linked from the Footer to a privacy settings page.

If you need stricter control, you can combine it with localStorage to record the user’s choice:

This article will introduce TypeScript utility functions used for theme switching and persistence, using type safety and environment detection for stricter control.

367 | export function getInitialTheme(): Theme;
368 | export function getSystemTheme(): Theme;
369 | export function setTheme(theme: Theme): void;
370 | export function applyTheme(theme: Theme): void;
371 | ```
372 |
373 | **Design principles**:
374 | - **Pure functions**: no side effects, except for `setTheme` and `applyTheme`
375 | - **Type safety**: complete TypeScript type inference
376 | - **Environment detection**: SSR-safe (`typeof window` check)
377 | - **Single responsibility**: each function does only one thing
378 |
379 | **Key implementation**:
380 | ```typescript
381 | export function getInitialTheme(): Theme {
382 | if (typeof window === 'undefined') return 'dark';
383 |
384 | const stored = localStorage.getItem(THEME_KEY);
385 | if (stored === 'light' || stored === 'dark') return stored;
386 |

File: openspec/changes/archive/2026-01-29-theme-toggle-implementation/design.md

// Simple example: check whether the user has declined analytics
const consent = localStorage.getItem('clarity_consent');
if (consent !== 'denied') {
// Run the Clarity initialization code above
window.clarity('start', clarityId);
}

As we rolled this solution out in HagiCode, we summarized several details that are easy to overlook:

  1. StarlightWrapper.astro is a trap: As mentioned earlier, do not try to create a global Wrapper to inject the script. That approach does not work in Starlight. The correct solution is to override a specific component such as StarlightFooter.astro or StarlightHead.astro.

  2. Performance considerations for script placement: Although Clarity recommends placing the script in <head> to ensure maximum data accuracy, for a documentation site, first-screen loading speed (LCP) directly affects SEO and user retention. We chose to place it in the Footer, near the bottom of the body. This may slightly miss a tiny amount of “bounce in seconds” user data, but it gives us a faster page-loading experience, which is a worthwhile trade-off.

  3. Interference from the development environment: Be sure to add the import.meta.env.PROD check. In development mode, you will refresh pages frequently, which would otherwise generate a large amount of meaningless test data and pollute your Clarity dashboard.

After deployment, you can view real-time data in the Clarity console. Usually within a few minutes, you will start seeing user heatmaps and recordings.

For HagiCode, this data helped us discover that:

  • many users repeatedly revisit the “Quick Start” section, suggesting that our installation guidance may still not be intuitive enough.
  • the “API Reference” page has the longest dwell time, confirming the needs of our core user group.

Integrating Microsoft Clarity does not require complex server-side changes, nor does it require bringing in a heavy SDK.

By taking advantage of Starlight’s component override mechanism, we achieved site-wide analytics with nothing more than a lightweight StarlightFooter.astro component. This kind of “micro-integration” keeps the codebase clean while giving us the ability to understand user behavior.

If you are also operating a technical project, especially one like HagiCode where the documentation needs to keep evolving, I strongly recommend trying Clarity. The data will tell you where users are really struggling.


Thank you for reading. If you found this article useful, click the like button below 👍 so more people can discover it.

This content was created with AI-assisted collaboration, reviewed by me, and reflects my own views and position.