{"id":361944,"date":"2026-09-11T09:01:48","date_gmt":"2026-09-11T09:01:48","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/outbound-click-tracking\/"},"modified":"2026-09-11T09:01:18","modified_gmt":"2026-09-11T09:01:18","slug":"hopcount-click-tracking","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/hopcount-click-tracking\/","author":13667033,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.1.1","stable_tag":"1.1.1","tested":"7.1","requires":"6.5","requires_php":"8.0","requires_plugins":null,"header_name":"Hopcount Click Tracking","header_author":"aph5","header_description":"Privacy-friendly outbound and affiliate click tracking. Server-side link pre-registration, token-only click endpoint, hourly aggregate counts, no cookies or per-click rows.","assets_banners_color":"26479f","last_updated":"2026-09-11 09:01:18","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/idkfa2\/hopcount-click-tracking","header_author_uri":"https:\/\/profiles.wordpress.org\/aph5\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":42,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.1.1":{"tag":"1.1.1","author":"aph5","date":"2026-09-11 09:01:18","revision":3691090}},"upgrade_notice":{"1.1.0":"<p>Plugin folder renamed to hopcount-click-tracking. Deactivate the old folder, upload the new one, activate. Data and settings are preserved.<\/p>","1.0.1":"<p>Hardening of the pre-1.0 data migration. No functional changes for fresh installs.<\/p>","1.0.0":"<p>Renamed plugin. Deactivate the pre-1.0 plugin before activating this one; data migrates automatically. Site code using the old filter names and any rate-limit rule on the old endpoint path must be updated.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3691090,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3691090,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3691090,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3691090,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3691090,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.1.1"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3691090,"resolution":"1","location":"assets","locale":"","width":1440,"height":740},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3691090,"resolution":"2","location":"assets","locale":"","width":1440,"height":740},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3691090,"resolution":"3","location":"assets","locale":"","width":1440,"height":1034},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3691090,"resolution":"4","location":"assets","locale":"","width":1440,"height":740},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3691090,"resolution":"5","location":"assets","locale":"","width":605,"height":635}},"screenshots":{"1":"Report: date range, host filter, search, summary cards and the sortable link table.","2":"Labels: manual names for destination URLs.","3":"Settings: master switch, excluded roles, internal hosts, retention, rate limiting and uninstall behaviour.","4":"Status: schema, cron and limiter health.","5":"Dashboard widget with the top links for the selected period."}},"plugin_section":[262246],"plugin_tags":[369,232,5375,5373,396],"plugin_category":[35,36,54],"plugin_contributors":[254585],"plugin_business_model":[],"class_list":["post-361944","plugin","type-plugin","status-publish","hentry","plugin_section-dashboard-widgets","plugin_tags-affiliate","plugin_tags-analytics","plugin_tags-click-tracking","plugin_tags-outbound-links","plugin_tags-privacy","plugin_category-advertising","plugin_category-analytics","plugin_category-security-and-spam-protection","plugin_contributors-aph5","plugin_committers-aph5"],"banners":{"banner":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/banner-772x250.png?rev=3691090","banner_2x":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/banner-1544x500.png?rev=3691090","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/icon.svg?rev=3691090","icon":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/icon.svg?rev=3691090","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/screenshot-1.png?rev=3691090","caption":"Report: date range, host filter, search, summary cards and the sortable link table."},{"src":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/screenshot-2.png?rev=3691090","caption":"Labels: manual names for destination URLs."},{"src":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/screenshot-3.png?rev=3691090","caption":"Settings: master switch, excluded roles, internal hosts, retention, rate limiting and uninstall behaviour."},{"src":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/screenshot-4.png?rev=3691090","caption":"Status: schema, cron and limiter health."},{"src":"https:\/\/ps.w.org\/hopcount-click-tracking\/assets\/screenshot-5.png?rev=3691090","caption":"Dashboard widget with the top links for the selected period."}],"raw_content":"<!--section=description-->\n<p>Hopcount Click Tracking records how often visitors click the outbound and affiliate links in your posts and pages, and shows the totals in wp-admin. It does this without redirecting or cloaking anything: your links keep their real destination URLs, which search engines and affiliate programme terms both prefer.<\/p>\n\n<p><strong>What it stores<\/strong><\/p>\n\n<ul>\n<li>Hourly click counts per link. That is the whole data model: no per-click rows, no IP addresses, no user agents, no referrers, no user IDs, no cookies.<\/li>\n<li>Which post each link appeared in, the destination URL and its host, and the link text (used as an automatic label).<\/li>\n<\/ul>\n\n<p>There is nothing to anonymise and nothing to disclose in a privacy policy beyond \"aggregate click counts are kept\".<\/p>\n\n<p><strong>How it works<\/strong><\/p>\n\n<ol>\n<li>When a post renders, eligible outbound links get a short server-generated token attribute. Links to your own site (and any hosts you list as internal) are skipped.<\/li>\n<li>A small deferred script (about 700 bytes compressed) sends a beacon to a REST endpoint when a link is activated, including middle-click and keyboard activation.<\/li>\n<li>The endpoint resolves the token and increments the hourly counter.<\/li>\n<\/ol>\n\n<p>Because the token lives in the rendered HTML, the plugin keeps working behind WP Rocket, WP Super Cache, Cloudflare APO and similar full-page caches. Cached page views cost zero database queries; only the click itself reaches PHP.<\/p>\n\n<p><strong>Reporting<\/strong><\/p>\n\n<ul>\n<li>Report screen with date presets or a custom range, host filter, search by post title or destination, summary cards and CSV export.<\/li>\n<li>Labels screen to give any destination URL a friendly name.<\/li>\n<li>Dashboard widget with the top links for today, yesterday, 7, 30 or 90 days.<\/li>\n<li>Status screen showing schema, cron and rate-limiter health.<\/li>\n<\/ul>\n\n<p><strong>Operations<\/strong><\/p>\n\n<ul>\n<li>Bounded retention: hourly rows older than the configured window (default 24 months) are pruned daily.<\/li>\n<li>Layered abuse protection: the endpoint is POST-only and token-validated, an optional built-in per-IP limiter runs on your persistent object cache, and the documentation recommends a CDN or WAF rate-limiting rule.<\/li>\n<li>Uninstall keeps your data unless you opt in to deletion.<\/li>\n<li>Two filters let you grant report and label access to editors or authors.<\/li>\n<\/ul>\n\n<p><strong>Compared with redirect-based link plugins<\/strong><\/p>\n\n<p>Redirect or \"cloaking\" plugins route every click through your server and typically store one row per click. This plugin keeps links as real hrefs, records aggregates only, and adds no PHP work to cached page views.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>hopcount-click-tracking<\/code> folder to <code>\/wp-content\/plugins\/<\/code>, or install it from the Plugins screen.<\/li>\n<li>Activate the plugin. The tables and the daily retention task are created on activation.<\/li>\n<li>Review <strong>Click Tracking &gt; Settings<\/strong> (master switch, excluded roles, internal hosts, retention, rate limiting).<\/li>\n<li>If you run a page cache, purge it once so existing posts are annotated on their next render.<\/li>\n<\/ol>\n\n<h4>Upgrading from the pre-1.0 release<\/h4>\n\n<p>Upload this plugin next to the old one, deactivate the old plugin first, then activate this one. Existing tables, settings and the widget preference are renamed in place automatically. Check <strong>Click Tracking &gt; Status<\/strong> afterwards, purge your page cache, then delete the old plugin folder. Update any site code using the old filter names and any CDN or WAF rule on the old endpoint path.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20it%20work%20with%20caching%20plugins%20and%20cdns%3F\"><h3>Does it work with caching plugins and CDNs?<\/h3><\/dt>\n<dd><p>Yes, by design. The tracking token is part of the cached HTML and the click beacon goes to a REST endpoint that bypasses page caches. WP Rocket, WP Super Cache, W3 Total Cache, Cloudflare APO and similar all work without special configuration.<\/p><\/dd>\n<dt id=\"does%20it%20store%20ip%20addresses%20or%20any%20personal%20data%3F\"><h3>Does it store IP addresses or any personal data?<\/h3><\/dt>\n<dd><p>No. Only hourly aggregate counts per link are stored. The optional rate limiter keeps a short-lived per-IP counter in your object cache (a few seconds to a minute) and never writes it to the database.<\/p><\/dd>\n<dt id=\"is%20it%20gdpr%20friendly%3F\"><h3>Is it GDPR friendly?<\/h3><\/dt>\n<dd><p>There are no cookies, no personal data and no per-visitor records, so no consent banner is needed for this plugin. Always check with your own adviser for your specific situation.<\/p><\/dd>\n<dt id=\"does%20it%20change%20my%20links%20or%20redirect%20visitors%3F\"><h3>Does it change my links or redirect visitors?<\/h3><\/dt>\n<dd><p>No. Links keep their original href. The plugin only adds a data attribute at render time.<\/p><\/dd>\n<dt id=\"what%20counts%20as%20an%20outbound%20link%3F\"><h3>What counts as an outbound link?<\/h3><\/dt>\n<dd><p>Any link in post or page content whose host is not your site (including its www and non-www forms) and not in your internal-hosts list. Links elsewhere in the theme (menus, widgets, footers) are not tracked.<\/p><\/dd>\n<dt id=\"which%20users%20are%20tracked%3F\"><h3>Which users are tracked?<\/h3><\/dt>\n<dd><p>Visitors and logged-in users whose role is not in the excluded-roles list (administrators and editors by default).<\/p><\/dd>\n<dt id=\"can%20editors%20or%20authors%20see%20the%20report%3F\"><h3>Can editors or authors see the report?<\/h3><\/dt>\n<dd><p>Yes, by adding two filters in a small site plugin or your theme's functions.php:<\/p>\n\n<pre><code>add_filter( 'octk_report_capability', fn() =&gt; 'edit_published_posts' );\nadd_filter( 'octk_label_capability', fn() =&gt; 'edit_published_posts' );\n<\/code><\/pre><\/dd>\n<dt id=\"does%20it%20support%20multisite%3F\"><h3>Does it support multisite?<\/h3><\/dt>\n<dd><p>Not yet. Activation on a multisite network is refused.<\/p><\/dd>\n<dt id=\"what%20happens%20on%20uninstall%3F\"><h3>What happens on uninstall?<\/h3><\/dt>\n<dd><p>Data is kept by default. Tick the option on the Settings screen if you want the tables and settings removed when the plugin is deleted.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.1.1<\/h4>\n\n<ul>\n<li>Every database query now passes table names through wpdb::prepare() (%i placeholders) and the report sort clause is chosen from a fixed list of complete ORDER BY strings. No behaviour change; addresses the WordPress.org plugin review feedback.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>Renamed to Hopcount Click Tracking (slug and text domain <code>hopcount-click-tracking<\/code>). Code prefix, hooks, tables, options and the REST route are unchanged, so existing data and site code carry over.<\/li>\n<\/ul>\n\n<h4>1.0.1<\/h4>\n\n<ul>\n<li>Legacy data adoption now verifies every copied option and user preference before removing the source, checks that the old cron hook was cleared, and records the schema version only after the migrated tables pass the structure probe.<\/li>\n<li>Plugin URI points at the public GitHub repository.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>First WordPress.org release. Pre-1.0 installs are migrated automatically on first load.<\/li>\n<li>Breaking: all hooks, the REST route and the link attribute now use the <code>octk<\/code> prefix.<\/li>\n<li>All strings are translatable.<\/li>\n<li>Admin styles and scripts use the WordPress enqueue API.<\/li>\n<\/ul>\n\n<h4>0.6.0<\/h4>\n\n<ul>\n<li>Dashboard widget lists the top 10 links.<\/li>\n<\/ul>\n\n<h4>0.5.x<\/h4>\n\n<ul>\n<li>De-branded for general use; CSV export filename includes the date range; documentation added.<\/li>\n<\/ul>\n\n<h4>0.3.x to 0.4.x<\/h4>\n\n<ul>\n<li>Report search rework, per-post link pinning, security hardening.<\/li>\n<\/ul>\n\n<h4>0.1.0 to 0.3.1<\/h4>\n\n<ul>\n<li>Initial private releases: token annotation, beacon listener, REST endpoint, hourly aggregates, Report, Labels, Settings and Status screens, dashboard widget, retention, rate limiter, CSV export.<\/li>\n<\/ul>","raw_excerpt":"Counts clicks on outbound and affiliate links without redirects, cookies, per-click rows or personal data. Works behind full-page caches.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/361944","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=361944"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/aph5"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=361944"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=361944"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=361944"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=361944"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=361944"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=361944"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}