=== WPCreatix ViewMeter – WordPress Visitor Counter ===
Contributors: wpcreatix
Tags: visitor counter, page views, post views, hit counter, analytics
Requires at least: 6.6
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A WordPress visitor counter for page views and post views, kept on your own server. Filters bots and AI crawlers and counts behind page caching.

== Description ==

ViewMeter is a visitor counter and analytics plugin for WordPress. It counts page views, post views and visitors on your own server. There is no account to create, and nothing about your visitors is sent anywhere.

Most counters show a number and ask you to trust it. ViewMeter shows you how it got there:

* **Bots stay out, and you can see them.** Search crawlers, AI scrapers such as GPTBot and ClaudeBot, headless browsers and browser prefetches are not counted. The Quality screen shows how many of each were kept out.
* **It counts behind your cache.** Pages served by a cache plugin or a CDN still count. There is nothing to set up.
* **It tells you when counting stops.** Eight health checks watch the counter. If one fails, it says what is wrong and how to fix it.

You also get sources and campaigns, countries and cities, devices, goals, live visitors, email reports and traffic alerts, and a Views column on your posts list.

Moving from another plugin? ViewMeter can import your history from Post Views Counter, WP Statistics, Koko Analytics, Burst Statistics, Independent Analytics and more, without touching the old plugin's data.

= Show post views on your site =

Show a view count on any post or page with the View count block, the `[viewmeter]` shortcode or a template tag. List your most viewed posts with the Most viewed block or `[viewmeter_most_viewed]`. In wp-admin, every post list gets a sortable Views column.

= The screens =

* **Overview** — views, visitors, visits, bounce rate, engaged time, and a chart that marks today as partial.
* **Content** — what was read, sortable, with a rail for one page's detail, plus links clicked and forms submitted.
* **Sources** — referrers classified as direct, search, social, referral, ad or AI, plus UTM campaigns decomposed into all five parameters.
* **Places** — countries and cities, on a map and in a table.
* **Devices** — device kind, browser, operating system.
* **Goals** — score a goal against one of four signals: a page reached, a link clicked, time on site, or pages read. Give it a value and the screen reports what the conversions were worth, not just how many there were.
* **Live** — who is reading right now, with the page, where they came from, their country, their browser and how long they have been on the page.
* **Quality** — what was refused and why, and the eight health checks.
* **Settings** — everything in groups you can search, plus import and export.
* **Debugger** — replay a visit and see what counting would do with it. Nothing is written, so a replay never moves a number.
* **Privacy Audit** — reads your live settings and tables and says what is stored.

Filters stack across screens, every reading screen exports CSV, and the whole app runs on one wp-admin page.

== External services ==

ViewMeter can connect to four external services, each for one purpose. Only the crawler list is on by default; the other three stay off until you turn them on or add your own key. Nothing about your visitors is sent to any of them. What each one is sent is written out below.

**WPCreatix crawler list** — `https://wpcreatix.com/viewmeter/crawlers.json`

New AI scrapers appear between plugin releases. Rather than wait for the next release to filter them, ViewMeter downloads an updated list of crawler user-agent signatures from that file, which is published by this plugin's author.

*What is sent:* nothing about your site or your visitors. The request is a plain GET for that one file, with no query string and no body. As with any outbound request WordPress makes, the server hosting the file sees your server's IP address and WordPress's default user agent string, which contains your WordPress version and your site's address.

*What comes back:* a JSON file of lowercase user-agent substrings. They are added to the list already compiled into the plugin. A response can never remove or replace a signature, so a bad or tampered file cannot switch filtering off — the worst it can do is nothing.

*How often:* once a week, on WP-Cron.

*How to stop it:* turn off "Keep the crawler list up to date" in Settings → Advanced. Nothing is fetched after that, and the list shipped with the plugin keeps working.

WPCreatix terms: https://wpcreatix.com/terms/
WPCreatix privacy policy: https://wpcreatix.com/privacy/

**DB-IP** — `https://download.db-ip.com/free/dbip-country-lite-YYYY-MM.mmdb.gz` (or `dbip-city-lite-YYYY-MM.mmdb.gz` if you choose the city database)

Country and city reporting needs an IP geolocation database. If you turn on "Keep the location database up to date", ViewMeter downloads DB-IP's free Lite database and stores it in your uploads folder. Lookups then happen entirely on your own server — no visitor address is ever sent anywhere.

*What is sent:* nothing about your site or your visitors. The request is a plain GET for that one file, with no query string and no body. As with any outbound request, DB-IP sees your server's IP address and the user agent ViewMeter identifies itself with, which contains the plugin version and your site's address.

*What comes back:* a gzip-compressed MaxMind DB (`.mmdb`) file of IP ranges mapped to countries — about 4 MB compressed, 8 MB on disk. The city database is about 58 MB compressed and 127 MB on disk. The file is checked before it replaces anything: it must parse as a MaxMind DB, declare itself a Country or City database, and successfully resolve a known address. A corrupt or wrong file is discarded and the previous database is left in place.

*How often:* the check runs weekly on WP-Cron and only downloads when the installed database is more than 25 days old, so in practice about once a month. Nothing is fetched on a site that already gets location from its CDN's headers.

*How to stop it:* it is **off by default**. Nothing is downloaded unless you turn on "Keep the location database up to date" in Settings. You can also install a database yourself — upload one on the settings screen, or run `wp viewmeter geo install /path/to/file.mmdb` — and leave the automatic download off entirely.

The DB-IP Lite databases are published under the Creative Commons Attribution 4.0 International licence, which is why ViewMeter shows an "IP Geolocation by DB-IP" credit on screens that display location figures.

DB-IP terms of service: https://db-ip.com/tos/
DB-IP privacy policy: https://db-ip.com/privacy/
Licence: https://creativecommons.org/licenses/by/4.0/

**Moz** — `https://lsapi.seomoz.com/v2/url_metrics`

Off by default. Used only if you choose Moz under "Referrer authority scores from" and add your own Moz API key in Settings → Connected services. ViewMeter then shows an authority score beside each site that sends you visitors.

*What is sent:* the domain names of the sites that sent you visitors (for example `news.ycombinator.com`) and your key. Nothing about your visitors.

*How often:* once a day, on WP-Cron. A score is kept for 30 days before it is asked for again.

*How to stop it:* set "Referrer authority scores from" back to "Not shown", or delete the key.

Moz terms of use: https://moz.com/terms-of-use
Moz privacy policy: https://moz.com/privacy-policy

**DeepL** — `https://api-free.deepl.com/v2/translate` (free key) or `https://api.deepl.com/v2/translate` (paid key)

Off by default. Used only if you turn on "Offer machine translation for report labels" and add your own DeepL key in Settings → Connected services. On a site with more than one language, ViewMeter asks DeepL to suggest translations for the labels you typed: your goal names and the display label shown beside a count. You accept or discard each suggestion, and nothing replaces your own words until you accept it.

*What is sent:* the label text, the source and target language codes, and your key. No page content and nothing about your visitors.

*How often:* once a day on WP-Cron for labels that have no suggestion yet, and when you press Suggest on a goal.

*How to stop it:* turn off "Offer machine translation for report labels", or delete the key.

DeepL terms: https://www.deepl.com/pro-license
DeepL privacy policy: https://www.deepl.com/privacy

No other data is sent anywhere. The health check and the fast endpoint each make a request, but both are addressed to your own site.

== Installation ==

1. Install and activate. The tables are created on activation.
2. Open ViewMeter in the admin menu. Counting starts on the next front-end page view.
3. Optional: choose what to count in Settings → Tracking, and who to skip in Settings → Exclusions.

Nothing else is required. If a check on the Quality screen is failing, it names the thing and the fix.

== Frequently Asked Questions ==

= Does the view counter work with caching plugins like WP Rocket? =

Yes, without configuration. Counting happens in the browser and posts to your site, so a cached page counts correctly. The tracker is explicitly excluded from the combining, minification and script-delay features of the common optimisers, because that is what usually breaks a counter. The health check confirms the endpoint answers.

= Why are my numbers lower than Google Analytics? =

Usually because ViewMeter refuses traffic other tools count: crawlers, AI scrapers, headless browsers, prefetches and your own signed-in team. The Quality screen shows exactly what was refused and why, so you can check rather than guess.

= Will it slow down my site? =

Not in any way you will notice. The counting script is small (about 6 KB compressed). A view is written to a file, not to the database, and a job turns the file into rows once a minute. On a very busy site you can turn on the fast endpoint in Settings → Advanced, which records a view without loading WordPress.

= Does it set cookies? =

Counting you sets none. Repeat visitors are recognised by a salted hash that is re-keyed daily. A first-party cookie mode is available in Settings → Privacy if you need true unique counts across days; it usually needs consent.

Two other cookies are written only when a visitor presses something: `viewmeter_consent`, which remembers their answer to the consent banner if you switch that on, and `viewmeter_optout`, which remembers that they asked never to be counted. Each holds one word and no identifier of any kind. The opt-out one is relied on as strictly necessary rather than consented to, because it exists only to carry out the request the visitor made — which is also why it keeps working for someone who refused consent.

= Is it GDPR compliant? =

ViewMeter is built to store nothing that identifies an anonymous visitor, and the Privacy Audit screen reads your live settings and tables so you can check that for your install rather than take it on trust. A privacy-policy section is supplied for your policy page, written from your live settings rather than from the defaults, and it links your readers to a page where they can withdraw consent or exclude themselves for good. WordPress's own export and erase requests are answered as well. Compliance depends on your whole site, so this is not legal advice.

= Can I import my old view counts from another plugin? =

Yes: Post Views Counter, WP Statistics, Burst Statistics, Koko Analytics, Independent Analytics, WordPress Popular Posts, SlimStat Analytics, Page Views Count, a post meta field, and CSV exports from Jetpack Stats and Plausible. The import never writes to the other plugin's tables, so you can evaluate it and go back.

= How do I show post views on my WordPress site? =

* Two blocks: View count and Most viewed.
* Shortcodes: `[viewmeter]`, `[viewmeter_most_viewed]` and `[viewmeter_site_views]`, plus `[viewmeter_product_views]` on WooCommerce sites.
* A classic widget, and an Elementor widget when Elementor is active.
* Four template tags: `viewmeter_get_views()`, `viewmeter_the_views()`, `viewmeter_get_most_viewed()`, `viewmeter_get_site_views()`.
* Automatic display above or below the content, with a threshold, so a post with three views shows no count at all.
* Your own markup for a count — `[viewmeter template="{views} views since {date}"]` — instead of only a label and a position. See "Can I change how the count looks?" below.
* `'orderby' => 'views'` in any `WP_Query`, and so in the Query Loop block. Totals are also copied to post meta (on by default, Settings → Data), so page builders that can only sort by a custom field can sort by views too.

= Where do country and city come from? =

From a header your CDN already sets — Cloudflare, CloudFront, App Engine, Fastly and the common GeoIP modules are all read — or from a local database file. No database is bundled in the download, and no lookup service is ever called, so a visitor's address is never sent anywhere: the file sits on your own server and is read there. You can upload one yourself, run `wp viewmeter geo install`, or switch on the automatic DB-IP download. With no source available, Places says so rather than showing zeros.

= Can it email me reports or alerts? =

Yes. Scheduled email reports go to the people you choose, and traffic alerts tell you when something changes.

ViewMeter also tells you when a page passes a round number of views, when the whole site does, and when it sets a new busiest day or busiest month. Each milestone is sent once, ever. The first check writes down the records your site already holds and sends nothing, so switching the feature on does not mail you years of history. Milestones report views only, never visitor counts.

= How do I know my visitor counter is still working? =

Analytics can fail without any sign, because no traffic and broken counting look the same on a chart. Eight checks run: the buffer folder is writable, the aggregator has drained recently, WP-Cron is alive, every table exists, the collection endpoint answers, counting without JavaScript is working if you turned it on, geolocation is resolving, and whether a persistent object cache is in use. A failing check names the thing, the condition and the fix. `wp viewmeter health` exits non-zero when one fails, so it can drive a monitor across a fleet.

ViewMeter never changes a setting on its own. If a firewall is challenging the loopback probe, it says the probe could not decide rather than claiming counting is broken.

= Does it filter bots and AI crawlers like GPTBot? =

Yes. A crawler list compiled into the plugin covers search engines and AI scrapers including GPTBot, ClaudeBot, CCBot, PerplexityBot and Bytespider. Headless browsers and automation drivers — HeadlessChrome, Puppeteer, Playwright, Selenium — are refused too. So are browser prefetch and prerender, so a page nobody looked at is not a view, and feeds, previews, your own team by role, and IP addresses, ranges or wildcards you list.

Sixteen reasons in total. The Quality screen lists every reason that refused something in the dates you picked, with how many hits it refused, its share of all hits, and one line saying what it means.

= Which other plugins does it work with? =

WooCommerce, counting products the way posts are counted and sorting the products list by views. WPML and Polylang, with translations sharing one count or keeping their own. AMP, through `amp-pixel`, because AMP forbids the normal beacon.

Caching and optimisation: WP Rocket, LiteSpeed Cache, WP Fastest Cache, WP-Optimize, Autoptimize, Hummingbird, W3 Total Cache, Breeze, Cache Enabler and Perfmatters. Each is opted out through its own documented mechanism, because an optimiser folding the tracker into a combined bundle, or deferring it past the point the visitor has gone, is a common reason a counter reads zero.

Sort-by-views in the query loops of Elementor Pro, Divi, Beaver Builder, GenerateBlocks and Kadence Blocks. Form plugins, which record that a form was submitted and never what was in it.

= Does it work with custom post types? =

Yes. Choose which types to count in Settings → Tracking. Anything switched off is named on the Quality screen, so a post type cannot read zero without you being told why.

= Can an editor see the reports without changing settings? =

Yes. Reading the reports and changing the settings are two separate capabilities, mapped per role. An administrator always holds both.

= Can I correct a count? =

Yes. Set a lifetime figure exactly in Quick Edit, or reset counts in Bulk Edit.

= How much database space does it use? =

It depends on your traffic. ViewMeter keeps its data in two layers.

The **aggregate layer** is kept forever: per-page per-day counters, running lifetime totals, a per-day rollup for each dimension, and the tally of what was refused. These are what the reports read, and they grow with your page count, not your traffic.

The **raw layer** — one row per hit, one per session, one per visitor hash — grows with traffic and is kept for the retention window, 365 days by default. Pruning removes only those rows; lifetime totals and per-day history do not move. What you lose is cross-tabs: "visitors from Germany, by day, for three years" answers forever; "visitors from Germany reading this post" only answers inside the window. Set the window to 0 and nothing is ever pruned.

There are eighteen tables, most of them small lookup tables. If you choose to archive old visit detail instead of deleting it, three more are added to hold it. The Debugger screen shows the size on disk and the row count for every table.

= Can I export, print or archive my data? =

Export as CSV, TSV, Excel, XML or NDJSON, streamed, so five years costs the same memory as a week.

You can also download a report as a PDF, or open a printable version and save it as a PDF from your browser's Print dialog. The PDF can only draw Western European letters; for other alphabets it tells you, and you can use the printable version instead.

Instead of deleting raw visit detail at the retention window, you can archive it: the same rows move into sibling tables the reports never read, so the live tables stay small and the detail is still there. Archived rows can be put back, and putting them back changes no reported figure. Worth knowing for a privacy policy: archived rows are kept, not deleted, so visitor hashes archived this way outlive the retention window until you delete the archive.

= Can I make stats public or read them from an API? =

Both are off when you install ViewMeter, and neither exposes anything until you switch it on.

The public stats page opens one address that shows your traffic to anyone who has the link, with no WordPress login. You choose how much it shows: totals and trend only, plus your most-read pages, or plus where the traffic came from. The page carries no login link, no form and no JavaScript, and search engines are asked not to index it. Only content a visitor could already open appears on it: drafts, private and password-gated posts, and posts of non-public post types are left out, and so are author archives, site-search rows and missing URLs.

The read API lets an external dashboard or script read the same figures using a token instead of a login. Create as many tokens as you need, name each one, and give each the narrowest access that does the job. ViewMeter shows a token once, when you create it, and stores only a hash of it, so nobody can read it back afterwards — if you lose one, revoke it and make another. Revoking takes effect on the very next request.

Neither surface can reach the per-visit log, the live visitor feed, the logged-in-visitor report, your site's search terms, visitor cities, or the per-author ranking. Those stay behind a WordPress login, because a list of visits is about the people who made them.

= Does it need cron? =

Yes. A one-minute job turns the buffered hits into rows, so a view is counted a minute or so after it happens, not instantly. WP-Cron only runs when someone visits, so on a very quiet site the delay is longer. `wp viewmeter aggregate` drains the buffer immediately, and the health check reports when WP-Cron is disabled. Nothing is lost by a late run — the hits are already on disk.

= Does it honour Do Not Track? =

Only if you ask it to. Both the DNT and the Global Privacy Control headers are read, but "Honour Do Not Track" in Settings → Privacy is off by default. Turn it on and a visit carrying either header is refused, and counted under its own reason on the Quality screen.

= What does it store about a visitor? =

A salted hash, the session's timings, and lookup ids for referrer, campaign, country, city, device, operating system and browser. No IP address in any form, no cookie from being counted, and no form field values. One exception is worth stating: for a signed-in visitor who is not excluded, the ids of that visitor's sessions are appended to their WordPress user meta so an administrator can ask what a registered user read.

With anonymous mode on, none of the above. No visitor hash, no session, no raw hit, no user agent, no referrer, no campaign tag and no country — only how many times each page was viewed, per day. A visitor who has excluded themselves has nothing stored at all, on any transport.

= How does it protect visitor privacy? =

No visitor data leaves your server. The outside services ViewMeter can use are listed under External services. Only the weekly crawler list download is on by default, and none of them is sent anything about your visitors.

A repeat visitor is recognised by `sha256(daily salt + IP + user agent)`; the address is used to build the hash and dropped in the same call, so no visitor IP address is written to any table. The salt is re-keyed every UTC day, so activity cannot be linked across days, and someone who returns tomorrow counts as a new visitor.

Do Not Track, Global Privacy Control and consent are covered above. The WP Consent API is supported, and Complianz, CookieYes, Cookiebot, Cookie Notice, Borlabs Cookie and Real Cookie Banner are recognised. ViewMeter's own consent banner is off by default, and it appears only when "Wait for consent" and "Show a consent banner" are both on and none of those consent plugins is active.

Anonymous mode records views per page per day and nothing else, so every report that needs a visitor — visitors, visits, sources, places, devices, live, goals and the rest — reads zero while it is on.

Any visitor can exclude themselves permanently at `…/wp-admin/admin-post.php?action=viewmeter_privacy`. That page exists on every install, is linked from the privacy-policy text and the banner, and needs no theme support. From then on nothing about their visits is recorded, and the refusal is filed under "Asked not to be counted" on the Quality screen. The same page withdraws consent.

For a signed-in visitor who is not excluded, the ids of that visitor's sessions are appended to their own user meta, so an administrator can ask what a registered user read. It honours your exclusion settings, only covers sessions still inside the retention window, and needs the manage capability, not the read one.

= What happens when I delete the plugin? =

Nothing, unless you asked for it. Deactivating never deletes data. Deleting removes the tables only if "Delete everything when the plugin is removed" was switched on first. Export your history from Settings → Data before you go.

= What does it not do? =

* No IP geolocation database is bundled and no lookup service is ever called with a visitor's address. With no CDN header and no local database, Places reports unknown rather than guessing.
* Cross-tabs between two dimensions only work inside the retention window.
* No cookie banner unless you ask for one, and none at all on a site already running a consent plugin.
* No advertising, and no notice outside ViewMeter's own screens.

= Can I change how the count looks? =

Yes. Pass a template to the shortcode: `[viewmeter template="..."]`. Anywhere in it you can use `{views}`, `{label}`, `{icon}`, `{title}`, `{date}`, `{period}` and `{url}`; anything else in braces is left exactly as you typed it. A template that comes out empty after sanitising falls back to the plugin's own markup, so a count never silently disappears.

A template may contain inline HTML such as `span`, `strong` or `a`. Scripts, images, forms and block tags are removed, using WordPress's own `wp_kses()` rules, before any placeholder is filled in — so a post title containing markup is shown as text.

= Can I use it from WP-CLI? =

Yes. Reads go through the same class the REST API uses, so a figure cannot differ between the command line and the dashboard. The main commands:

* `wp viewmeter stats` — site totals for a date range, compared with the period before
* `wp viewmeter top` — most viewed pages
* `wp viewmeter dimension <name>` — one breakdown, such as referrer, country, device or browser
* `wp viewmeter exclusions` — what was refused, by reason
* `wp viewmeter health` — whether counting works; exits non-zero when it does not
* `wp viewmeter aggregate` — process waiting views now
* `wp viewmeter prune` — delete visit detail older than the retention window, `--dry-run` supported
* `wp viewmeter rotate-salt` — start a new visitor salt now
* `wp viewmeter reset` — empty every table

There are more for location (`wp viewmeter geo`), content (`wp viewmeter content`), domains (`wp viewmeter hosts`) and API tokens (`wp viewmeter public-token`). Run `wp help viewmeter` for the full list.

= Can I extend it in code? =

Yes. ViewMeter has 77 hooks: 73 in PHP (59 filters and 14 actions) and 4 in the admin app's JavaScript through `@wordpress/hooks`, which let another plugin add a screen, a route or a settings card. They cover the counting path (what counts as a bot, what a visit's dimensions are, when a hit is refused), the figures (bounce threshold, how a referrer is classified, which axes a report may group by), what visitors see, geolocation, scheduled email, Site Health, and the features that boot at all. The main ones are listed in Settings → Add-ons, and every hook is documented where it is applied in the source.

A filter is added the usual way:

`add_filter( 'viewmeter_bounce_ms', function ( $ms ) { return 10000; } );`

= How does counting work? =

The count happens in the browser. The page identity is worked out in PHP while the page renders and signed, so the browser can only echo back something the server already vouched for. A hit is appended to a file, not written to the database, and a one-minute job turns the file into rows. That is why a cached page still counts, and why a busy page does not cost a database write per view.

There is no mode to pick and no nonce baked into HTML that expires while the page is still cached. Counting on the server as well is a separate switch, off by default, and a view the tracker already reported is dropped rather than counted twice.

Two things are off by default and worth knowing about. The **fast endpoint** writes a small PHP file to your WordPress root that records a view without loading WordPress; it re-tests itself hourly and falls back to the REST route if it stops answering. The **hidden tracker name** serves the counting script under a per-site name that contains none of the words ad-blocker lists match on. Both live in Settings → Advanced and neither is on unless you turn it on.

== Screenshots ==

1. See views, visitors and visits at a glance. Today is marked as partial, so a half-finished day never looks like a drop.
2. Find your most-read pages, and click one to see its own figures without leaving the list.
3. See where visitors come from: search, social, other sites, AI assistants and your tagged campaigns.
4. See which countries and cities your readers are in.
5. See which bots and crawlers were kept out of your numbers, and how many.
6. Eight health checks tell you if counting stops, and how to fix it.
7. Test a visit to see whether it would be counted, and why. Nothing is saved.
8. A Views column with a small trend chart on your posts list.

== Credits ==

Country outlines are drawn from Natural Earth 1:50m admin-0 boundaries, which are public domain, redistributed as world-atlas (ISC): https://github.com/topojson/world-atlas

Everything else — icons, flags, drawings, the mark — was drawn for this plugin.

== Changelog ==

= 1.0.0 =
* First release.
* Browser-side counting that works behind full-page caching, buffered to a file and aggregated once a minute.
* Sixteen refusal reasons, each one reported with its count and its meaning on the Quality screen.
* Weekly crawler-signature updates, which can only add to the list shipped in the plugin. Off with one setting.
* Aggregate tables kept forever, raw visit detail kept for a retention window that defaults to 365 days.
* Screens for Overview, Content, Sources, Places, Devices, Goals, Live, Quality and Settings, plus a Debugger and a Privacy Audit.
* Eight health checks, plus a replay tool that answers why a visit was or was not counted.
* Blocks, shortcodes, widgets, template tags, an editor metabox and `orderby=views`.
* A sortable Views column with a sparkline on post lists, and a Views column on term lists and the users list.
* Import from eight other analytics plugins, a post meta field, and CSV exports from Jetpack Stats and Plausible; export as CSV, TSV, Excel, XML or NDJSON, plus a PDF report and a printable report.
* Email reports, traffic alerts and view milestones.
* Optional referrer authority scores from Moz and label translation suggestions from DeepL, each with your own key and off by default.
* WP-CLI commands and a REST API under `viewmeter/v1`.
* WooCommerce, WPML, Polylang, AMP, consent plugins, caching plugins and page builders.

== Upgrade Notice ==

= 1.0.0 =
First release.
