Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion FIRST_PARTY.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Four presets in `proxy-configs.ts` cover all proxy-enabled scripts:
| `PRIVACY_NONE` | all false | (not currently assigned to any script) |
| `PRIVACY_FULL` | all true | Meta, TikTok, X, Snap, Reddit, LinkedIn |
| `PRIVACY_HEATMAP` | ip, language, hardware | GA, Clarity, Hotjar |
| `PRIVACY_IP_ONLY` | ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Fathom, CF Web Analytics, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo |
| `PRIVACY_IP_ONLY` | ip only | PostHog, Plausible, Umami, Rybbit, Databuddy, Ahrefs, Fathom, CF Web Analytics, Vercel, Matomo, Carbon Ads, Lemon Squeezy, Intercom, Gravatar, YouTube, Vimeo |

Note: GTM, Segment, Crisp, Mixpanel, and Bing UET are bundle-only (no proxy capability), so no privacy transforms are applied.

Expand All @@ -128,6 +128,7 @@ Note: GTM, Segment, Crisp, Mixpanel, and Bing UET are bundle-only (no proxy capa
| `snapchatPixel` | snapchatPixel | `PRIVACY_FULL` | Path A |
| `redditPixel` | redditPixel | `PRIVACY_FULL` | Path A |
| `linkedinInsight` | linkedinInsight | `PRIVACY_FULL` | Path A |
| `ahrefsAnalytics` | ahrefsAnalytics | `PRIVACY_IP_ONLY` | Path A |
| `clarity` | clarity | `PRIVACY_HEATMAP` | Path A |
| `hotjar` | hotjar | `PRIVACY_HEATMAP` | Path A |
| `posthog` | posthog | `PRIVACY_IP_ONLY` | **Path B** (npm-only) + autoInject |
Expand Down
4 changes: 2 additions & 2 deletions docs/content/docs/1.guides/2.first-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
|------|-------------------|---------|
| **Full** | IP, user agent, language, screen, timezone, hardware fingerprints | Meta Pixel, TikTok Pixel, X Pixel, Snapchat Pixel, Reddit Pixel, LinkedIn Insight Tag |
| **Heatmap-safe** | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Microsoft Clarity, Hotjar |
| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense |
| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Ahrefs Web Analytics, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor | ⚑ Quick win

Google AdSense is listed under the wrong privacy tier.

The "IP only" row here includes Google AdSense, but according to FIRST_PARTY.md, AdSense shares Google Analytics's proxy config and uses PRIVACY_HEATMAP (IP + language + hardware). It should either be removed from this row or moved to Heatmap-safe alongside Google Analytics.

πŸ“ Suggested fix
-| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Ahrefs Web Analytics, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy, Google AdSense |
+| **IP only** | IP addresses anonymised to subnet level | Plausible, PostHog, Umami, Fathom, CF Web Analytics, Vercel Analytics, Rybbit, Databuddy, Matomo, Ahrefs Web Analytics, Intercom, YouTube, Vimeo, Gravatar, Carbon Ads, Lemon Squeezy |

And update the Heatmap-safe row:

-| **Heatmap-safe** | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Microsoft Clarity, Hotjar |
+| **Heatmap-safe** | IP, language, hardware fingerprints (preserves screen and user agent for session replay) | Google Analytics, Google AdSense, Microsoft Clarity, Hotjar |
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/1.guides/2.first-party.md` at line 66, The "IP only" table
row incorrectly lists Google AdSense; per FIRST_PARTY.md AdSense uses the same
proxy config as Google Analytics and belongs to PRIVACY_HEATMAP. Remove Google
AdSense from the "IP only" row and add it to the "Heatmap-safe" row alongside
Google Analytics (ensure the term "Google AdSense" matches existing casing and
that you reference PRIVACY_HEATMAP/Heatmap-safe in the docs to keep consistency
with FIRST_PARTY.md).


Sensitive headers (`cookie`, `authorization`) are **always** stripped regardless of tier.

Expand Down Expand Up @@ -180,7 +180,7 @@

### Static Hosting (SSG)

The reverse proxy requires a **server runtime**. For static deployments (`nuxt generate`), the proxy is automatically disabled. Scripts are still bundled and served from your domain, but runtime collection requests (analytics beacons, pixel fires) go directly to third-party servers.

Check warning on line 183 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

Check warning on line 183 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

If you need proxying with static hosting, configure platform-level rewrites manually. The pattern is `/_scripts/p/<domain>/:path*` β†’ `https://<domain>/:path*`:

Expand Down Expand Up @@ -323,7 +323,7 @@

| Category | Scripts |
|----------|---------|
| **Analytics** | [Google Analytics](/scripts/google-analytics), [Plausible](/scripts/plausible-analytics), [Cloudflare Web Analytics](/scripts/cloudflare-web-analytics), [Umami](/scripts/umami-analytics), [Fathom](/scripts/fathom-analytics), [Rybbit](/scripts/rybbit-analytics), [Databuddy](/scripts/databuddy-analytics), [Vercel Analytics](/scripts/vercel-analytics), [Microsoft Clarity](/scripts/clarity), [Hotjar](/scripts/hotjar) |
| **Analytics** | [Google Analytics](/scripts/google-analytics), [Plausible](/scripts/plausible-analytics), [Cloudflare Web Analytics](/scripts/cloudflare-web-analytics), [Umami](/scripts/umami-analytics), [Fathom](/scripts/fathom-analytics), [Rybbit](/scripts/rybbit-analytics), [Databuddy](/scripts/databuddy-analytics), [Ahrefs Web Analytics](/scripts/ahrefs-analytics), [Vercel Analytics](/scripts/vercel-analytics), [Microsoft Clarity](/scripts/clarity), [Hotjar](/scripts/hotjar) |
| **Ad Pixels** | [Meta Pixel](/scripts/meta-pixel), [TikTok Pixel](/scripts/tiktok-pixel), [X Pixel](/scripts/x-pixel), [Snapchat Pixel](/scripts/snapchat-pixel), [Reddit Pixel](/scripts/reddit-pixel), [LinkedIn Insight Tag](/scripts/linkedin-insight), [Google AdSense](/scripts/google-adsense) |
| **Video** | [YouTube Player](/scripts/youtube-player), [Vimeo Player](/scripts/vimeo-player) |
| **Utility** | [Intercom](/scripts/intercom), [Gravatar](/scripts/gravatar) |
Expand Down Expand Up @@ -461,7 +461,7 @@
| Problem | Fix |
|---------|-----|
| Analytics not tracking | Check DevTools β†’ Network for `/_scripts/p/` requests. Check Nitro server logs for proxy errors |
| Proxy not working on static site | The reverse proxy is automatically disabled for SSG. Use platform rewrites or switch to server mode. See [Static Hosting](#static-hosting-ssg) |

Check warning on line 464 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice

Check warning on line 464 in docs/content/docs/1.guides/2.first-party.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is automatically disabled". Consider rewriting in active voice
| Stale script | `rm -rf .nuxt/cache/scripts` and rebuild |
| Build download fails | Set `assets.fallbackOnSrcOnBundleFail: true`{lang="ts"} to fall back to direct loading |
| Debugging | Open Nuxt DevTools β†’ Scripts to see proxy routes and privacy status |
Expand Down
70 changes: 70 additions & 0 deletions docs/content/scripts/ahrefs-analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Ahrefs Web Analytics
description: Use Ahrefs Web Analytics in your Nuxt app to track page views and custom events with a privacy-first, cookie-less analytics script.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/registry/ahrefs-analytics.ts
size: xs
---

[Ahrefs Web Analytics](https://ahrefs.com/web-analytics) is a privacy-first, cookie-less web analytics service from [Ahrefs](https://ahrefs.com) that tracks page views and custom events without sharing visitor data with third parties.

::script-stats
::

::script-docs
::

The composable comes with the following defaults:
- **Trigger: Client** Script will load when Nuxt is hydrating.

You can access the `AhrefsAnalytics` object as a proxy directly or await the `$script` promise to access the object. It's recommended to use the proxy for any void functions.

::code-group

```ts [Proxy]
const { proxy } = useScriptAhrefsAnalytics({
key: 'your-project-key',
})
function trackSignup() {
proxy.AhrefsAnalytics.sendEvent('signup', {
props: { plan: 'pro' },
})
}
```

```ts [onLoaded]
const { onLoaded } = useScriptAhrefsAnalytics({
key: 'your-project-key',
})
onLoaded(({ AhrefsAnalytics }) => {
AhrefsAnalytics.sendEvent('signup', {
props: { plan: 'pro' },
})
})
```

::

## SPA navigation

Ahrefs Analytics tracks single-page-app navigations natively: the loaded `analytics.js` patches `history.pushState` and listens for `popstate`, firing a fresh page-view whenever the URL changes. No extra configuration is needed for Nuxt route changes.

Check warning on line 52 in docs/content/scripts/ahrefs-analytics.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is needed". Consider rewriting in active voice

Check warning on line 52 in docs/content/scripts/ahrefs-analytics.md

View workflow job for this annotation

GitHub Actions / lint

Passive voice: "is needed". Consider rewriting in active voice

::script-types
::

## Example

Loading Ahrefs Web Analytics through `app.vue` when Nuxt is ready.

```vue [app.vue]
<script setup lang="ts">
useScriptAhrefsAnalytics({
key: 'your-project-key',
scriptOptions: {
trigger: 'onNuxtReady'
}
})
</script>
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dev": "nuxt dev playground",
"dev:ssl": "nuxt dev playground --https",
"dev:prepare": "pnpm -r dev:prepare && nuxt prepare && nuxt prepare playground && pnpm prepare:fixtures",
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/cdn && nuxt prepare test/fixtures/extend-registry && nuxt prepare test/fixtures/partytown && nuxt prepare test/fixtures/first-party && nuxt prepare test/fixtures/linkedin-insight && nuxt prepare test/fixtures/linkedin-insight-cdn",
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/cdn && nuxt prepare test/fixtures/extend-registry && nuxt prepare test/fixtures/partytown && nuxt prepare test/fixtures/first-party && nuxt prepare test/fixtures/linkedin-insight && nuxt prepare test/fixtures/linkedin-insight-cdn && nuxt prepare test/fixtures/ahrefs-analytics && nuxt prepare test/fixtures/ahrefs-analytics-cdn",
"typecheck": "nuxt typecheck",
"release": "pnpm build && bumpp -r --output=CHANGELOG.md",
"lint": "eslint .",
Expand Down
23 changes: 23 additions & 0 deletions packages/script/src/plugins/rewrite-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,29 @@ export function rewriteScriptUrlsAST(content: string, filename: string, rewrites
}
}

// SDK patch: replace-new-url-origin
// Matches `new URL(<expr>).origin` and replaces with `self.location.origin + "<proxyPath>"`
if (sdkPatches?.some(p => p.type === 'replace-new-url-origin')
&& node.type === 'MemberExpression'
&& !(node as any).computed) {
const obj = (node as any).object
const prop = (node as any).property
if (prop?.type === 'Identifier' && prop.name === 'origin'
&& obj?.type === 'NewExpression'
&& obj.callee?.type === 'Identifier' && obj.callee.name === 'URL'
&& obj.arguments?.length >= 1) {
for (const patch of sdkPatches) {
if (patch.type !== 'replace-new-url-origin')
continue
const rewrite = rewrites.find(r => r.from === patch.fromDomain)
if (!rewrite)
continue
s.overwrite(node.start, node.end, `${needsLeadingSpace(node.start)}(self.location.origin+"${rewrite.to}")`)
break
}
}
}

// new XMLHttpRequest / new Image / new x.XMLHttpRequest / new x.Image
if (node.type === 'NewExpression' && !options?.skipApiRewrites) {
const callee = (node as any).callee
Expand Down
1 change: 1 addition & 0 deletions packages/script/src/registry-logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { BuiltInRegistryScriptKey } from './runtime/types'
type LogoValue = string | { light: string, dark: string }

export const LOGOS = {
ahrefsAnalytics: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#FF6A00" d="M9.6 4h4.8a4.8 4.8 0 0 1 4.8 4.8v4.8H9.6A4.8 4.8 0 0 1 4.8 8.8 4.8 4.8 0 0 1 9.6 4Zm14.4 24h-4.8a4.8 4.8 0 0 1-4.8-4.8v-9.6h4.8A4.8 4.8 0 0 1 24 18.4Z"/></svg>`,
plausibleAnalytics: `<svg height="32" id="Layer_2" viewBox="0 0 46 60" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient id="New_Gradient_Swatch_1" x1="14.841" y1="22.544" x2="27.473" y2="44.649" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#909cf7"/><stop offset="1" stop-color="#4b38d8"/></linearGradient><linearGradient id="New_Gradient_Swatch_1-2" x1="7.984" y1="-1.358" x2="21.001" y2="21.422" xlink:href="#New_Gradient_Swatch_1"/><style>.cls-3{stroke-width:0;fill:#1f2961}</style></defs><g id="Plausible_-_Branding"><g id="Gradient_Logo_-_Purple_Gradient_on_White"><g id="Symbol_-_Purple_Gradient"><path d="M45.246 22.603C44.155 33.059 35.013 40.83 24.5 40.83h-4.047v9.57a9.6 9.6 0 0 1-9.6 9.6H3.36A3.36 3.36 0 0 1 0 56.64V36.938l5.038-7.07a3.362 3.362 0 0 1 4.037-1.149l2.866 1.2a3.353 3.353 0 0 0 4.025-1.145l6.717-9.417a3.34 3.34 0 0 1 4.014-1.14l5.52 2.32a3.347 3.347 0 0 0 4.022-1.142l6.46-9.063c2.025 3.56 3.014 7.789 2.547 12.27Z" style="stroke-width:0;fill:url(#New_Gradient_Swatch_1)"/><path d="M3.292 28.873c.823-1.155 2.021-2.044 3.414-2.312a5.41 5.41 0 0 1 3.147.316l2.865 1.2a1.357 1.357 0 0 0 1.62-.464l6.594-9.245c.823-1.154 2.02-2.041 3.412-2.309a5.368 5.368 0 0 1 3.128.314l5.52 2.32a1.35 1.35 0 0 0 1.619-.46l6.919-9.707C37.827 3.364 31.78 0 24.945 0H3.36A3.36 3.36 0 0 0 0 3.36v30.132l3.292-4.62Z" style="fill:url(#New_Gradient_Swatch_1-2);stroke-width:0"/></g></g></g></svg>`,
cloudflareWebAnalytics: `<svg xmlns="http://www.w3.org/2000/svg" width="70.02" height="32" viewBox="0 0 256 117"><path fill="#FBAD41" d="M205.52 50.813c-.858 0-1.705.03-2.551.058c-.137.007-.272.04-.398.094a1.424 1.424 0 0 0-.92.994l-3.628 12.672c-1.565 5.449-.983 10.48 1.646 14.174c2.41 3.416 6.42 5.421 11.289 5.655l19.679 1.194c.585.03 1.092.312 1.4.776a1.92 1.92 0 0 1 .2 1.692a2.496 2.496 0 0 1-2.134 1.662l-20.448 1.193c-11.11.515-23.062 9.58-27.255 20.633l-1.474 3.9a1.092 1.092 0 0 0 .967 1.49h70.425a1.872 1.872 0 0 0 1.81-1.365A51.172 51.172 0 0 0 256 101.828c0-28.16-22.582-50.984-50.449-50.984"/><path fill="#F6821F" d="m174.782 115.362l1.303-4.583c1.568-5.449.987-10.48-1.639-14.173c-2.418-3.417-6.424-5.422-11.296-5.656l-92.312-1.193a1.822 1.822 0 0 1-1.459-.776a1.919 1.919 0 0 1-.203-1.693a2.496 2.496 0 0 1 2.154-1.662l93.173-1.193c11.063-.511 23.015-9.58 27.208-20.633l5.313-14.04c.214-.596.27-1.238.156-1.86C191.126 20.51 166.91 0 137.96 0C111.269 0 88.626 17.403 80.5 41.596a26.996 26.996 0 0 0-19.156-5.359C48.549 37.524 38.25 47.946 36.979 60.88a27.905 27.905 0 0 0 .702 9.642C16.773 71.145 0 88.454 0 109.726c0 1.923.137 3.818.413 5.667c.115.897.879 1.57 1.783 1.568h170.48a2.223 2.223 0 0 0 2.106-1.63"/></svg>`,
vercelAnalytics: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 512 512"><path d="M256 48L496 464H16z" fill="currentColor"/></svg>`,
Expand Down
30 changes: 30 additions & 0 deletions packages/script/src/registry-types.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
{
"types": {
"ahrefs-analytics": [
{
"name": "AhrefsAnalyticsOptions",
"kind": "const",
"code": "export const AhrefsAnalyticsOptions = object({\n /**\n * Your Ahrefs Web Analytics project key. Set as the `data-key` attribute\n * on the loaded `analytics.js` script tag.\n * @see https://ahrefs.com/web-analytics\n */\n key: pipe(string(), minLength(1)),\n})"
},
{
"name": "AhrefsAnalyticsSendEventOptions",
"kind": "interface",
"code": "export interface AhrefsAnalyticsSendEventOptions {\n /** Custom dimensions sent under `props`. */\n props?: Record<string, string>\n /** Arbitrary metadata sent under `meta`. */\n meta?: Record<string, unknown>\n /** Optional callback invoked once the beacon request completes. */\n callback?: (result?: { status?: number }) => void\n}"
},
{
"name": "AhrefsAnalyticsInstance",
"kind": "interface",
"code": "export interface AhrefsAnalyticsInstance {\n /**\n * Manually send an event to Ahrefs Analytics. The script auto-fires\n * page-view events on initial load and on `history.pushState`/`popstate`,\n * so SPA navigations are tracked without calling this.\n */\n sendEvent: (name: string, options?: AhrefsAnalyticsSendEventOptions) => void\n}"
},
{
"name": "AhrefsAnalyticsApi",
"kind": "interface",
"code": "export interface AhrefsAnalyticsApi {\n AhrefsAnalytics: AhrefsAnalyticsInstance\n}"
}
],
"bing-uet": [
{
"name": "BingUetOptions",
Expand Down Expand Up @@ -1115,6 +1137,14 @@
]
},
"schemaFields": {
"AhrefsAnalyticsOptions": [
{
"name": "key",
"type": "string",
"required": true,
"description": "Your Ahrefs Web Analytics project key. Set as the `data-key` attribute on the loaded `analytics.js` script tag."
}
],
"BlueskyEmbedOptions": [
{
"name": "postUrl",
Expand Down
15 changes: 15 additions & 0 deletions packages/script/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ProxyAutoInject, ProxyCapability, ProxyConfig, RegistryScript, Reg
import { joinURL, withBase, withQuery } from 'ufo'
import { LOGOS } from './registry-logos'
import {
AhrefsAnalyticsOptions,
BingUetOptions,
BlueskyEmbedOptions,
ClarityOptions,
Expand Down Expand Up @@ -132,6 +133,7 @@ export const registryMeta: RegistryScriptMeta[] = [
m('clarity', 'Clarity', 'analytics', 'useScriptClarity', { bundle: true, proxy: true, partytown: true }, PRIVACY_HEATMAP),
m('vercelAnalytics', 'Vercel Analytics', 'analytics', 'useScriptVercelAnalytics', { bundle: true, proxy: true }, PRIVACY_IP_ONLY),
m('mixpanelAnalytics', 'Mixpanel', 'analytics', 'useScriptMixpanelAnalytics', { bundle: true, partytown: true }, null),
m('ahrefsAnalytics', 'Ahrefs Web Analytics', 'analytics', 'useScriptAhrefsAnalytics', { bundle: true, proxy: true }, PRIVACY_IP_ONLY),
// ad
m('bingUet', 'Bing UET', 'ad', 'useScriptBingUet', { bundle: true, partytown: true }, null),
m('metaPixel', 'Meta Pixel', 'ad', 'useScriptMetaPixel', { bundle: true, proxy: true, partytown: true }, PRIVACY_FULL),
Expand Down Expand Up @@ -274,6 +276,19 @@ export async function registry(resolve?: (path: string) => Promise<string>): Pro

return Promise.all([
// analytics
def('ahrefsAnalytics', {
schema: AhrefsAnalyticsOptions,
label: 'Ahrefs Web Analytics',
src: 'https://analytics.ahrefs.com/analytics.js',
category: 'analytics',
envDefaults: { key: '' },
bundle: true,
proxy: {
domains: ['analytics.ahrefs.com'],
privacy: PRIVACY_IP_ONLY,
sdkPatches: [{ type: 'replace-new-url-origin', fromDomain: 'analytics.ahrefs.com' }],
},
}),
def('plausibleAnalytics', {
label: 'Plausible Analytics',
category: 'analytics',
Expand Down
60 changes: 60 additions & 0 deletions packages/script/src/runtime/registry/ahrefs-analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { RegistryScriptInput, UseScriptContext } from '#nuxt-scripts/types'
import { useRegistryScript } from '../utils'
import { AhrefsAnalyticsOptions } from './schemas'

export { AhrefsAnalyticsOptions }

export type AhrefsAnalyticsInput = RegistryScriptInput<typeof AhrefsAnalyticsOptions, true, false>

export interface AhrefsAnalyticsSendEventOptions {
/** Custom dimensions sent under `props`. */
props?: Record<string, string>
/** Arbitrary metadata sent under `meta`. */
meta?: Record<string, unknown>
/** Optional callback invoked once the beacon request completes. */
callback?: (result?: { status?: number }) => void
}

export interface AhrefsAnalyticsInstance {
/**
* Manually send an event to Ahrefs Analytics. The script auto-fires
* page-view events on initial load and on `history.pushState`/`popstate`,
* so SPA navigations are tracked without calling this.
*/
sendEvent: (name: string, options?: AhrefsAnalyticsSendEventOptions) => void
}

export interface AhrefsAnalyticsApi {
AhrefsAnalytics: AhrefsAnalyticsInstance
}

declare global {
interface Window extends AhrefsAnalyticsApi {}
}

/**
* Load Ahrefs Web Analytics and expose its `sendEvent` API.
*
* The script attaches `window.AhrefsAnalytics` once loaded, fires an initial
* page-view, and tracks SPA navigations natively by patching
* `history.pushState` and listening to `popstate`.
*
* @see https://ahrefs.com/web-analytics
*/
export function useScriptAhrefsAnalytics<T extends AhrefsAnalyticsApi>(
_options?: AhrefsAnalyticsInput,
): UseScriptContext<T> {
return useRegistryScript<T, typeof AhrefsAnalyticsOptions>('ahrefsAnalytics', options => ({
scriptInput: {
'src': 'https://analytics.ahrefs.com/analytics.js',
'data-key': options.key,
'crossorigin': false,
},
schema: import.meta.dev ? AhrefsAnalyticsOptions : undefined,
scriptOptions: {
use() {
return { AhrefsAnalytics: window.AhrefsAnalytics }
},
},
}), _options)
}
9 changes: 9 additions & 0 deletions packages/script/src/runtime/registry/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ const gcmConsentState = object({
region: optional(array(string())),
})

export const AhrefsAnalyticsOptions = object({
/**
* Your Ahrefs Web Analytics project key. Set as the `data-key` attribute
* on the loaded `analytics.js` script tag.
* @see https://ahrefs.com/web-analytics
*/
key: pipe(string(), minLength(1)),
})

export const BlueskyEmbedOptions = object({
/**
* The Bluesky post URL to embed.
Expand Down
11 changes: 10 additions & 1 deletion packages/script/src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
import type { Import } from 'unimport'
import type { InferInput, ObjectEntries, ObjectSchema, UnionSchema, ValiError } from 'valibot'
import type { ComputedRef, Ref } from 'vue'
import type { AhrefsAnalyticsInput } from './registry/ahrefs-analytics'
import type { BingUetInput } from './registry/bing-uet'
import type { BlueskyEmbedInput } from './registry/bluesky-embed'
import type { ClarityInput } from './registry/clarity'
Expand Down Expand Up @@ -224,6 +225,7 @@ export interface NuxtDevToolsScriptInstance {
}

export interface ScriptRegistry {
ahrefsAnalytics?: AhrefsAnalyticsInput
bingUet?: BingUetInput
blueskyEmbed?: BlueskyEmbedInput
carbonAds?: true
Expand Down Expand Up @@ -271,7 +273,7 @@ export interface ScriptRegistry {
* Use this to type-check records that must enumerate all built-in scripts (logos, meta, etc.).
*/
export type BuiltInRegistryScriptKey
= | 'bingUet' | 'blueskyEmbed' | 'carbonAds' | 'crisp' | 'clarity' | 'cloudflareWebAnalytics'
= | 'ahrefsAnalytics' | 'bingUet' | 'blueskyEmbed' | 'carbonAds' | 'crisp' | 'clarity' | 'cloudflareWebAnalytics'
| 'databuddyAnalytics' | 'metaPixel' | 'fathomAnalytics' | 'instagramEmbed'
| 'plausibleAnalytics' | 'googleAdsense' | 'googleAnalytics' | 'googleMaps'
| 'googleRecaptcha' | 'googleSignIn' | 'lemonSqueezy' | 'googleTagManager'
Expand Down Expand Up @@ -433,6 +435,13 @@ export type SdkPatch
* the correct proxy path.
*/
| { type: 'replace-src-split', separator: string, fromDomain: string, appendPath?: string }
/**
* Replace `new URL(<expr>).origin` with `self.location.origin + "<proxyPath>"`.
* Used by SDKs that derive their API host as `new URL(currentScript.src).origin + "/api/..."`.
* When bundled, the script src origin is the Nuxt origin, so the derived endpoint
* lands on a 404 instead of the proxy. This patch redirects it through the proxy.
*/
| { type: 'replace-new-url-origin', fromDomain: string }

/**
* Partytown capability config. When present, the script can run in a
Expand Down
Loading
Loading