Analytics Implementation on ButterCMS | OpsBlu Docs

Analytics Implementation on ButterCMS

Tracking script installation, data layer patterns, and event handling for ButterCMS using the REST API, Pages, Collections, and frontend SDKs.

Analytics Architecture on ButterCMS

ButterCMS is an API-first headless CMS with three primary content types: Pages, Blog Posts, and Collections. Content is delivered through a REST API and consumed by frontend frameworks via language-specific SDKs. ButterCMS renders no HTML -- all analytics implementation happens in the frontend.

Content delivery flow:

ButterCMS REST API
    |-- Pages (landing pages, custom page types)
    |-- Blog Posts (author, category, tag)
    |-- Collections (reusable structured data)
    |
    v
Frontend SDK (JavaScript, React, Vue, Angular, Python, Ruby, PHP, etc.)
    |
    v
HTML Output (where tracking scripts execute)

Key components for analytics:

  • REST API -- Returns JSON with content fields, SEO metadata, dates, and author/category relationships. All fields are available for data layer population.
  • SDKs -- Language-specific wrappers (buttercms npm package for JS). Simplify content fetching with typed responses.
  • Webhooks -- Triggered on content create, update, and delete. Used for server-side event tracking and cache invalidation.
  • Page Types -- Custom page schemas with defined fields. You can include tracking-specific fields (campaign IDs, UTM parameters) directly in page type definitions.
  • SEO fields -- Built-in meta_title, meta_description, og_image fields on blog posts. These are accessible in the API response for data layer enrichment.

Installing Tracking Scripts

ButterCMS does not inject any scripts. Your frontend framework handles all tracking code.

React SPA

// public/index.html or index.html (Vite)
<head>
  <script>
    (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-XXXXXXX');
  </script>
</head>

Next.js

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <Script
          id="gtm"
          strategy="afterInteractive"
          dangerouslySetInnerHTML={{
            __html: `(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-XXXXXXX');`
          }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Vue / Nuxt

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          children: `(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-XXXXXXX');`,
        }
      ]
    }
  }
});

Angular

// angular.json -- add to "scripts" array or inject via index.html
// src/index.html
<head>
  <script>
    (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-XXXXXXX');
  </script>
</head>

Data Layer Implementation

Pushing Blog Post Metadata

The ButterCMS blog API returns posts with rich metadata. Fetch and push to the data layer:

// app/blog/[slug]/page.tsx
import Butter from 'buttercms';

const butter = Butter(process.env.BUTTERCMS_API_TOKEN);

export default async function BlogPost({ params }) {
  const response = await butter.post.retrieve(params.slug);
  const post = response.data.data;

  return (
    <>
      <script
        dangerouslySetInnerHTML={{
          __html: `window.dataLayer = window.dataLayer || [];
          window.dataLayer.push({
            event: 'page_data_ready',
            content_type: 'blog_post',
            content_id: '${post.slug}',
            content_title: '${post.title.replace(/'/g, "\\'")}',
            content_author: '${post.author.first_name} ${post.author.last_name}',
            content_category: '${post.categories?.[0]?.name || "uncategorized"}',
            content_tags: '${post.tags?.map(t => t.name).join(",") || ""}',
            content_published: '${post.published}',
            content_updated: '${post.updated}',
            content_status: '${post.status}'
          });`
        }}
      />
      {/* render post */}
    </>
  );
}

Pushing Page Type Metadata

ButterCMS Pages use custom page types with defined fields:

const response = await butter.page.retrieve('landing_page', 'spring-sale');
const page = response.data.data;

window.dataLayer.push({
  event: 'page_data_ready',
  content_type: 'landing_page',
  page_slug: page.slug,
  page_name: page.name,
  campaign_id: page.fields.campaign_id,       // custom field
  utm_source: page.fields.utm_source,          // custom field
  content_updated: page.updated
});

Using Collections for Tracking Configuration

Collections are reusable structured data. Store analytics configuration as a Collection:

// Fetch tracking config from ButterCMS Collection
const response = await butter.content.retrieve(['analytics_config']);
const config = response.data.data.analytics_config[0];

// config.ga_measurement_id, config.gtm_container_id, etc.
// Use these values to dynamically configure tracking scripts

Server-Side Events via Webhooks

Configure webhooks in ButterCMS Settings > Webhooks:

URL: https://yoursite.com/api/butter-webhook
Events: post.published, post.updated, page.published

Webhook handler:

// app/api/butter-webhook/route.ts
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const body = await req.json();
  const { webhook_event, data } = body;

  await fetch(
    `https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXX&api_secret=SECRET`,
    {
      method: 'POST',
      body: JSON.stringify({
        client_id: 'buttercms',
        events: [{
          name: 'content_event',
          params: {
            action: webhook_event,
            content_type: data.type || 'blog_post',
            content_slug: data.slug,
            content_title: data.title
          }
        }]
      })
    }
  );

  return NextResponse.json({ ok: true });
}

Common Issues

Blog post status field confusion

ButterCMS blog posts have status: 'published' or status: 'draft'. The API returns drafts only when using the preview=1 parameter. If you accidentally include preview=1 in production API calls, draft posts will appear on the live site and their views will be tracked.

// Production: never include preview parameter
const response = await butter.post.list({ page: 1, page_size: 10 });

// Preview only: for editorial review
const response = await butter.post.list({ page: 1, page_size: 10, preview: 1 });

Client-side routing in SPAs

ButterCMS is commonly used with React SPAs. React Router navigation does not trigger full page loads. Implement route change tracking:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function RouteTracker() {
  const location = useLocation();
  useEffect(() => {
    window.dataLayer?.push({
      event: 'page_view',
      page_path: location.pathname
    });
  }, [location.pathname]);
  return null;
}

API token exposed in client-side code

The ButterCMS API token is a read-only key, but it is visible in network requests if you fetch content client-side. This is expected behavior -- the token only grants read access. However, if you track API usage analytics, be aware that exposed tokens can be used by third parties, inflating your API call counts.

For server-rendered frameworks (Next.js SSR, Nuxt SSR), fetch content server-side to keep the token out of the browser.

Paginated content missing from analytics

ButterCMS paginates blog post lists (default 10 per page). If your data layer only captures metadata from the first page of results, posts on subsequent pages will not have tracking data until a user navigates directly to them.

SEO fields not populating data layer

Blog posts include seo_title and meta_description in the API response, but only if they were explicitly set in the ButterCMS editor. If not set, these fields return null. Check for null values before pushing to the data layer.


Platform-Specific Considerations

API rate limits -- ButterCMS does not publicly document hard rate limits, but the API is throttled at high volumes. If you build analytics dashboards that poll the ButterCMS API, cache responses and use webhooks for change detection instead of polling.

Multi-language content -- ButterCMS supports localization through separate API endpoints per locale. Each locale returns its own set of content. Include the active locale in your data layer push:

window.dataLayer.push({
  content_locale: currentLocale,
  content_title: localizedPost.title
});

Write API -- ButterCMS provides a Write API for programmatic content creation. If you use this to auto-generate content (e.g., from analytics data), the created content will trigger webhooks like manually created content.

Image CDN -- ButterCMS serves images through cdn.buttercms.com. Images support URL parameter transforms for resizing and format conversion. Different transform parameters create different CDN URLs, which can inflate unique asset counts in network analytics.

No preview URL verification -- Unlike some headless CMSs, ButterCMS does not verify preview URLs with a secret token. If you implement a preview mode that disables analytics, ensure the preview flag cannot be set by arbitrary users visiting your site.