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.”
Article read event
Section titled “Article read event”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 articledataLayer.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});| 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. |
Video play event
Section titled “Video play event”For custom video players (not YouTube or Vimeo with GA4’s Enhanced Measurement enabled), implement video tracking manually.
// Video play starteddataLayer.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 milestonedataLayer.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 completeddataLayer.push({ event: 'content_video_complete', content_id: 'video-summer-lookbook-2024', content_title: 'Summer Lookbook 2024', video_duration: 187});Content share event
Section titled “Content share event”// User shares content via a share buttondataLayer.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'});| 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. |
File download event
Section titled “File download event”// User clicks a download linkdataLayer.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'});Content save / bookmark event
Section titled “Content save / bookmark event”dataLayer.push({ event: 'content_save', content_type: 'product_guide', content_id: 'sizing-guide-jackets', content_title: 'Jacket Sizing Guide'});Time-on-content events
Section titled “Time-on-content events”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 secondsdataLayer.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]; // secondsconst 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);Common mistakes
Section titled “Common mistakes”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.