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-analyticsJSON - 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:
- Server renders page with metadata
- Chartbeat config inline in HTML
- Client receives fully-rendered HTML
- Chartbeat script loads and reads config
- 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:
- Building simple website or blog
- Using static site generator (Gatsby, Hugo)
- No server-side framework
- Minimal technical complexity desired
- Quick implementation needed
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:
- Server renders page with config
- Client receives HTML with inline config
- Chartbeat script loads client-side
- Script reads config object
- Initial ping sent to Chartbeat
- Heartbeat pings continue every 15 seconds
- User engagement tracked
- 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
- Server Generates, Client Tracks: Use server for config, client for tracking
- Validate SSR Output: Always check HTML source to verify server-rendered config
- Implement Guards: Prevent duplicate initialization with proper checks
- Optimize Performance: Inline config in HTML for faster tracking start
- Test Both Paths: Verify server rendering AND client tracking
- Handle Edge Cases: Plan for missing data, preview modes, dev environments
- Monitor Consistently: Check Real-Time dashboard regularly for anomalies
- 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