Last active
June 8, 2025 07:29
-
-
Save colorwebdesigner/1b689075777e038739845896f3e59c4d to your computer and use it in GitHub Desktop.
MODx Plugin for Unique page views counter with cookie-based tracking
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* pageViewCounter - Unique page views counter with cookie-based tracking | |
* by Colorwebdesigner | |
* based on: https://itchief.ru/modx/page-hit-counter | |
* | |
* EVENT: OnLoadWebDocument | |
* | |
* CONFIGURATION: | |
* 1. Create a TV (Template Variable) with name "views_page" (type: Number) | |
* 2. Attach TV to relevant templates | |
* 3. Install this plugin and activate | |
* | |
* USAGE: | |
* - Use placeholder [[+views_page]] in templates | |
* - View counts update only for published resources | |
* - Cookie expires after 1 year | |
* - Cookies are set for all subdomains (domain-wide) | |
* | |
* SECURITY FEATURES: | |
* - HttpOnly cookies | |
* - Secure flag for HTTPS | |
* - Bot detection | |
* - TV template validation | |
* - Domain-wide cookie support (.yourdomain.com) | |
*/ | |
// ====================== CONFIGURATION ====================== | |
$tvName = 'views_page'; // Template Variable name for counter | |
$placeholder = 'views_page'; // Placeholder for output | |
$cookiePrefix = 'viewed_'; // Cookie prefix (followed by resource ID) | |
$cookieLifetime = 86400 * 365; // 1 year cookie duration | |
$cookieDomain = ''; // Auto-detected (all subdomains). Manually set like: '.yourdomain.com' | |
// =========================================================== | |
// Event handling | |
if ($modx->event->name !== 'OnLoadWebDocument') return; | |
// Main resource check | |
$resource = $modx->resource; | |
if (!$resource || !$resource->get('published') || $resource->get('deleted')) return; | |
// Get TV by name | |
$tv = $modx->getObject('modTemplateVar', ['name' => $tvName]); | |
if (!$tv) { | |
$modx->log(modX::LOG_LEVEL_ERROR, '[pageViewCounter] TV not found: ' . $tvName); | |
return; | |
} | |
$tvId = $tv->get('id'); | |
// Verify TV-template association | |
$tvs = $resource->getTemplateVars(); | |
$tvAttached = false; | |
foreach ($tvs as $tvObj) { | |
if ($tvObj->get('id') == $tvId) { | |
$tvAttached = true; | |
break; | |
} | |
} | |
if (!$tvAttached) return; | |
// Ignore bots/crawlers | |
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; | |
$isBot = preg_match('/bot|crawl|slurp|spider|mediapartners/i', $userAgent); | |
if ($isBot) return; | |
// Get or create TV record | |
$tvResource = $modx->getObject('modTemplateVarResource', [ | |
'tmplvarid' => $tvId, | |
'contentid' => $resource->id | |
]); | |
if (!$tvResource) { | |
$tvResource = $modx->newObject('modTemplateVarResource'); | |
$tvResource->fromArray([ | |
'tmplvarid' => $tvId, | |
'contentid' => $resource->id, | |
'value' => 0 | |
]); | |
$tvResource->save(); | |
} | |
// Process view counting | |
$currentViews = (int)$tvResource->get('value'); | |
$cookieName = $cookiePrefix . $resource->id; | |
if (empty($_COOKIE[$cookieName])) { | |
// Update view count | |
$newViews = $currentViews + 1; | |
$tvResource->set('value', $newViews); | |
if ($tvResource->save()) { | |
// Set HTTP-only secure cookie (domain-wide) | |
$secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; | |
// Auto-detect domain for cross-subdomain support | |
$domain = $cookieDomain ?: ( | |
$_SERVER['HTTP_HOST'] ? '.' . ltrim($_SERVER['HTTP_HOST'], '.') : '' | |
); | |
setcookie( | |
$cookieName, | |
'1', | |
time() + $cookieLifetime, | |
'/', | |
$domain, // Sets for all subdomains | |
$secure, | |
true // HTTP-only | |
); | |
$currentViews = $newViews; | |
} else { | |
$modx->log(modX::LOG_LEVEL_ERROR, | |
'[pageViewCounter] Failed to save TV for resource ' . $resource->id | |
); | |
} | |
} | |
// Output placeholder | |
$modx->setPlaceholder($placeholder, $currentViews); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment