Prismic Analytics: Slice-Based Tracking | OpsBlu Docs

Prismic Analytics: Slice-Based Tracking

Implement analytics on Prismic-powered sites. Covers slice-level component tracking, route resolver page view events, SliceMachine integration, and...

Analytics Architecture on Prismic

Prismic is a headless CMS that delivers content through REST and GraphQL APIs. Analytics tracking lives entirely in the frontend framework (Next.js, Nuxt, SvelteKit, etc.), not in Prismic itself. The CMS provides structured content through its Slice-based system, which maps directly to frontend components.

Key architecture points for analytics:

  • Prismic Client fetches content via API. The response includes document metadata (type, tags, timestamps, locale) that feeds data layers
  • SliceMachine generates typed components for each Slice. Analytics hooks attach at the Slice component level for granular content tracking
  • Route Resolver maps Prismic documents to frontend URLs. Route changes trigger virtual page view events in SPAs
  • Preview mode uses a preview session cookie. Filter preview traffic from production analytics to avoid skewed data
  • Localization is built into the document model. Each locale variant is a separate document with its own UID, requiring locale-aware tracking

Since Prismic sites are SPAs, traditional page-load-based tracking does not work. Every route transition requires an explicit analytics event.


Installing Scripts in Your Frontend

Next.js with Prismic (App Router)

Prismic's official Next.js integration (@prismicio/next) works with the App Router. Add tracking scripts in the root layout:

// app/layout.tsx
import Script from 'next/script';
import { PrismicPreview } from '@prismicio/next';
import { repositoryName } from '@/prismicio';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <Script id="gtm" strategy="afterInteractive">
          {`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
          new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
          j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
          'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
          })(window,document,'script','dataLayer','GTM-XXXXXX');`}
        </Script>
      </head>
      <body>
        {children}
        <PrismicPreview repositoryName={repositoryName} />
      </body>
    </html>
  );
}

Nuxt 3 with Prismic

<!-- nuxt.config.ts -->
<script setup>
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/prismic'],
  prismic: { endpoint: 'your-repo-name' },
  app: {
    head: {
      script: [
        { src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXX', async: true },
        { innerHTML: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-XXXXXX');` }
      ]
    }
  }
});
</script>

Data Layer from Prismic Documents

Build data layer pushes from the Prismic document response. The client returns structured metadata for every document:

// components/PrismicAnalytics.tsx
'use client';
import { useEffect } from 'react';
import { PrismicDocument } from '@prismicio/client';

interface Props {
  document: PrismicDocument;
}

export function PrismicAnalytics({ document }: Props) {
  useEffect(() => {
    if (!document) return;

    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'prismic_page_view',
      content_type: document.type,
      content_id: document.id,
      content_uid: document.uid,
      content_tags: document.tags.join(','),
      content_lang: document.lang,
      first_published: document.first_publication_date,
      last_published: document.last_publication_date,
      alternate_languages: document.alternate_languages
        ?.map(alt => alt.lang).join(',') || ''
    });
  }, [document.id]);

  return null;
}

Use it in page components:

// app/[uid]/page.tsx
import { createClient } from '@/prismicio';
import { PrismicAnalytics } from '@/components/PrismicAnalytics';

export default async function Page({ params }: { params: { uid: string } }) {
  const client = createClient();
  const page = await client.getByUID('page', params.uid);

  return (
    <>
      <PrismicAnalytics document={page} />
      {/* page content */}
    </>
  );
}

Slice-Level Component Tracking

Prismic's Slice system lets you track engagement at the component level. Each Slice renders as a distinct section, and you can attach intersection observers for visibility tracking:

// slices/CallToAction/index.tsx
'use client';
import { useRef, useEffect } from 'react';
import { SliceComponentProps } from '@prismicio/react';
import { Content } from '@prismicio/client';

type Props = SliceComponentProps<Content.CallToActionSlice>;

export default function CallToAction({ slice }: Props) {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    if (!ref.current) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          window.dataLayer?.push({
            event: 'slice_view',
            slice_type: slice.slice_type,
            slice_label: slice.slice_label || 'default',
            slice_variation: slice.variation
          });
          observer.disconnect();
        }
      },
      { threshold: 0.5 }
    );

    observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return (
    <section ref={ref} data-slice-type={slice.slice_type}>
      {/* slice content */}
    </section>
  );
}

This fires a slice_view event when a CTA Slice becomes 50% visible, giving you engagement data at the content block level.


SPA Route Change Tracking

Since Prismic sites use client-side routing, track virtual page views on route transitions:

// components/RouteAnalytics.tsx
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';

export function RouteAnalytics() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const isFirstRender = useRef(true);

  useEffect(() => {
    // Skip initial render (handled by server-side page view)
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }

    window.dataLayer?.push({
      event: 'virtual_page_view',
      page_path: pathname,
      page_search: searchParams.toString()
    });
  }, [pathname, searchParams]);

  return null;
}

Add this component to your root layout alongside the GTM script.


Preview Mode Filtering

Prismic's preview mode injects a session cookie (io.prismic.preview) and loads draft content. Filter preview sessions from analytics to keep production data clean:

// components/PrismicAnalytics.tsx (updated)
'use client';
import { useEffect } from 'react';

export function PrismicAnalytics({ document }) {
  useEffect(() => {
    // Skip tracking in preview mode
    if (document.cookie?.includes('io.prismic.preview')) return;
    if (typeof window !== 'undefined' && document.referrer?.includes('prismic.io')) return;

    window.dataLayer?.push({
      event: 'prismic_page_view',
      // ...document metadata
    });
  }, [document.id]);

  return null;
}

Alternatively, set a GTM variable that checks for the preview cookie and use it as a blocking trigger on all tags.


Common Errors

Error Cause Fix
Duplicate page views on navigation Both route change listener and page component fire events Skip first render in route listener with a ref flag
Document data undefined in analytics Async fetch not resolved before component mounts Push data layer in useEffect with document dependency
Preview traffic in production analytics Preview cookie not filtered Check for io.prismic.preview cookie before pushing events
Slice tracking fires for every re-render IntersectionObserver not disconnected Call observer.disconnect() after first intersection
Wrong language tracked Using document.lang returns Prismic locale code (en-us) Map Prismic locale codes to your analytics language values
Tags array causes GTM errors GTM expects string, not array Join tags with .join(',') before pushing to data layer
Stale data after content update ISR/SSG serving cached page with old metadata Set appropriate revalidate interval or use on-demand ISR
Missing alternate language data Document has no translations configured Check alternate_languages.length > 0 before accessing

Performance Considerations

  • Prismic Client caching: The @prismicio/client supports built-in caching. Use fetchOptions: { next: { revalidate: 60 } } in Next.js to avoid redundant API calls
  • Static generation: Use generateStaticParams to pre-render Prismic pages at build time, reducing API calls and improving TTFB
  • Script loading: Use strategy="afterInteractive" on Next.js Script components to avoid blocking the initial paint
  • Slice lazy loading: Use React.lazy() or dynamic imports for heavy Slice components below the fold
  • API response size: Use Prismic's graphQuery parameter to fetch only the fields you need, reducing payload size