Skip to content

Commit

Permalink
feat: useTrackedPage
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Mar 16, 2024
1 parent d67369f commit 0367917
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 55 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,36 @@ useTrackingScript('https://www.google-analytics.com/analytics.js', {
})
```

### `useTrackedPage`

It's common when using tracking scripts to send an event when the page changes. Due to Nuxt's head implementation being
async, it's not possible to send the page title on route change.

`useTrackedPage` solves this by providing you with the page title and path when they change.

You can either provide a function to call on page change or use the ref that's returned.

```ts
useTrackedPage(({ title, path }) => {
gtag('event', 'page_view', {
page_title: title,
page_location: 'https://example.com',
page_path: path
})
})
```

```ts
const trackedPage = useTrackedPage()
watch(trackedPage, ({ title, path }) => {
gtag('event', 'page_view', {
page_title: title,
page_location: 'https://example.com',
page_path: path
})
})
```

## License

Licensed under the [MIT license](https://github.com/nuxt/scripts/blob/main/LICENSE.md).
Expand Down
13 changes: 13 additions & 0 deletions playground/app.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
<script lang="ts" setup>
import { useTrackedPage } from '../src/runtime/composables/useTrackedPage'
import { useTrackingScript } from '../src/runtime/composables/useTrackingScript'
const { track } = useTrackingScript<{ track: (title: string, path: string) => void }>('https://example.com/script.js', {
ignoreDoNotTrack: true,
consent: true,
})!
useTrackedPage((payload) => {
track(payload)
})
</script>

<template>
<div class="flex flex-col min-h-screen">
<header class="sticky top-0 z-50 w-full backdrop-blur flex-none border-b border-gray-900/10 dark:border-gray-50/[0.06] bg-white/75 dark:bg-gray-900/75">
Expand Down
6 changes: 5 additions & 1 deletion playground/pages/analytics/cloudflare.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<script lang="ts" setup>
import { useCloudflareAnalytics } from '../../../third-parties/src/runtime/composables/cloudflareAnalytics'
import { ref } from '#imports'
import { ref, useHead } from '#imports'
useHead({
title: 'Cloudflare',
})
// composables return the underlying api as a proxy object and a $script with the script state
const { $script } = useCloudflareAnalytics({
Expand Down
6 changes: 5 additions & 1 deletion playground/pages/analytics/fathom.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<script lang="ts" setup>
import { useFathomAnalytics } from '../../../third-parties/src/runtime/composables/fathomAnalytics'
import { ref } from '#imports'
import { ref, useHead } from '#imports'
useHead({
title: 'Fathom',
})
// composables return the underlying api as a proxy object and a $script with the script state
const { $script, trackPageview, trackGoal } = useFathomAnalytics({
Expand Down
58 changes: 5 additions & 53 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<script lang="ts" setup>
import type { VueScriptInstance } from '@unhead/vue'
import { type JSConfettiApi, useConfetti } from '../../third-parties/src/runtime/composables/confetti'
import { ref } from '#imports'
import { ref, useHead } from '#imports'
const state = ref<{ trigger: 'default' | 'manual' | 'idle', assetStrategy: 'default' | 'inline' | 'proxy' }>({
trigger: 'default',
assetStrategy: 'default',
})
useHead({
title: 'Home',
})
const script = ref<VueScriptInstance<JSConfettiApi> | null>(null)
let doConfetti: JSConfettiApi['addConfetti'] = () => {}
Expand Down Expand Up @@ -111,22 +115,6 @@ function reset() {
Fathom Analytics
</ULink>
</li>
<li>
<ULink
to="/analytics/google-analytics"
class="underline"
>
Google Analytics
</ULink>
</li>
<li>
<ULink
to="/analytics/google-tag-manager"
class="underline"
>
Google Tag Manager
</ULink>
</li>
<li>
<ULink
to="/analytics/cloudflare"
Expand Down Expand Up @@ -163,42 +151,6 @@ function reset() {
</li>
</ul>
</div>
<div>
<h2 class="font-bold mb-5 text-xl flex items-center">
<Icon
name="carbon:video-player"
class="opacity-70 mr-2"
/>Video
</h2>
<ul class="space-y-5">
<li>
<ULink
to="/video/youtube"
class="underline"
>
Youtube
</ULink>
</li>
</ul>
</div>
<div>
<h2 class="font-bold mb-5 text-xl flex items-center">
<Icon
name="carbon:map"
class="opacity-70 mr-2"
/>Map
</h2>
<ul class="space-y-5">
<li>
<ULink
to="/maps/google-maps"
class="underline"
>
Google Maps
</ULink>
</li>
</ul>
</div>
</div>
</div>
</template>
40 changes: 40 additions & 0 deletions src/runtime/composables/useTrackedPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { injectHead, ref, useNuxtApp, useRoute } from '#imports'
import type { TrackedPage } from '#nuxt-scripts'

export function useTrackedPage(onChange?: (payload: TrackedPage) => void) {
const nuxt = useNuxtApp()
const route = useRoute()
const head = injectHead()
const payload = ref<TrackedPage>({
path: route.fullPath,
title: typeof document !== 'undefined' ? document.title : '',
})
let lastPayload: TrackedPage = { path: '', title: '' }
if (import.meta.server) {
// we need to compute the title ahead of time
return payload
}
let stopDomWatcher: () => void
// TODO make sure useAsyncData isn't running
nuxt.hooks.hook('page:finish', () => {
Promise.race([
// possibly no head update is needed
new Promise(resolve => setTimeout(resolve, 100)),
new Promise((resolve) => {
stopDomWatcher = head.hooks.hook('dom:rendered', () => resolve())
}),
]).finally(() => {
stopDomWatcher && stopDomWatcher()
}).then(() => {
payload.value = {
path: route.fullPath,
title: document.title,
}
if (lastPayload.path !== payload.value.path || lastPayload.title !== payload.value.title) {
onChange && onChange(payload.value)
lastPayload = payload.value
}
})
})
return payload
}
5 changes: 5 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type NuxtUseScriptOptions<T = any> = UseScriptOptions<T>

export type NuxtUseScriptInput = UseScriptInput

export interface TrackedPage {
title?: string
path: string
}

export type NuxtUseTrackingScriptOptions<T = any> = Omit<NuxtUseScriptOptions<T>, 'trigger'> & {
consent: Promise<boolean | void> | Ref<boolean> | boolean
ignoreDoNotTrack?: boolean
Expand Down

0 comments on commit 0367917

Please sign in to comment.