Chartbeat Server-Side vs Client-Side | OpsBlu Docs

Chartbeat Server-Side vs Client-Side

Server-side vs client-side tracking approaches for Chartbeat. Covers implementation trade-offs, data accuracy, privacy compliance, ad blocker resilience.

Overview

Chartbeat primarily operates as a client-side analytics solution, but understanding when and how to leverage server-side rendering (SSR) for configuration or metadata generation is important for modern web applications. This guide clarifies the architectural decisions, trade-offs, and implementation patterns for both approaches.

Fundamental Architecture

Chartbeat's Design: Client-side JavaScript tracker

  • Runs in the user's browser
  • Sends heartbeat pings to Chartbeat servers
  • Monitors engagement signals (mouse, scroll, focus)
  • Tracks real-time user behavior

Why Client-Side?:

  • Engagement tracking requires browser-level signals
  • Real-time heartbeat pings need active client connection
  • User interactions (scrolling, clicking) are client-side events
  • Browser-specific data (window size, scroll position) only available client-side

The Server-Side Role

While Chartbeat tracking is client-side, the server plays important roles:

  • Generate tracking configuration based on content metadata
  • Render meta tags for SEO and canonical URLs
  • Pre-populate data layer with CMS data
  • Handle server-side rendering (SSR) frameworks like Next.js, Nuxt

Server does NOT:

  • Send tracking pings to Chartbeat (client does this)
  • Monitor user engagement (client does this)
  • Replace client-side tracking script

When to Use Client-Side Collection

Standard Client-Side Implementation

This is the default and recommended approach for most websites.

Use Client-Side When:

  • Running traditional multi-page applications (MPAs)
  • Using client-side rendering (CSR) frameworks
  • Content is static HTML or simple CMS
  • No complex server-side logic needed

Implementation:

<!DOCTYPE html>
<html>
<head>
  <title>Article Title</title>
</head>
<body>
  <!-- Page content -->

  <!-- Chartbeat Client-Side Tracking -->
  <script type="text/javascript">
    var _sf_async_config = {
      uid: 12345,
      domain: 'example.com',
      sections: 'News,Politics',
      authors: 'Jane Smith',
      title: 'Article Headline',
      path: '/news/politics/article'
    };

    (function() {
      var e = document.createElement('script');
      e.src = '//static.chartbeat.com/js/chartbeat.js';
      e.async = true;
      document.body.appendChild(e);
    })();
  </script>
</body>
</html>

Advantages:

  • Simple implementation
  • No server-side dependencies
  • Works with any hosting provider
  • Easy to debug in browser DevTools
  • Supports all Chartbeat features (engagement, video, etc.)

Disadvantages:

  • Script load time adds to page load
  • Blocked by ad blockers (though rare for Chartbeat)
  • No tracking if JavaScript disabled (rare but possible)

Single-Page Applications (SPAs)

SPAs require client-side tracking with virtual page views.

Client-Side SPA Pattern:

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

function App() {
  const location = useLocation();

  useEffect(() => {
    // Initialize Chartbeat on mount
    window._sf_async_config = {
      uid: 12345,
      domain: 'example.com',
      useCanonical: true
    };

    const script = document.createElement('script');
    script.src = '//static.chartbeat.com/js/chartbeat.js';
    script.async = true;
    document.body.appendChild(script);
  }, []);

  useEffect(() => {
    // Trigger virtual page view on route change
    if (typeof window.pSUPERFLY !== 'undefined') {
      const pageData = getPageMetadata(location.pathname);

      window.pSUPERFLY.virtualPage({
        sections: pageData.sections,
        authors: pageData.authors,
        title: pageData.title,
        path: location.pathname
      });
    }
  }, [location]);

  return <Routes>{/* routes */}</Routes>;
}

Why Client-Side for SPAs:

  • Virtual page view API (pSUPERFLY.virtualPage()) requires client script
  • Engagement timer needs to persist across route changes
  • Heartbeat pings must continue during navigation
  • User interactions tracked in real-time

When to Use Server-Side Collection

Server-Side Configuration Generation

The server generates the Chartbeat configuration, but tracking still happens client-side.

Use Server-Generated Config When:

  • Using server-side rendering (SSR) frameworks
  • Content metadata comes from database/CMS
  • Need to populate config before page hydration
  • SEO and performance are priorities

Next.js Example (SSR):

// pages/article/[slug].js
export async function getServerSideProps({ params }) {
  const article = await fetchArticle(params.slug);

  return {
    props: {
      article,
      chartbeatConfig: {
        uid: 12345,
        domain: 'example.com',
        sections: article.categories.join(','),
        authors: article.authors.map(a => a.name).join(','),
        title: article.title,
        path: `/article/${params.slug}`
      }
    }
  };
}

export default function Article({ article, chartbeatConfig }) {
  useEffect(() => {
    // Set config from server-rendered props
    window._sf_async_config = chartbeatConfig;

    // Load Chartbeat script (client-side)
    const script = document.createElement('script');
    script.src = '//static.chartbeat.com/js/chartbeat.js';
    script.async = true;
    document.body.appendChild(script);
  }, [chartbeatConfig]);

  return <div>{/* article content */}</div>;
}

Advantages:

  • Configuration ready before client hydration
  • Faster initial page load (config inline)
  • Accurate metadata from authoritative source (database)
  • SEO benefits from server rendering

Server-Side HTML Generation

Pre-render Chartbeat configuration in HTML.

Example: Server Template (Node.js/Express):

// server.js
app.get('/article/:slug', async (req, res) => {
  const article = await getArticle(req.params.slug);

  const chartbeatConfig = {
    uid: 12345,
    domain: 'example.com',
    sections: article.sections.join(','),
    authors: article.authors.join(','),
    title: article.title,
    path: req.path
  };

  res.render('article', {
    article,
    chartbeatConfig: JSON.stringify(chartbeatConfig)
  });
});
<!-- views/article.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= article.title %></title>
  <link rel="canonical" href="https://example.com<%= article.path %>">
</head>
<body>
  <!-- Article content -->

  <!-- Server-rendered Chartbeat config -->
  <script type="text/javascript">
    var _sf_async_config = <%- chartbeatConfig %>;
  </script>

  <!-- Client-side Chartbeat script -->
  <script type="text/javascript">
    (function() {
      var e = document.createElement('script');
      e.src = '//static.chartbeat.com/js/chartbeat.js';
      e.async = true;
      document.body.appendChild(e);
    })();
  </script>
</body>
</html>

AMP (Accelerated Mobile Pages)

AMP is a special case where configuration is server-rendered but tracking is client-side via amp-analytics.

AMP Implementation:

<!doctype html>
<html amp>
<head>
  <script async src="https://cdn.ampproject.org/v0.js"></script>
  <script async custom-element="amp-analytics"
    src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
  <link rel="canonical" href="https://example.com/article/story">
</head>
<body>
  <!-- AMP content -->

  <!-- Chartbeat AMP Analytics (server-rendered config) -->
  <amp-analytics type="chartbeat">
    <script type="application/json">
    {
      "vars": {
        "uid": "12345",
        "domain": "example.com",
        "sections": "<% article.sections %>",
        "authors": "<% article.authors %>",
        "title": "<% article.title %>",
        "path": "<% article.path %>"
      }
    }
    </script>
  </amp-analytics>
</body>
</html>

Why Server-Rendered for AMP:

  • AMP restricts custom JavaScript
  • Configuration must be in amp-analytics JSON
  • Server templates populate metadata
  • Tracking still happens client-side via AMP runtime

Hybrid Approaches

SSR with Client-Side Tracking

Most modern frameworks use hybrid approach: server renders initial HTML, client hydrates and tracks.

Next.js Hybrid Pattern:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);

    // Server-side: Get page metadata
    const { page } = ctx.query;
    const metadata = await getPageMetadata(page);

    return { ...initialProps, metadata };
  }

  render() {
    const { metadata } = this.props;

    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />

          {/* Server-rendered config */}
          <script
            dangerouslySetInnerHTML={{
              __html: `
                var _sf_async_config = {
                  uid: 12345,
                  domain: 'example.com',
                  sections: '${metadata.sections || ''}',
                  authors: '${metadata.authors || ''}',
                  title: '${metadata.title || ''}',
                  path: '${metadata.path || ''}'
                };
              `
            }}
          />

          {/* Client-side script loads after config */}
          <script async src="//static.chartbeat.com/js/chartbeat.js" />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

Flow:

  1. Server renders page with metadata
  2. Chartbeat config inline in HTML
  3. Client receives fully-rendered HTML
  4. Chartbeat script loads and reads config
  5. Tracking begins client-side

WordPress (Server-Generated Config)

WordPress generates config server-side via PHP:

<!-- WordPress template (footer.php) -->
<?php
// Server-side: Get post metadata
$categories = get_the_category();
$sections = array_map(function($cat) { return $cat->name; }, $categories);
$chartbeat_sections = implode(',', $sections);
$chartbeat_author = get_the_author();
$chartbeat_title = get_the_title();
$chartbeat_path = parse_url(get_permalink(), PHP_URL_PATH);
?>

<!-- Chartbeat Config (server-rendered) -->
<script type="text/javascript">
  var _sf_async_config = {
    uid: <?php echo get_option('chartbeat_uid', 12345); ?>,
    domain: '<?php echo $_SERVER['HTTP_HOST']; ?>',
    sections: '<?php echo esc_js($chartbeat_sections); ?>',
    authors: '<?php echo esc_js($chartbeat_author); ?>',
    title: '<?php echo esc_js($chartbeat_title); ?>',
    path: '<?php echo esc_js($chartbeat_path); ?>',
    useCanonical: true
  };
</script>

<!-- Chartbeat Script (client-side) -->
<script type="text/javascript" async
  src="//static.chartbeat.com/js/chartbeat.js"></script>

Coordination & Deduplication

Preventing Double Tracking

If both server and client might initialize Chartbeat, implement guards:

Guard Pattern:

// Prevent duplicate initialization
(function() {
  // Check if already initialized
  if (window.chartbeatInitialized) {
    console.warn('Chartbeat already initialized, skipping');
    return;
  }

  // Mark as initialized
  window.chartbeatInitialized = true;

  // Set config (if not already set by server)
  if (typeof window._sf_async_config === 'undefined') {
    window._sf_async_config = {
      uid: 12345,
      domain: 'example.com',
      // ... other config
    };
  }

  // Load script
  var e = document.createElement('script');
  e.src = '//static.chartbeat.com/js/chartbeat.js';
  e.async = true;
  document.body.appendChild(e);
})();

Conditional Loading

Load Chartbeat only when appropriate:

// Only load on production
if (process.env.NODE_ENV === 'production') {
  loadChartbeat();
}

// Only load if consent granted
if (userConsentGranted) {
  loadChartbeat();
}

// Don't load in preview mode
if (!window.location.search.includes('preview=true')) {
  loadChartbeat();
}

Tag Manager Coordination

If using both server-rendered config and tag manager:

Server Provides Config:

<script>
  // Server sets config
  var _sf_async_config = {
    uid: 12345,
    domain: 'example.com',
    sections: 'News',
    authors: 'Jane Smith'
  };
</script>

Tag Manager Loads Script:

// GTM tag: Custom HTML
// Only load script, don't set config (server did that)
(function() {
  if (typeof window._sf_async_config !== 'undefined') {
    var e = document.createElement('script');
    e.src = '//static.chartbeat.com/js/chartbeat.js';
    e.async = true;
    document.body.appendChild(e);
  }
})();

Comparison Table

Aspect Client-Side Server-Side Config Hybrid
Tracking Location Client (browser) Client (browser) Client (browser)
Config Generation Client Server Server
Script Loading Client Client Client
Best For Simple sites, SPAs SSR frameworks, CMSs Modern web apps
Performance Good Better (config ready) Best (optimized)
Complexity Low Medium Medium-High
SEO Good Excellent Excellent
Engagement Tracking Full support Full support Full support
Video Tracking Supported Supported Supported

Decision Framework

Choose Client-Side When:

Choose Server-Generated Config When:

  • Using SSR framework (Next.js, Nuxt, SvelteKit)
  • Running WordPress, Drupal, or enterprise CMS
  • Content metadata in database
  • SEO and performance are priorities
  • Need accurate metadata from authoritative source

Choose Hybrid When:

  • Building complex modern web application
  • Using multiple deployment environments
  • Need maximum flexibility and control
  • Want optimal performance and SEO
  • Have technical resources for implementation

Validation & Testing

Verify Server-Side Config

Check Config in HTML Source:

# View page source (Ctrl+U)
# Search for "_sf_async_config"
# Verify config object populated with correct values

# Example:
var _sf_async_config = {
  uid: 12345,
  domain: "example.com",
  sections: "News,Politics",
  authors: "Jane Smith",
  title: "Article Title",
  path: "/news/politics/article"
};

Test SSR Rendering:

// Disable JavaScript in browser
// Load page
// View source - config should still be present
// This proves server rendered the config

Verify Client-Side Tracking

Check Chartbeat Loaded:

// Open DevTools console
console.log(typeof window.pSUPERFLY !== 'undefined' ? 'Loaded' : 'Not loaded');

Check Heartbeat Pings:

# Open DevTools Network tab
# Filter: chartbeat.net
# Verify pings every 15 seconds
# Check ping contains metadata from config

End-to-End Testing

Full Flow Test:

  1. Server renders page with config
  2. Client receives HTML with inline config
  3. Chartbeat script loads client-side
  4. Script reads config object
  5. Initial ping sent to Chartbeat
  6. Heartbeat pings continue every 15 seconds
  7. User engagement tracked
  8. Data appears in Real-Time dashboard

Troubleshooting

Issue Symptoms Diagnosis Resolution
Config not set _sf_async_config undefined Server not rendering config Check server template, verify data passed to view
Duplicate tracking Multiple pings per heartbeat Both server and client loading script Implement initialization guard
Missing metadata Config object empty Server data not available Verify database query, check data pipeline
Script not loading No network requests to chartbeat Client script loader broken Check script tag, verify CDN accessible
SSR hydration mismatch React hydration errors Server and client configs differ Ensure config consistent between server and client
Config overwritten Metadata changes after load Client overwriting server config Merge configs instead of replacing
AMP not working No tracking on AMP pages AMP analytics misconfigured Verify amp-analytics type="chartbeat"
Preview mode tracking Test data in production reports No environment check Add conditional loading for preview/dev modes

Best Practices

  1. Server Generates, Client Tracks: Use server for config, client for tracking
  2. Validate SSR Output: Always check HTML source to verify server-rendered config
  3. Implement Guards: Prevent duplicate initialization with proper checks
  4. Optimize Performance: Inline config in HTML for faster tracking start
  5. Test Both Paths: Verify server rendering AND client tracking
  6. Handle Edge Cases: Plan for missing data, preview modes, dev environments
  7. Monitor Consistently: Check Real-Time dashboard regularly for anomalies
  8. Document Architecture: Clearly document server vs. client responsibilities

Summary

Key Takeaway: Chartbeat tracking is always client-side, but configuration can be generated server-side for better performance and accuracy.

Recommended Pattern:

  • Server: Generate configuration from CMS/database
  • Client: Load Chartbeat script, send tracking pings
  • Result: Best of both worlds - accurate metadata + real-time engagement tracking