prefix . 'azlinkpro_clicks'; } /** Create or update the table when the stored version differs. */ function azlinkpro_clicks_install() { if (get_option('azlinkpro_clicks_db') === AZLINKPRO_CLICKS_DB_VERSION) { return; } global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = azlinkpro_clicks_table(); $charset = $wpdb->get_charset_collate(); dbDelta("CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, day date NOT NULL, page_id bigint(20) unsigned NOT NULL DEFAULT 0, src_type varchar(12) NOT NULL DEFAULT '', src_id bigint(20) unsigned NOT NULL DEFAULT 0, asin varchar(10) NOT NULL DEFAULT '', spot varchar(8) NOT NULL DEFAULT '', clicks int(10) unsigned NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY click (day,page_id,src_type,src_id,asin,spot), KEY day (day) ) {$charset};"); update_option('azlinkpro_clicks_db', AZLINKPRO_CLICKS_DB_VERSION); } add_action('admin_init', 'azlinkpro_clicks_install'); /** Settings: on (count clicks) and editors (also count logged-in editors and admins). */ function azlinkpro_clicks_settings() { $saved = get_option('azlinkpro_click_tracking', array()); $saved = is_array($saved) ? $saved : array(); return array( 'on' => !isset($saved['on']) || !empty($saved['on']), 'editors' => !empty($saved['editors']), ); } /** Display types: shortcode/block => [type key, post type]. */ function azlinkpro_clicks_sources() { return array( 'azlinkpro_table' => array('table', 'azlinkpro_table'), 'amatablepro' => array('table', 'azlinkpro_table'), 'azlinkpro_showcase' => array('showcase', 'azlinkpro_showcase'), 'amatablepro_showcase' => array('showcase', 'azlinkpro_showcase'), 'azlinkpro_showcase_grid' => array('showcase', 'azlinkpro_showcase'), 'azlinkpro_pros_cons' => array('proscons', 'azlinkpro_proscons'), 'azlinkpro_procon' => array('proscons', 'azlinkpro_proscons'), 'amatablepro_pros_cons' => array('proscons', 'azlinkpro_proscons'), 'amatablepro_procon' => array('proscons', 'azlinkpro_proscons'), 'azlinkpro_amazon_image' => array('image', 'azlinkpro_image'), ); } /** Type key => post type, plus labels for reports. */ function azlinkpro_clicks_types() { return array( 'table' => array('azlinkpro_table', 'Table', 'Tables'), 'showcase' => array('azlinkpro_showcase', 'Showcase', 'Showcases'), 'proscons' => array('azlinkpro_proscons', 'Pros & Cons card', 'Pros & Cons cards'), 'image' => array('azlinkpro_image', 'Amazon Image', 'Amazon Images'), ); } /** Should this page view load the click counter? */ function azlinkpro_clicks_should_track() { if (is_admin() || is_feed() || (defined('REST_REQUEST') && REST_REQUEST)) { return false; } $settings = azlinkpro_clicks_settings(); if (!$settings['on']) { return false; } return $settings['editors'] || !current_user_can('edit_posts'); } /** Add data-azlinkpro-src="type-id" to the first real HTML tag of a display. */ function azlinkpro_clicks_mark($output, $type, $id) { if (!is_string($output) || $output === '' || strpos($output, 'data-azlinkpro-src=') !== false) { return $output; } if (!preg_match_all('/<([a-zA-Z][a-zA-Z0-9-]*)\b/', $output, $m, PREG_OFFSET_CAPTURE)) { return $output; } foreach ($m[1] as $tag) { if (in_array(strtolower($tag[0]), array('style', 'script', 'link', 'noscript', 'template', 'meta'), true)) { continue; } $at = $tag[1] + strlen($tag[0]); azlinkpro_clicks_enqueue(); return substr_replace($output, ' data-azlinkpro-src="' . esc_attr($type . '-' . absint($id)) . '"', $at, 0); } return $output; } function azlinkpro_clicks_shortcode_output($output, $tag, $attr) { $sources = azlinkpro_clicks_sources(); if (!isset($sources[$tag]) || !azlinkpro_clicks_should_track()) { return $output; } $id = is_array($attr) ? absint($attr['id'] ?? 0) : 0; return azlinkpro_clicks_mark($output, $sources[$tag][0], $id); } // After the layout (30) and width (40) filters, which also edit the opening tag. add_filter('do_shortcode_tag', 'azlinkpro_clicks_shortcode_output', 50, 3); /** Custom Comparison and Side-by-Side tables replace the output through pre_do_shortcode_tag. */ function azlinkpro_clicks_shortcode_override($override, $tag, $attr) { return is_string($override) && $override !== '' ? azlinkpro_clicks_shortcode_output($override, $tag, $attr) : $override; } add_filter('pre_do_shortcode_tag', 'azlinkpro_clicks_shortcode_override', 50, 3); /** The Amazon Image block renders without do_shortcode. */ function azlinkpro_clicks_block_output($content, $block) { if (($block['blockName'] ?? '') !== 'azlinkpro/amazon-image' || !azlinkpro_clicks_should_track()) { return $content; } return azlinkpro_clicks_mark($content, 'image', absint($block['attrs']['id'] ?? 0)); } add_filter('render_block', 'azlinkpro_clicks_block_output', 10, 2); /** Load the small click counter in the footer of pages that show an AzlinkPRO display. */ function azlinkpro_clicks_enqueue() { static $done = false; if ($done) { return; } $done = true; wp_enqueue_script('azlinkpro-clicks', plugin_dir_url(AZLINKPRO_FILE) . 'assets/click-tracker.js', array(), AZLINKPRO_VERSION, true); wp_localize_script('azlinkpro-clicks', 'azlinkproClicks', array( 'url' => esc_url_raw(rest_url('azlinkpro/v1/click')), 'page' => is_singular() ? (int) get_queried_object_id() : 0, )); } /* ------------------------------------------------------------------ Counting */ function azlinkpro_clicks_routes() { register_rest_route('azlinkpro/v1', '/click', array( 'methods' => 'POST', 'callback' => 'azlinkpro_clicks_record', // Visitors are not logged in. Every value is checked below, and the only effect is +1 on a counter. 'permission_callback' => '__return_true', 'args' => array( 'src_type' => array('type' => 'string', 'required' => true, 'enum' => array_keys(azlinkpro_clicks_types())), 'src_id' => array('type' => 'integer', 'default' => 0, 'minimum' => 0), 'page_id' => array('type' => 'integer', 'default' => 0, 'minimum' => 0), 'asin' => array('type' => 'string', 'default' => '', 'pattern' => '^([A-Za-z0-9]{10})?$'), 'spot' => array('type' => 'string', 'default' => 'title', 'enum' => array('button', 'image', 'title')), ), )); } add_action('rest_api_init', 'azlinkpro_clicks_routes'); /** Search engines and link checkers are not visitors. */ function azlinkpro_clicks_is_bot($agent) { return $agent === '' || (bool) preg_match('/bot|crawl|spider|slurp|preview|headless|monitor|checker|curl|wget|python|httpclient/i', $agent); } function azlinkpro_clicks_record(WP_REST_Request $request) { $settings = azlinkpro_clicks_settings(); $agent = (string) $request->get_header('user_agent'); if (!$settings['on'] || azlinkpro_clicks_is_bot($agent)) { return new WP_REST_Response(null, 204); } $types = azlinkpro_clicks_types(); $type = (string) $request['src_type']; $src_id = absint($request['src_id']); // The display must exist and be the right kind; 0 means a grid or display without a saved item. if ($src_id && get_post_type($src_id) !== $types[$type][0]) { $src_id = 0; } $page_id = absint($request['page_id']); if ($page_id && get_post_status($page_id) !== 'publish') { $page_id = 0; } $asin = strtoupper((string) $request['asin']); azlinkpro_clicks_install(); global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- one counter row per day; nothing to cache. $wpdb->query($wpdb->prepare( 'INSERT INTO %i (day, page_id, src_type, src_id, asin, spot, clicks) VALUES (%s, %d, %s, %d, %s, %s, 1) ON DUPLICATE KEY UPDATE clicks = clicks + 1', azlinkpro_clicks_table(), wp_date('Y-m-d'), $page_id, $type, $src_id, $asin, (string) $request['spot'] )); return new WP_REST_Response(null, 204); } /* ------------------------------------------------------------------ Totals */ /** Y-m-d for "N days ago" in the site's time zone (0 = today). */ function azlinkpro_clicks_day($days_ago) { $today = new DateTimeImmutable('today', wp_timezone()); return $today->modify('-' . absint($days_ago) . ' days')->format('Y-m-d'); } /** Total clicks from $from to $to (Y-m-d, inclusive). */ function azlinkpro_clicks_total($from, $to) { if (get_option('azlinkpro_clicks_db') !== AZLINKPRO_CLICKS_DB_VERSION) { return 0; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin-only total of the plugin's own table. return (int) $wpdb->get_var($wpdb->prepare('SELECT SUM(clicks) FROM %i WHERE day BETWEEN %s AND %s', azlinkpro_clicks_table(), $from, $to)); } /** "+12%", "−5%", "new" or "" comparing two totals. */ function azlinkpro_clicks_change($now, $before) { if ($before <= 0) { return $now > 0 ? 'new' : ''; } $pct = (int) round(($now - $before) * 100 / $before); return ($pct > 0 ? '+' : ($pct < 0 ? '−' : '')) . number_format_i18n(abs($pct)) . '%'; } /* ------------------------------------------------------------------ Admin */ function azlinkpro_clicks_menu() { add_submenu_page('azlinkpro', 'Link Clicks', 'Link Clicks', 'edit_posts', 'azlinkpro-clicks', 'azlinkpro_clicks_page'); } add_action('admin_menu', 'azlinkpro_clicks_menu', 19); function azlinkpro_clicks_assets($hook) { if ($hook !== 'azlinkpro_page_azlinkpro-clicks' && $hook !== 'toplevel_page_azlinkpro') { return; } wp_enqueue_style('azlinkpro-clicks-admin', plugin_dir_url(AZLINKPRO_FILE) . 'assets/clicks.css', array(), AZLINKPRO_VERSION); } add_action('admin_enqueue_scripts', 'azlinkpro_clicks_assets'); /** Dashboard card: clicks in the last 30 days, every plan. */ function azlinkpro_clicks_dashboard_metric() { $settings = azlinkpro_clicks_settings(); $now = azlinkpro_clicks_total(azlinkpro_clicks_day(29), azlinkpro_clicks_day(0)); $before = azlinkpro_clicks_total(azlinkpro_clicks_day(59), azlinkpro_clicks_day(30)); $change = azlinkpro_clicks_change($now, $before); ?>

Link Clicks

vs. the 30 days before

Link Clicks

Counts clicks on the Amazon links in your Tables, Showcases, Pros & Cons cards and Amazon Images. Counts stay on this site: no visitor's IP address or cookie is stored.

Click settings saved.

Click counting is turned off, so new clicks are not counted. Turn it on below.

Last 30 days
Last 7 days

See which products and pages earn your clicks

The full click report is included in the plans:

Your clicks are already being counted, so the full report shows your history from day one.

See plans

Settings


Leave this off so your own testing doesn't inflate the numbers. Turn it on for a moment to check that counting works.

!empty($_POST['on']) ? 1 : 0, 'editors' => !empty($_POST['editors']) ? 1 : 0), false); wp_safe_redirect(admin_url('admin.php?page=azlinkpro-clicks&saved=1')); exit; } add_action('admin_post_azlinkpro_clicks_settings', 'azlinkpro_clicks_save_settings');