Storyblok's component-based architecture and Visual Editor require custom event tracking implementation. This guide shows how to track Storyblok-specific interactions with GA4.
Storyblok Event Categories
Component Events
- Component View - Individual component rendered on page
- Component Interaction - User interacts with component
- Component CTA Click - Call-to-action button clicked
- Component Form Submit - Form within component submitted
Visual Editor Events
- Editor Accessed - User enters Visual Editor mode
- Editor Story Viewed - Specific story viewed in editor
- Editor Preview - Preview button clicked in editor
Story Events
- Story View - Storyblok story rendered
- Link Click - Internal story link clicked
- External Link Click - External link clicked
Event Tracking Implementation
Method 1: Tracking Component Views (Nuxt 3)
Create a composable for tracking:
// composables/useComponentTracking.ts
export const useComponentTracking = (blok: any) => {
const hasTracked = ref(false);
const elementRef = ref<HTMLElement | null>(null);
onMounted(() => {
if (!elementRef.value || hasTracked.value) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !hasTracked.value) {
hasTracked.value = true;
if (typeof window.gtag !== 'undefined') {
window.gtag('event', 'view_item', {
event_category: 'component',
event_label: blok.component,
component_type: blok.component,
component_uid: blok._uid,
});
}
}
});
},
{ threshold: 0.5 }
);
observer.observe(elementRef.value);
onUnmounted(() => observer.disconnect());
});
return { elementRef };
};
Usage in component:
<!-- components/HeroSection.vue -->
<template>
<div ref="elementRef" v-editable="blok" class="hero">
<h1>{{ blok.title }}</h1>
<button @click="handleCTAClick">{{ blok.cta_text }}</button>
</div>
</template>
<script setup>
const { blok } = defineProps(['blok']);
const { elementRef } = useComponentTracking(blok);
const handleCTAClick = () => {
if (typeof window.gtag !== 'undefined') {
window.gtag('event', 'click', {
event_category: 'component_cta',
event_label: 'hero_cta',
component_type: blok.component,
cta_text: blok.cta_text,
});
}
};
</script>
Method 2: Tracking Visual Editor Usage (Nuxt 3)
// plugins/storyblok-editor-tracking.client.ts
export default defineNuxtPlugin(() => {
const route = useRoute();
// Detect Visual Editor
const isEditor = computed(() => route.query._storyblok !== undefined);
watch(isEditor, (inEditor) => {
if (inEditor && typeof window.gtag !== 'undefined') {
window.gtag('set', 'user_properties', {
editor_mode: 'active',
});
window.gtag('event', 'visual_editor_accessed', {
event_category: 'editor',
event_label: 'Storyblok Visual Editor Active',
});
}
}, { immediate: true });
// Track editor story changes
if (window.storyblok) {
window.storyblok.on(['input'], (event) => {
if (typeof window.gtag !== 'undefined') {
window.gtag('event', 'editor_content_changed', {
event_category: 'editor',
event_label: 'Content Modified in Editor',
story_id: event.story.id,
});
}
});
}
});
Method 3: Tracking Story Views
<!-- pages/[...slug].vue (Nuxt 3) -->
<script setup>
const { slug } = useRoute().params;
const story = await useAsyncStoryblok(slug.join('/'), { version: 'draft' });
onMounted(() => {
if (story.value && typeof window.gtag !== 'undefined') {
window.gtag('event', 'view_story', {
event_category: 'storyblok_content',
event_label: story.value.content.component,
story_id: story.value.id,
story_name: story.value.name,
story_slug: story.value.slug,
created_at: story.value.created_at,
published_at: story.value.published_at,
});
}
});
</script>
Method 4: Tracking Link Clicks (Next.js)
// components/StoryblokLink.js
'use client';
import Link from 'next/link';
import { storyblokEditable } from '@storyblok/react';
export default function StoryblokLink({ blok }) {
const handleClick = () => {
const isExternal = blok.link.linktype === 'url';
if (typeof window.gtag !== 'undefined') {
window.gtag('event', 'click', {
event_category: isExternal ? 'outbound_link' : 'internal_link',
event_label: blok.link.cached_url || blok.link.url,
link_type: blok.link.linktype,
component_type: blok.component,
});
}
};
return (
<Link
href={blok.link.cached_url || blok.link.url}
{...storyblokEditable(blok)}
>
{blok.text}
</Link>
);
}
Method 5: Tracking Form Submissions
<!-- components/ContactForm.vue (Nuxt 3) -->
<template>
<form @submit.prevent="handleSubmit" v-editable="blok">
<input v-model="email" type="email" required />
<button type="submit">Submit</button>
</form>
</template>
<script setup>
const { blok } = defineProps(['blok']);
const email = ref('');
const handleSubmit = async () => {
if (typeof window.gtag !== 'undefined') {
window.gtag('event', 'generate_lead', {
event_category: 'form',
event_label: 'contact_form_submit',
component_type: blok.component,
form_name: blok.form_name,
});
}
// Submit form logic
};
</script>
Recommended GA4 Custom Events
Create in GA4 Console
Navigate to GA4 → Configure → Events → Create Event:
component_interaction
- Event name:
click - Match condition:
event_category = component_cta
- Event name:
editor_usage
- Event name:
visual_editor_accessed - Parameter:
event_category = editor
- Event name:
story_engagement
- Event name:
view_story - Parameter:
story_idexists
- Event name:
Custom Dimensions
Set up in GA4 → Configure → Custom Definitions:
| Dimension Name | Parameter | Scope |
|---|---|---|
| Component Type | component_type | Event |
| Component UID | component_uid | Event |
| Story ID | story_id | Event |
| Story Name | story_name | Event |
| Editor Mode | editor_mode | User |
Advanced Tracking Patterns
Track Content Journey
// composables/useStoryblokJourney.ts
export const useStoryblokJourney = () => {
const journey = ref<Array<any>>([]);
const trackStoryView = (story: any) => {
journey.value.push({
id: story.id,
name: story.name,
component: story.content.component,
timestamp: Date.now(),
});
if (typeof window.gtag !== 'undefined') {
window.gtag('event', 'content_journey', {
event_category: 'engagement',
event_label: 'Story Path',
journey_depth: journey.value.length,
story_sequence: journey.value.map(s => s.name).join(' > '),
});
}
};
return { trackStoryView };
};
Performance Considerations
Throttle Component Tracking
const useThrottledTracking = (callback: Function, delay = 1000) => {
const timeoutRef = ref<number | null>(null);
const lastCallRef = ref(0);
return (...args: any[]) => {
const now = Date.now();
if (now - lastCallRef.value < delay) return;
if (timeoutRef.value) clearTimeout(timeoutRef.value);
timeoutRef.value = window.setTimeout(() => {
callback(...args);
lastCallRef.value = now;
}, 100);
};
};
Testing Event Tracking
1. DebugView
Enable debug mode:
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
});
Check GA4 → Admin → DebugView for real-time events.
2. Browser Console
// Test event manually
gtag('event', 'test_component_view', {
component_type: 'hero',
test: true,
});
// Verify dataLayer
console.log(window.dataLayer);