Odometer for Craft CMS

FAQ

Is this analytics?

No, and deliberately not. There are no sessions, no referrers, no bounce rates, no funnels, no per-visitor anything, because all of that requires keeping records about people.

Odometer answers one question — "which of these has been read, and how much" — which is the question that actually changes what a site puts on its homepage. If you want analytics, use analytics; Telescope puts GA4 in the control panel and is also free.

What do you store about visitors?

Nothing.

The ledger holds element ids, site ids, counter names, dates and numbers. There is no address, no user agent, no user id, no session, no cookie and no row per view.

The repeat-visit rule does need to recognise a returning reader, and it does that by hashing the address and user agent together with the site's security key and the calendar date, keeping the hash in Craft's cache for exactly as long as the dedupe window, and never writing it anywhere. The hash cannot be reversed and stops being meaningful at midnight.

Will it work behind Blitz / Varnish / a CDN?

Yes, in beacon mode. Switch Settings → Odometer → How views are counted to With a beacon.

Not in automatic mode, and this is worth being blunt about: with a page cache in front of the site, Craft renders each page once and never hears about the readers who get the cached copy. The count does not break in a visible way — it becomes "how many cache misses happened", which looks like a plausible number and is not one.

Does the beacon get blocked?

Less than you would expect. It is served from your own domain rather than an analytics host, so the blocklists that stop third-party trackers do not match it. Nothing is guaranteed — a reader who blocks images will not report an image beacon, and a reader with JavaScript off will not report a script one.

If you need to catch both, the image is the safer default: JavaScript is switched off far more often than images are.

Can somebody inflate their own view count?

Only by being a lot of different visitors. The beacon token is signed with the site's security key and names exactly one element, so nobody can craft a request for an arbitrary id — but the token is in the page, and anybody can fetch that URL again.

That is what the dedupe window is for: repeated requests from the same address and user agent inside it count once. Somebody determined enough to rotate addresses can move a number, the same as with every view counter ever written. If the number is load-bearing — a competition, a payout — do not use a view counter for it.

Why is my count lower than Google Analytics?

Because they measure different things and disagree in both directions.

Odometer ignores obvious robots, honours Do Not Track, folds a visitor's repeat views inside the dedupe window into one, and (by default) skips admins. GA loses everybody with a blocker, which is a material share of most audiences.

Neither number is the number. Use one of them consistently and watch it move.

Why is my count higher than I expected?

Check the dedupe window first — 0 counts every request including refreshes. Then check the robot list: a crawler that does not say it is one gets counted, and a poorly-behaved one can generate a lot of views very quickly. Compare Odometer → Popular with your server's access log for the same day.

Does it slow the site down?

Two INSERT … ON DUPLICATE KEY UPDATE statements per counted view, both hitting a unique index, and no reads at all — there is no select-then-update anywhere, so two people reading the same page cannot lose one of each other's views.

Reading is a single indexed query. A views column on an element index is one join for the whole page, not one query per row.

Can one element have more than one counter?

Yes. Every method takes a key:

{% do craft.odometer.record(entry, { key: 'downloads' }) %}
{{ craft.odometer.views(entry, 'all', { key: 'downloads' }) }}
{{ craft.odometer.popular({ key: 'downloads', limit: 5 }) }}

Counters are completely independent and none of them knows the others exist.

What happens on a multi-site install?

Every count is per site, because a page read in French and a page read in English are two pages being read.

Pass siteId: '*' anywhere a site is accepted to ask about all of them at once, which is usually what you want when a piece is published in several languages and the interest in it is the sum.

What happens when I delete an entry?

Its counts go with it. The tables cascade from elements, so there is nothing left orphaned and nothing to clean up.

Soft-deleted entries keep their counts while they are in the trash, and get them back if they are restored.

Does it count previews?

No. A preview is an author looking at their own draft, and an entry that gains a view every time somebody saves it would be worse than one with no views at all. Drafts and revisions are never counted either — a view of a provisional draft belongs to the entry it is a draft of.

Can I import counts from another plugin?

Yes, with Views::set():

use justinholtweb\odometer\Plugin;

foreach ($oldCounts as $elementId => $views) {
    Plugin::getInstance()->views->set($elementId, $siteId, $views);
}

That sets the lifetime total. There is no day-by-day history to import into unless the old plugin kept any, and most did not.

Why is there no hit log?

Because it is a trap. A row per view grows without bound on exactly the sites that need this plugin most, it stores a visitor's address to answer a question nobody asked, and every query anybody writes against it is a GROUP BY over millions of rows that a daily bucket already answered.

Day buckets are the smallest thing that can answer "this week" and "trending", and they are already aggregated.

Is it really free?

Yes. No editions, no licence key, no licensing code in the plugin.