$links * @return array */ function add_plugin_action_links(array $links): array { $settings_link = sprintf( '%s', esc_url(admin_url(SETTINGS_PAGE)), esc_html__('Settings', 'admin-views-column-for-matomo') ); $links['admivico_settings'] = $settings_link; return $links; } /** * @param array $links * @return array */ function open_author_link_in_new_tab(array $links, string $file): array { if ($file !== plugin_basename(__FILE__)) { return $links; } foreach ($links as $key => $link) { if (strpos($link, 'https://www.pushaune.com/') === false) { continue; } $links[$key] = str_replace( ', use_old_slugs: bool} */ function get_plugin_options(): array { $defaults = [ 'post_types' => ['post', 'page'], 'period' => 'last30', 'metric' => 'nb_hits', 'cache_ttl' => 3600, 'source' => 'local', 'remote_url' => '', 'remote_site_id' => 1, 'remote_token' => '', 'redirect_rules' => [], 'use_old_slugs' => true, ]; $options = get_option(OPTION_NAME, []); if (!is_array($options)) { return $defaults; } $options = array_merge($defaults, $options); $options['post_types'] = array_values(array_filter(array_map('sanitize_key', (array) $options['post_types']))); $options['period'] = sanitize_key((string) $options['period']); $options['metric'] = sanitize_key((string) $options['metric']); $options['cache_ttl'] = max(300, absint($options['cache_ttl'])); $options['source'] = $options['source'] === 'remote' ? 'remote' : 'local'; $options['remote_url'] = esc_url_raw(trim((string) $options['remote_url'])); $options['remote_site_id'] = absint($options['remote_site_id']); $options['remote_token'] = trim((string) $options['remote_token']); $options['redirect_rules'] = sanitize_redirect_rules($options['redirect_rules']); $options['use_old_slugs'] = !empty($options['use_old_slugs']); return $options; } /** * @param mixed $rules * @return array */ function sanitize_redirect_rules($rules): array { if (!is_array($rules)) { return []; } $clean = []; foreach ($rules as $rule) { if (!is_array($rule)) { continue; } $from = isset($rule['from']) && is_scalar($rule['from']) ? trim(sanitize_text_field((string) $rule['from'])) : ''; $to = isset($rule['to']) && is_scalar($rule['to']) ? trim(sanitize_text_field((string) $rule['to'])) : ''; if ($from === '' || $to === '') { continue; } $clean[] = ['from' => $from, 'to' => $to]; } return $clean; } /** * Cache key component identifying where the stats come from, so switching * source settings never serves stale data from another Matomo instance. * * @param array{source: string, remote_url: string, remote_site_id: int} $options * @return array{source: string, remote_url: string, remote_site_id: int} */ function get_source_fingerprint(array $options): array { return [ 'source' => $options['source'], 'remote_url' => $options['source'] === 'remote' ? $options['remote_url'] : '', 'remote_site_id' => $options['source'] === 'remote' ? $options['remote_site_id'] : 0, ]; } function register_admin_columns(): void { if (!is_admin()) { return; } add_filter('manage_posts_columns', __NAMESPACE__ . '\\add_views_column'); add_filter('manage_pages_columns', __NAMESPACE__ . '\\add_views_column'); add_filter('manage_edit-post_sortable_columns', __NAMESPACE__ . '\\add_sortable_views_column'); add_filter('manage_edit-page_sortable_columns', __NAMESPACE__ . '\\add_sortable_views_column'); add_action('manage_posts_custom_column', __NAMESPACE__ . '\\render_views_column', 10, 2); add_action('manage_pages_custom_column', __NAMESPACE__ . '\\render_views_column', 10, 2); foreach (get_column_post_types() as $post_type) { add_filter("manage_{$post_type}_posts_columns", __NAMESPACE__ . '\\add_views_column'); add_filter("manage_edit-{$post_type}_sortable_columns", __NAMESPACE__ . '\\add_sortable_views_column'); add_action("manage_{$post_type}_posts_custom_column", __NAMESPACE__ . '\\render_views_column', 10, 2); } } function register_current_screen_columns(\WP_Screen $screen): void { if ($screen->base !== 'edit' || empty($screen->post_type)) { return; } $options = get_plugin_options(); if (!in_array($screen->post_type, $options['post_types'], true)) { return; } add_filter("manage_{$screen->post_type}_posts_columns", __NAMESPACE__ . '\\add_views_column'); add_filter("manage_edit-{$screen->post_type}_sortable_columns", __NAMESPACE__ . '\\add_sortable_views_column'); add_action("manage_{$screen->post_type}_posts_custom_column", __NAMESPACE__ . '\\render_views_column', 10, 2); } /** * @return string[] */ function get_supported_post_types(): array { $objects = get_post_types([], 'objects'); $post_types = []; foreach ($objects as $post_type => $object) { if ($post_type === 'attachment') { continue; } if (!empty($object->show_ui)) { $post_types[] = $post_type; } } return array_values($post_types); } /** * @return string[] */ function get_column_post_types(): array { $options = get_plugin_options(); return array_values(array_unique(array_merge(get_supported_post_types(), $options['post_types']))); } /** * @param array $columns * @return array */ function add_views_column(array $columns): array { $options = get_plugin_options(); $screen = get_current_screen(); if (!$screen || !in_array($screen->post_type, $options['post_types'], true)) { return $columns; } if (array_key_exists(COLUMN_KEY, $columns)) { return $columns; } $new_columns = []; foreach ($columns as $key => $label) { if ($key === 'date') { $new_columns[COLUMN_KEY] = __('Matomo Views', 'admin-views-column-for-matomo'); } $new_columns[$key] = $label; } if (!array_key_exists(COLUMN_KEY, $new_columns)) { $new_columns[COLUMN_KEY] = __('Matomo Views', 'admin-views-column-for-matomo'); } return $new_columns; } /** * @param array $columns * @return array */ function add_sortable_views_column(array $columns): array { $options = get_plugin_options(); $screen = get_current_screen(); if (!$screen || !in_array($screen->post_type, $options['post_types'], true)) { return $columns; } $columns[COLUMN_KEY] = COLUMN_KEY; return $columns; } function prepare_matomo_views_sorting(\WP_Query $query): void { if (!is_admin() || !$query->is_main_query()) { return; } $orderby = $query->get('orderby'); if ($orderby !== COLUMN_KEY) { return; } $post_type = get_query_post_type($query); $options = get_plugin_options(); if ($post_type === '' || !in_array($post_type, $options['post_types'], true)) { return; } sync_post_type_views_meta($post_type); $query->set('meta_key', META_KEY); $query->set('orderby', 'meta_value_num'); $query->set('order', strtoupper((string) $query->get('order')) === 'ASC' ? 'ASC' : 'DESC'); } function get_query_post_type(\WP_Query $query): string { $post_type = $query->get('post_type'); if (is_string($post_type) && $post_type !== '') { return $post_type; } if (is_array($post_type) && count($post_type) === 1 && is_string(reset($post_type))) { return (string) reset($post_type); } return 'post'; } function sync_post_type_views_meta(string $post_type): void { $options = get_plugin_options(); $sync_key = CACHE_PREFIX . 'synced_' . md5(wp_json_encode([ 'post_type' => $post_type, 'period' => $options['period'], 'metric' => $options['metric'], 'home' => home_url('/'), 'src' => get_source_fingerprint($options), 'history' => [$options['redirect_rules'], $options['use_old_slugs']], ])); if (get_transient($sync_key)) { return; } $stats = get_matomo_url_stats(); if ($stats === null) { return; } $post_ids = get_posts([ 'post_type' => $post_type, 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ]); foreach ($post_ids as $post_id) { $value = get_post_views_from_stats((int) $post_id, $stats); update_post_meta((int) $post_id, META_KEY, $value); } set_plugin_transient($sync_key, '1', $options['cache_ttl']); } function render_views_column(string $column, int $post_id): void { if ($column !== COLUMN_KEY) { return; } static $rendered = []; $render_key = $post_id . ':' . $column; if (isset($rendered[$render_key])) { return; } $rendered[$render_key] = true; $value = get_post_views($post_id); if ($value === null) { echo '—'; return; } echo esc_html(number_format_i18n($value)); } function get_post_views(int $post_id): ?int { $stats = get_matomo_url_stats(); if ($stats !== null) { return get_post_views_from_stats($post_id, $stats); } return null; } /** * @param array $stats */ function get_post_views_from_stats(int $post_id, array $stats): int { $url = get_permalink($post_id); if (!$url) { return 0; } $current = null; $counted_keys = []; foreach (get_url_lookup_keys($url) as $key) { $counted_keys[$key] = true; if ($current === null && isset($stats[$key])) { $current = $stats[$key]; } } if ($current === null) { $current = get_single_url_views($url); } $historical = 0; foreach (get_historical_urls($post_id, $url) as $old_url) { $old_keys = []; $overlaps = false; foreach (get_url_lookup_keys($old_url) as $key) { if (isset($counted_keys[$key])) { $overlaps = true; } $old_keys[] = $key; } if ($overlaps) { continue; } foreach ($old_keys as $key) { $counted_keys[$key] = true; if (isset($stats[$key])) { $historical += $stats[$key]; break; } } } return (int) $current + $historical; } /** * URLs that previously pointed to this post (old slugs, 301 redirect rules) * and whose Matomo statistics should be added to the post count. * * @return string[] */ function get_historical_urls(int $post_id, string $current_url): array { $options = get_plugin_options(); $urls = []; if ($options['use_old_slugs']) { $post = get_post($post_id); if ($post && $post->post_name !== '') { foreach (get_post_meta($post_id, '_wp_old_slug', false) as $old_slug) { if (!is_string($old_slug) || $old_slug === '' || $old_slug === $post->post_name) { continue; } $urls[] = str_replace( '/' . $post->post_name . '/', '/' . $old_slug . '/', trailingslashit($current_url) ); } } } foreach ($options['redirect_rules'] as $rule) { $old_url = reverse_redirect_rule($current_url, $rule['from'], $rule['to']); if ($old_url !== null) { $urls[] = $old_url; } } /** * Filters the historical URLs whose Matomo statistics are added to a post's count. * * @param string[] $urls * @param int $post_id * @param string $current_url */ $urls = apply_filters('admivico_url_candidates', $urls, $post_id, $current_url); return array_values(array_unique(array_filter(array_map('strval', (array) $urls)))); } /** * Rebuild the old URL of a post from an "old structure => new structure" * redirect rule, by matching the rule backwards against the current * permalink (full URL or path). "*" captures the unchanged part of the path. */ function reverse_redirect_rule(string $current_url, string $from, string $to): ?string { $targets = [$current_url]; $path = wp_parse_url($current_url, PHP_URL_PATH); if (is_string($path) && $path !== '' && $path !== $current_url) { $targets[] = $path; } $star_pos = strpos($to, '*'); foreach ($targets as $target) { if ($star_pos === false) { if (untrailingslashit($target) === untrailingslashit($to)) { return $from; } continue; } $prefix = substr($to, 0, $star_pos); $suffix = substr($to, $star_pos + 1); if (strlen($target) < strlen($prefix) + strlen($suffix)) { continue; } if ($prefix !== '' && strpos($target, $prefix) !== 0) { continue; } if ($suffix !== '' && substr($target, -strlen($suffix)) !== $suffix) { continue; } $capture = substr($target, strlen($prefix), strlen($target) - strlen($prefix) - strlen($suffix)); return str_replace('*', (string) $capture, $from); } return null; } /** * @return array|null */ function get_matomo_url_stats(): ?array { static $stats = null; static $loaded = false; if ($loaded) { return $stats; } $loaded = true; $options = get_plugin_options(); $cache_key = CACHE_PREFIX . md5(wp_json_encode([ 'period' => $options['period'], 'metric' => $options['metric'], 'home' => home_url('/'), 'src' => get_source_fingerprint($options), ])); $cached = get_transient($cache_key); if (is_array($cached)) { $stats = $cached; return $stats; } $rows = request_matomo_rows($options['period']); if ($rows === null) { return null; } $map = build_url_stats_map($rows, $options['metric']); set_plugin_transient($cache_key, $map, $options['cache_ttl']); $stats = $map; return $stats; } function get_single_url_views(string $url): ?int { $options = get_plugin_options(); $cache_key = CACHE_PREFIX . 'single_' . md5(wp_json_encode([ 'period' => $options['period'], 'metric' => $options['metric'], 'url' => $url, 'src' => get_source_fingerprint($options), ])); $cached = get_transient($cache_key); if ($cached !== false) { return is_numeric($cached) ? absint($cached) : null; } $best_value = null; foreach (get_url_api_candidates($url) as $candidate_url) { $rows = request_matomo_rows($options['period'], 'getPageUrl', ['pageUrl' => $candidate_url]); if ($rows === null) { continue; } $value = sum_metric_from_rows($rows, $options['metric']); $best_value = max($best_value ?? 0, $value); } if ($best_value === null) { set_plugin_transient($cache_key, 'miss', min($options['cache_ttl'], 900)); return null; } set_plugin_transient($cache_key, $best_value, $options['cache_ttl']); return $best_value; } /** * @param array $rows * @return array */ function build_url_stats_map(array $rows, string $metric): array { $map = []; foreach (flatten_matomo_rows($rows) as $row) { $row_urls = get_row_url_candidates($row); if (!$row_urls) { continue; } $value = isset($row[$metric]) ? absint($row[$metric]) : 0; foreach ($row_urls as $row_url) { foreach (get_url_lookup_keys($row_url) as $key) { $map[$key] = ($map[$key] ?? 0) + $value; } } } return $map; } /** * @param array $rows */ function sum_metric_from_rows(array $rows, string $metric): int { $total = 0; foreach (flatten_matomo_rows($rows) as $row) { $total += isset($row[$metric]) ? absint($row[$metric]) : 0; } return $total; } /** * @param array $rows * @return array> */ function flatten_matomo_rows(array $rows): array { $flat = []; foreach ($rows as $row) { if (!is_array($row)) { continue; } $flat[] = $row; foreach (['subtable', 'idsubdatatable', 'children'] as $child_key) { if (isset($row[$child_key]) && is_array($row[$child_key])) { $flat = array_merge($flat, flatten_matomo_rows($row[$child_key])); } } } return $flat; } /** * @param array $row * @return string[] */ function get_row_url_candidates(array $row): array { $candidates = []; foreach (['url', 'label', 'segment', 'idsubdatatable_in_db'] as $key) { if (isset($row[$key]) && is_scalar($row[$key])) { $value = trim((string) $row[$key]); if ($value !== '' && looks_like_page_url($value)) { $candidates[] = $value; } } } return array_values(array_unique($candidates)); } function looks_like_page_url(string $value): bool { return strpos($value, 'http://') === 0 || strpos($value, 'https://') === 0 || strpos($value, '/') === 0; } /** * @param array $extra_params * @return array|null */ function request_matomo_rows(string $period, string $api_action = 'getPageUrls', array $extra_params = []): ?array { $options = get_plugin_options(); if ($options['source'] === 'remote') { return request_remote_matomo_rows($period, $api_action, $extra_params, $options); } if (!class_exists('\WP_REST_Request')) { return null; } $request = new \WP_REST_Request('GET', '/matomo/v1/api/processed_report'); $range = matomo_period_params($period); $request->set_query_params(array_merge([ 'period' => $range['period'], 'date' => $range['date'], 'apiModule' => 'Actions', 'apiAction' => $api_action, 'flat' => '1', 'expanded' => '1', 'filter_limit' => '-1', ], $extra_params)); $response = rest_do_request($request); if ($response->is_error()) { return null; } $data = $response->get_data(); if (isset($data['reportData']) && is_array($data['reportData'])) { return $data['reportData']; } if (isset($data['data']) && is_array($data['data'])) { return $data['data']; } if (array_is_list_compat($data)) { return $data; } return null; } /** * Query the Reporting API of a remote Matomo instance over HTTP. * * The token is sent in the POST body so it never appears in server logs. * * @param array $extra_params * @param array{period: string, remote_url: string, remote_site_id: int, remote_token: string} $options * @return array|null */ function request_remote_matomo_rows(string $period, string $api_action, array $extra_params, array $options): ?array { if ($options['remote_url'] === '' || $options['remote_site_id'] < 1 || $options['remote_token'] === '') { return null; } $range = matomo_period_params($period); $response = wp_remote_post( trailingslashit($options['remote_url']) . 'index.php', [ 'timeout' => 15, 'body' => array_merge([ 'module' => 'API', 'method' => 'Actions.' . $api_action, 'idSite' => (string) $options['remote_site_id'], 'period' => $range['period'], 'date' => $range['date'], 'format' => 'JSON', 'flat' => '1', 'expanded' => '1', 'filter_limit' => '-1', 'token_auth' => $options['remote_token'], ], $extra_params), ] ); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return null; } $data = json_decode(wp_remote_retrieve_body($response), true); if (!is_array($data)) { return null; } if (isset($data['result']) && $data['result'] === 'error') { return null; } return array_is_list_compat($data) ? $data : null; } /** * @return string[] */ function get_url_api_candidates(string $url): array { $candidates = [$url, rtrim($url, '/')]; $parsed = wp_parse_url($url); $path = isset($parsed['path']) ? '/' . ltrim((string) $parsed['path'], '/') : ''; if ($path !== '') { $candidates[] = $path; $candidates[] = rtrim($path, '/'); $candidates[] = home_url($path); $candidates[] = rtrim(home_url($path), '/'); } return array_values(array_unique(array_filter($candidates))); } /** * @return array{period: string, date: string} */ function matomo_period_params(string $period): array { switch ($period) { case 'today': return ['period' => 'day', 'date' => 'today']; case 'yesterday': return ['period' => 'day', 'date' => 'yesterday']; case 'last7': return ['period' => 'range', 'date' => 'last7']; case 'last90': return ['period' => 'range', 'date' => 'last90']; case 'year': return ['period' => 'year', 'date' => 'today']; case 'last365': return ['period' => 'range', 'date' => 'last365']; case 'all': return ['period' => 'range', 'date' => '2000-01-01,today']; case 'last30': default: return ['period' => 'range', 'date' => 'last30']; } } /** * @return string[] */ function get_url_lookup_keys(string $url): array { $url = html_entity_decode($url, ENT_QUOTES); $parsed = wp_parse_url($url); $path = isset($parsed['path']) ? (string) $parsed['path'] : ''; if ($path === '' && strpos($url, '/') === 0) { $path = $url; } $host = isset($parsed['host']) ? strtolower((string) $parsed['host']) : ''; $scheme = isset($parsed['scheme']) ? strtolower((string) $parsed['scheme']) : ''; $home_host = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST)); $path = '/' . ltrim($path, '/'); $path_no_slash = rtrim($path, '/') ?: '/'; $keys = [ normalize_lookup_key($path), normalize_lookup_key($path_no_slash), ]; if ($host !== '') { $authority = $host; if (isset($parsed['port'])) { $authority .= ':' . (string) $parsed['port']; } $full = ($scheme ?: 'https') . '://' . $authority . $path; $keys[] = normalize_lookup_key($full); $keys[] = normalize_lookup_key(rtrim($full, '/')); } if ($home_host !== '' && $host === '') { $full = home_url($path); $keys[] = normalize_lookup_key($full); $keys[] = normalize_lookup_key(rtrim($full, '/')); } return array_values(array_unique(array_filter($keys))); } function normalize_lookup_key(string $value): string { $value = strtolower(rawurldecode($value)); $value = strtok($value, '?') ?: $value; $value = strtok($value, '#') ?: $value; return rtrim($value, '/') ?: '/'; } /** * @param mixed $value */ function array_is_list_compat($value): bool { if (!is_array($value)) { return false; } $index = 0; foreach (array_keys($value) as $key) { if ($key !== $index++) { return false; } } return true; } function register_settings_page(): void { add_options_page( __('Matomo Columns', 'admin-views-column-for-matomo'), __('Matomo Columns', 'admin-views-column-for-matomo'), 'manage_options', 'admivico', __NAMESPACE__ . '\\render_settings_page' ); } function enqueue_settings_assets(string $hook_suffix): void { if ($hook_suffix !== 'settings_page_admivico') { return; } wp_enqueue_script( 'admivico-settings', plugins_url('js/admin.js', __FILE__), [], '1.2.0', true ); wp_localize_script( 'admivico-settings', 'admivicoSettings', [ 'optionName' => OPTION_NAME, 'removeLabel' => __('Remove rule', 'admin-views-column-for-matomo'), /* translators: %d: number of redirect rules imported from the CSV file. */ 'csvAdded' => __('%d rules added from the CSV file. Remember to save your changes.', 'admin-views-column-for-matomo'), 'csvEmpty' => __('No valid rules found in the file. Expected two columns: old URL, new URL.', 'admin-views-column-for-matomo'), ] ); } function register_settings(): void { register_setting( 'admivico_settings', OPTION_NAME, [ 'type' => 'array', 'sanitize_callback' => __NAMESPACE__ . '\\sanitize_options', 'default' => [ 'post_types' => ['post', 'page'], 'period' => 'last30', 'metric' => 'nb_hits', 'cache_ttl' => 3600, 'source' => 'local', 'remote_url' => '', 'remote_site_id' => 1, 'remote_token' => '', 'redirect_rules' => [], 'use_old_slugs' => true, ], 'show_in_rest' => false, ] ); } /** * @param mixed $input * @return array{post_types: string[], period: string, metric: string, cache_ttl: int, source: string, remote_url: string, remote_site_id: int, remote_token: string, redirect_rules: array, use_old_slugs: bool} */ function sanitize_options($input): array { $input = is_array($input) ? $input : []; $defaults = [ 'post_types' => ['post', 'page'], 'period' => 'last30', 'metric' => 'nb_hits', 'cache_ttl' => 3600, 'source' => 'local', 'remote_url' => '', 'remote_site_id' => 1, 'remote_token' => '', 'redirect_rules' => [], 'use_old_slugs' => true, ]; $supported_post_types = get_supported_post_types(); $requested_post_types = []; if (isset($input['post_types']) && is_array($input['post_types'])) { foreach ($input['post_types'] as $post_type) { if (!is_scalar($post_type)) { continue; } $requested_post_types[] = sanitize_key((string) $post_type); } } $post_types = array_values(array_intersect($requested_post_types, $supported_post_types)); $periods = array_keys(get_period_choices()); $metrics = array_keys(get_metric_choices()); $period = isset($input['period']) && is_scalar($input['period']) ? sanitize_key((string) $input['period']) : $defaults['period']; $metric = isset($input['metric']) && is_scalar($input['metric']) ? sanitize_key((string) $input['metric']) : $defaults['metric']; $cache_ttl = isset($input['cache_ttl']) && is_scalar($input['cache_ttl']) ? absint($input['cache_ttl']) : $defaults['cache_ttl']; $source = isset($input['source']) && $input['source'] === 'remote' ? 'remote' : 'local'; $remote_url = isset($input['remote_url']) && is_scalar($input['remote_url']) ? esc_url_raw(trim((string) $input['remote_url'])) : ''; $remote_site_id = isset($input['remote_site_id']) && is_scalar($input['remote_site_id']) ? absint($input['remote_site_id']) : $defaults['remote_site_id']; $remote_token = isset($input['remote_token']) && is_scalar($input['remote_token']) ? trim((string) $input['remote_token']) : ''; $redirect_rules = sanitize_redirect_rules($input['redirect_rules'] ?? null); $use_old_slugs = !empty($input['use_old_slugs']); return [ 'post_types' => $post_types ?: $defaults['post_types'], 'period' => in_array($period, $periods, true) ? $period : $defaults['period'], 'metric' => in_array($metric, $metrics, true) ? $metric : $defaults['metric'], 'cache_ttl' => max(300, $cache_ttl), 'source' => $source, 'remote_url' => $remote_url, 'remote_site_id' => max(1, $remote_site_id), 'remote_token' => $remote_token, 'redirect_rules' => $redirect_rules, 'use_old_slugs' => $use_old_slugs, ]; } /** * @return array */ function get_period_choices(): array { return [ 'today' => __('Today', 'admin-views-column-for-matomo'), 'yesterday' => __('Yesterday', 'admin-views-column-for-matomo'), 'last7' => __('Last 7 days', 'admin-views-column-for-matomo'), 'last30' => __('Last 30 days', 'admin-views-column-for-matomo'), 'last90' => __('Last 90 days', 'admin-views-column-for-matomo'), 'last365' => __('Last 365 days', 'admin-views-column-for-matomo'), 'year' => __('Current year', 'admin-views-column-for-matomo'), 'all' => __('All time', 'admin-views-column-for-matomo'), ]; } /** * @return array */ function get_metric_choices(): array { return [ 'nb_hits' => __('Page views', 'admin-views-column-for-matomo'), 'nb_visits' => __('Unique views', 'admin-views-column-for-matomo'), ]; } function render_settings_page(): void { if (!current_user_can('manage_options')) { return; } $options = get_plugin_options(); $post_types = get_supported_post_types(); ?>

> > >