Skip to content

Content Engagement

Content engagement events measure what users do with your content beyond the page view. Did they read the article or just scroll past? Did they watch the video or click play and immediately stop? Did they share or save the piece? These events transform your content analytics from “how many people visited” to “how many people engaged.”

Fire when a user has meaningfully engaged with an article — not just scrolled past it. The most defensible trigger is scroll depth, typically 75–90% of the article body height.

// User has scrolled to 90% of the article
dataLayer.push({
event: 'content_article_read',
content_type: 'article',
content_id: 'article-how-to-choose-a-leather-jacket',
content_title: 'How to Choose the Perfect Leather Jacket',
content_author: 'Alex Morgan',
content_category: 'Style Guide',
content_word_count: 1850,
content_publish_date: '2024-02-15',
reading_depth_percent: 90
});
Event Schema content_article_read
Parameter Type Required Description
event string Required "content_article_read" or "article_read" depending on your namespace convention.
content_type string Required Type of content: article, guide, review, news.
content_id string Required Unique identifier for the content piece — URL slug or CMS ID.
content_title string Required The article title.
content_author string Optional Author name.
content_category string Optional Primary content category.
content_word_count number Optional Word count of the article — helps normalize engagement by length.
content_publish_date string Optional ISO 8601 publish date: 2024-02-15.
reading_depth_percent number Optional Scroll depth percentage at which the event fires. Typically 75 or 90.

For custom video players (not YouTube or Vimeo with GA4’s Enhanced Measurement enabled), implement video tracking manually.

// Video play started
dataLayer.push({
event: 'content_video_play',
content_type: 'video',
content_id: 'video-summer-lookbook-2024',
content_title: 'Summer Lookbook 2024',
video_duration: 187, // total duration in seconds
video_current_time: 0, // position when play was clicked
video_provider: 'self_hosted'
});
// Video progress milestone
dataLayer.push({
event: 'content_video_progress',
content_id: 'video-summer-lookbook-2024',
video_percent: 50, // 25, 50, 75, 100 are standard milestones
video_current_time: 93
});
// Video completed
dataLayer.push({
event: 'content_video_complete',
content_id: 'video-summer-lookbook-2024',
content_title: 'Summer Lookbook 2024',
video_duration: 187
});
// User shares content via a share button
dataLayer.push({
event: 'content_share',
content_type: 'article',
content_id: 'article-how-to-choose-a-leather-jacket',
content_title: 'How to Choose the Perfect Leather Jacket',
share_method: 'twitter', // twitter, facebook, copy_link, email, whatsapp
share_location: 'article_end'
});
Event Schema content_share
Parameter Type Required Description
event string Required "content_share" — or GA4 recommended "share" if you prefer.
content_type string Required Type of content being shared.
content_id string Required Unique content identifier.
content_title string Optional Title of the content.
share_method string Required Platform or method: twitter, facebook, copy_link, email, whatsapp.
share_location string Optional Where in the page the share action occurred: article_top, article_end, sidebar.
// User clicks a download link
dataLayer.push({
event: 'file_download',
file_name: 'heritage-co-style-guide-2024.pdf',
file_extension: 'pdf',
file_size_kb: 2400,
content_id: 'style-guide-2024',
content_title: '2024 Style Guide',
link_url: '/downloads/heritage-co-style-guide-2024.pdf'
});
dataLayer.push({
event: 'content_save',
content_type: 'product_guide',
content_id: 'sizing-guide-jackets',
content_title: 'Jacket Sizing Guide'
});

Instead of a single “article read” event, you can fire time milestones to measure genuine engagement. This is more nuanced but harder to implement.

// After 30 seconds on the article (tab must be active)
dataLayer.push({
event: 'content_time_spent',
content_id: 'article-how-to-choose-a-leather-jacket',
content_type: 'article',
time_spent_seconds: 30
});
// After 60 seconds
dataLayer.push({
event: 'content_time_spent',
content_id: 'article-how-to-choose-a-leather-jacket',
content_type: 'article',
time_spent_seconds: 60
});

Time-based events should only count active time (tab in focus). Use the Page Visibility API to pause counting when the tab is hidden.

let activeTime = 0;
let lastActivated = Date.now();
let isActive = !document.hidden;
const milestones = [30, 60, 120, 300]; // seconds
const firedMilestones = new Set();
function tick() {
if (isActive) {
activeTime += (Date.now() - lastActivated) / 1000;
lastActivated = Date.now();
milestones.forEach(milestone => {
if (activeTime >= milestone && !firedMilestones.has(milestone)) {
firedMilestones.add(milestone);
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'content_time_spent',
content_id: window.__CONTENT_ID__,
content_type: 'article',
time_spent_seconds: milestone
});
}
});
} else {
lastActivated = Date.now();
}
}
document.addEventListener('visibilitychange', () => {
isActive = !document.hidden;
lastActivated = Date.now();
});
setInterval(tick, 1000);

Firing article_read on page load. A page view is not a content engagement event. Fire content engagement events only after the user has demonstrated engagement through scroll depth, time, or explicit action.

Tracking every scroll position as an event. Scroll depth events should fire at milestones (25%, 50%, 75%, 90%) — once per threshold, once per page load. Firing on every percentage point floods GA4 with low-value events.

Not normalizing by content length. A 90% read rate on a 200-word page is meaningless. On a 2,000-word article it’s meaningful. Include content_word_count so you can segment engagement by content length.