WordPress 接入 Microsoft Clarity Cookie 同意与偏好设置

Microsoft Clarity 自 2025 年 10 月 31 日起,对来自欧洲经济区(EEA)、英国和瑞士的访问执行 Cookie 同意信号要求。没有有效同意时,跨页面会话关联等统计能力会受到限制。为 WordPress 添加提示条时,需要把访客的选择真正传给 Clarity,而不只是把弹窗隐藏。参见 微软的无 Cookie 同意报告说明。

Cookie 同意的作用

Clarity 通过 consentv2 接收两类独立信号:analytics_Storage 控制分析存储同意,ad_Storage 控制广告相关存储同意。每项值只能使用 granted 或 denied,参数名的大小写也要保持一致。没有有效选择时,两项均为 denied。这两项是 Clarity API 参数,不要直接套用其他统计产品的参数命名。

拒绝 Cookie 后,Clarity 的无同意模式仍可能发送有限的统计数据;无 Cookie 不等于不采集,也不能直接称为完全匿名。本教程实现的是 Cookie 同意模式。如果网站要求用户同意前完全不向微软发送请求,需要另行采用阻止 Clarity 加载的集成方案。ConsentV2 官方文档说明了允许、拒绝和撤回后的处理。

提示条和偏好设置

插件保留底部提示条、独立偏好弹窗、必要功能说明、隐私政策链接和移动端适配。访客可直接选择“接受全部”“管理设置”或“全部拒绝”;保存后仍能通过页面角落的“Cookie 设置”重新打开偏好,关闭选项并保存,或全部拒绝来撤回同意。

  • 接受全部:两类信号都发送 granted
  • 全部拒绝:两类信号都发送 denied
  • 管理设置:分析和广告两项分别发送对应状态
  • 取消、按 Esc 或点击弹窗外部:只关闭设置,不改变已保存的选择
分析与广告 Cookie 偏好设置示例

让选择真正生效

提示条的文案可以按网站风格调整,选择本身必须由访客决定。插件先在本地读取有效偏好并建立 Clarity 调用队列,再插入官方跟踪脚本。脚本尚未下载完成时发生多次选择,只保留最新的同意状态;加载完成和偏好变更时再次发送当前状态。

偏好保存在当前站点的 localStorage,默认有效期为 180 天。代码会检查记录格式、项目 ID、政策版本和时间戳;记录失效后恢复为拒绝状态并重新显示提示。其他同源标签页的修改、清除存储以及从浏览器往返缓存恢复页面时也会同步。浏览器禁止保存时,本页仍可应用选择,并显示无法保存的提示。

同意信号不能代替完整的隐私处理。需要结合网站实际使用的服务、数据用途和访问地区配置政策。这里的广告开关只传递 Clarity 的广告存储信号,不会自动控制 Google Analytics、广告联盟或其他插件的脚本。

同意设置主题插图

配置 Clarity 和 WordPress

  1. 在 Clarity 的项目设置中取得自己的项目 ID,不要使用示例值。
  2. 进入 Clarity 项目的 Settings → Setup,把默认写入 Cookie 的开关设为 OFF,启用需要同意才写 Cookie 的行为。不要把“打开同意模式”理解成开启默认写 Cookie。具体入口参见 Consent Mode 官方说明。
  3. 只保留一个 Clarity 加载入口。移除或停用主题、GTM、其他插件中的重复 Clarity 代码,让下面的插件负责加载。如果已有 CMP,优先在同一个 CMP 内接入,不要叠加两套互相覆盖的同意逻辑。
  4. 在 WordPress 的“设置 → 隐私”中指定已发布的隐私政策页面,并按实际行为说明 Cookie、有限统计、数据用途及撤回方式。
  5. 在 wp-content/plugins/site-clarity-consent/ 下建立下面三个文件;PHP 文件、JavaScript 文件与 CSS 文件放在同一级目录。
  6. 在 wp-config.php 的结束注释之前设置项目 ID,再到插件页面启用。项目 ID 缺失、格式无效或隐私政策页面未配置时,插件不加载 Clarity,并在管理后台给出配置提示。
define('SITE_CLARITY_PROJECT_ID', '替换为你的项目ID');

安装环境使用 WordPress 6.3 或更新版本、PHP 7.4 或更新版本,以及支持原生 <dialog> 的现代浏览器。生产环境还应使用仍受维护的 WordPress 和 PHP 版本。缓存、合并、延迟执行插件要排除本插件脚本,避免打乱“配置 → 同意初始化 → Clarity”的顺序。使用 CSP 时,需要按自己的策略允许本地脚本、WordPress 输出的配置脚本及 Clarity 所需资源。

配置步骤配图

完整插件代码

三份文件共同组成插件,不要把 PHP 或跟踪脚本直接粘贴到文章编辑器中执行。提示条文案和颜色可在对应文件中调整。用途或隐私政策发生需要重新征询的变化时,修改 PHP 配置中的 policyVersion;更换项目 ID 也会使旧偏好失效。

site-clarity-consent.php

<?php
/**
 * Plugin Name: Site Clarity Consent
 * Description: Cookie 偏好设置与 Microsoft Clarity ConsentV2 集成。
 * Version: 2.0.0
 * Requires at least: 6.3
 * Requires PHP: 7.4
 * License: GPL-3.0-only
 */

if (!defined('ABSPATH')) { exit; }

// 在 wp-config.php 中设置:define('SITE_CLARITY_PROJECT_ID', '你的项目ID');
function scc_project_id() {
    $id = defined('SITE_CLARITY_PROJECT_ID') ? (string) SITE_CLARITY_PROJECT_ID : '';
    return preg_match('/^[a-z0-9]+$/i', $id) ? $id : '';
}

function scc_ready() {
    return scc_project_id() !== '' && get_privacy_policy_url() !== '';
}

add_action('admin_notices', function () {
    if (!current_user_can('manage_options') || scc_ready()) { return; }
    echo '<div class="notice notice-info"><p>Site Clarity Consent:请先配置 SITE_CLARITY_PROJECT_ID,并在“设置 → 隐私”中选择隐私政策页面。配置完成前不加载 Clarity。</p></div>';
});

add_action('wp_enqueue_scripts', function () {
    if (!scc_ready()) { return; }
    $base = plugin_dir_url(__FILE__);
    wp_enqueue_style('site-clarity-consent', $base . 'consent.css', array(), '2.0.0');
    // 放在 head 中,不设置 async/defer:先建立同意状态,再由 JS 加载 Clarity。
    wp_enqueue_script('site-clarity-consent', $base . 'consent.js', array(), '2.0.0', false);
    wp_add_inline_script('site-clarity-consent', 'window.SiteClarityConfig = ' . wp_json_encode(array(
        'projectId' => scc_project_id(),
        'policyVersion' => '1', // 用途、供应商或政策变更时修改此值,重新征询。
        'retentionDays' => 180,
    ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ';', 'before');
}, 1);

add_action('wp_footer', function () {
    if (!scc_ready()) { return; }
    ?>
    <section id="scc-banner" class="scc-banner" aria-label="Cookie 偏好提示" hidden>
      <p>本站使用 Microsoft Clarity 分析访问体验。你可以接受、拒绝或分别设置 Cookie。拒绝后仍可能进行无 Cookie 的有限统计。详情见 <a href="<?php echo esc_url(get_privacy_policy_url()); ?>">隐私政策</a>。</p>
      <div class="scc-actions">
        <button type="button" data-scc-action="reject">全部拒绝</button>
        <button type="button" data-scc-action="manage">管理设置</button>
        <button type="button" data-scc-action="accept">接受全部</button>
      </div>
    </section>
    <button type="button" id="scc-reopen" class="scc-reopen" data-scc-action="manage" hidden>Cookie 设置</button>
    <dialog id="scc-dialog" class="scc-dialog" aria-labelledby="scc-title" aria-describedby="scc-description">
      <h2 id="scc-title">Cookie 偏好设置</h2>
      <p id="scc-description">必要功能保持启用。下面两项只控制 Clarity 的 Cookie 同意信号,不控制其他统计或广告脚本。</p>
      <label><input type="checkbox" checked disabled> 必要功能(始终启用)</label>
      <label><input type="checkbox" id="scc-analytics"> 分析 Cookie(跨页、跨会话统计)</label>
      <label><input type="checkbox" id="scc-ads"> 广告相关 Cookie</label>
      <p>可随时关闭选项并保存,或点“全部拒绝”撤回同意。</p>
      <div class="scc-actions">
        <button type="button" data-scc-action="close">取消</button>
        <button type="button" data-scc-action="reject">全部拒绝</button>
        <button type="button" data-scc-action="save">保存设置</button>
      </div>
    </dialog>
    <p id="scc-status" class="scc-status" role="status" aria-live="polite" hidden></p>
    <?php
});

consent.js

(function (w, d) {
  'use strict';
  if (w.SiteCookieConsent) { return; }
  var cfg = w.SiteClarityConfig || {};
  if (!/^[a-z0-9]+$/i.test(cfg.projectId || '')) { return; }
  var KEY = 'site_clarity_consent_v2';
  var days = Number(cfg.retentionDays);
  var TTL = (days > 0 && days <= 365 ? days : 180) * 86400000;
  var policy = String(cfg.policyVersion || '1');
  var state = null;
  var expiryTimer = null;
  var retryTimers = [];
  var ui = null;
  var previousFocus = null;
  var persistenceOK = true;
  var pendingStatus = '';
  var queue = function () { (queue.q = queue.q || []).push(arguments); };

  // 只允许这一个加载入口。不要同时使用主题、GTM 或其他插件嵌入 Clarity。
  var conflict = typeof w.clarity !== 'undefined' || !!d.querySelector('script[src*="clarity.ms/tag/"]');
  if (!conflict) { w.clarity = queue; }

  function validate(raw) {
    if (!raw || raw.version !== 2 || raw.projectId !== cfg.projectId || raw.policy !== policy ||
        typeof raw.analytics !== 'boolean' || typeof raw.ads !== 'boolean' ||
        !Number.isFinite(raw.ts) || raw.ts > Date.now() || Date.now() - raw.ts >= TTL) { return null; }
    return { version: 2, projectId: cfg.projectId, policy: policy, analytics: raw.analytics, ads: raw.ads, ts: raw.ts };
  }
  function read() {
    try { return validate(JSON.parse(w.localStorage.getItem(KEY) || 'null')); }
    catch (e) { return null; }
  }
  function payload() {
    return { ad_Storage: state && state.ads ? 'granted' : 'denied',
      analytics_Storage: state && state.analytics ? 'granted' : 'denied' };
  }
  function send() {
    if (typeof w.clarity !== 'function') { return; }
    // 脚本尚未加载时只保留最新选择,避免异步队列重放过时的允许状态。
    if (w.clarity === queue) {
      queue.q = (queue.q || []).filter(function (args) { return args[0] !== 'consentv2'; });
    }
    w.clarity('consentv2', payload());
  }
  function sync() {
    retryTimers.forEach(w.clearTimeout);
    send();
    // SDK 在撤销分析同意时可能重启;重发的始终是当前状态,不是旧快照。
    retryTimers = [w.setTimeout(send, 600), w.setTimeout(send, 1800)];
  }
  function status(message) {
    pendingStatus = message;
    if (ui) { ui.status.textContent = message; ui.status.hidden = !message; }
  }
  function render() {
    if (!ui) { return; }
    ui.banner.hidden = state !== null;
    ui.reopen.hidden = state === null;
    if (conflict) { status('检测到其他 Clarity 加载入口,请站点管理员检查重复配置。'); }
  }
  function armExpiry() {
    w.clearTimeout(expiryTimer);
    if (!state) { return; }
    // setTimeout 的最大值有限,长时间停留页面也会重新检查期限。
    expiryTimer = w.setTimeout(function () {
      if (!validate(state)) { state = null; sync(); render(); }
      else { armExpiry(); }
    }, Math.min(Math.max(1, state.ts + TTL - Date.now()), 2147483647));
  }
  function close() {
    if (ui && ui.dialog.open) { ui.dialog.close(); }
  }
  function choose(analytics, ads) {
    state = { version: 2, projectId: cfg.projectId, policy: policy,
      analytics: !!analytics, ads: !!ads, ts: Date.now() };
    persistenceOK = true;
    try { w.localStorage.setItem(KEY, JSON.stringify(state)); }
    catch (e) {
      persistenceOK = false;
      // 避免配额错误留下之前的允许记录;删除失败时在界面明确说明。
      try { w.localStorage.removeItem(KEY); } catch (ignored) {}
    }
    sync(); armExpiry(); close(); render();
    status(persistenceOK ? '' : '偏好已在本页生效,但浏览器无法保存。下次访问请重新检查偏好,必要时清除本站存储。');
    if (ui) { ui.reopen.focus(); }
  }
  function open() {
    if (!ui) { return; }
    previousFocus = d.activeElement;
    ui.analytics.checked = !!(state && state.analytics);
    ui.ads.checked = !!(state && state.ads);
    if (!ui.dialog.open) { ui.dialog.showModal(); }
    ui.analytics.focus();
  }

  state = read(); // 无有效选择时默认为双 denied。
  sync();        // 在创建任何 Clarity script 标签之前建立队列。
  armExpiry();
  if (!conflict) {
    var tag = d.createElement('script');
    tag.async = true;
    tag.id = 'site-clarity-tag';
    tag.src = 'https://www.clarity.ms/tag/' + encodeURIComponent(cfg.projectId);
    tag.onload = sync;
    tag.onerror = function () { status('统计服务暂时无法加载;Cookie 偏好仍可正常设置。'); };
    d.head.appendChild(tag);
  } else {
    console.error('Site Clarity Consent: remove the other Clarity loader before use.');
  }

  w.SiteCookieConsent = {
    open: open,
    reject: function () { choose(false, false); },
    get: function () { return state ? Object.assign({}, state) : null; }
  };
  w.addEventListener('storage', function (event) {
    if (event.key !== KEY && event.key !== null) { return; }
    state = read(); sync(); armExpiry(); render();
    if (ui && ui.dialog.open) {
      ui.analytics.checked = !!(state && state.analytics);
      ui.ads.checked = !!(state && state.ads);
    }
  });
  w.addEventListener('pageshow', function (event) {
    // 从往返缓存恢复时读取其他页面可能已撤回的选择。
    if (event.persisted) { state = read(); sync(); armExpiry(); render(); }
  });

  function ready() {
    ui = {
      banner: d.getElementById('scc-banner'), reopen: d.getElementById('scc-reopen'),
      dialog: d.getElementById('scc-dialog'), analytics: d.getElementById('scc-analytics'),
      ads: d.getElementById('scc-ads'), status: d.getElementById('scc-status')
    };
    if (Object.keys(ui).some(function (key) { return !ui[key]; })) { ui = null; return; }
    d.querySelectorAll('[data-scc-action]').forEach(function (button) {
      button.addEventListener('click', function () {
        switch (button.getAttribute('data-scc-action')) {
          case 'accept': choose(true, true); break;
          case 'reject': choose(false, false); break;
          case 'manage': open(); break;
          case 'close': close(); break;
          case 'save': choose(ui.analytics.checked, ui.ads.checked); break;
        }
      });
    });
    ui.dialog.addEventListener('click', function (event) {
      if (event.target !== ui.dialog) { return; }
      var rect = ui.dialog.getBoundingClientRect();
      if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) { close(); }
    });
    ui.dialog.addEventListener('close', function () {
      if (previousFocus && previousFocus.isConnected) { previousFocus.focus(); }
    });
    render(); status(pendingStatus);
  }
  if (d.readyState === 'loading') { d.addEventListener('DOMContentLoaded', ready, { once: true }); }
  else { ready(); }
})(window, document);

consent.css

.scc-banner[hidden], .scc-reopen[hidden], .scc-status[hidden] { display: none !important; }
.scc-banner, .scc-dialog, .scc-reopen, .scc-status {
  box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
  color: #242424; font-size: 14px; line-height: 1.6;
}
.scc-banner {
  position: fixed; inset: auto 0 0; z-index: 99999;
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  background: #fffaf5; border-top: 1px solid #e7d7c7; padding: 16px;
  box-shadow: 0 -6px 16px rgba(0,0,0,.08); max-height: 65vh; overflow: auto;
}
.scc-banner p { margin: 0; flex: 1 1 400px; }
.scc-banner a { color: #873700; text-decoration: underline; }
.scc-actions { display: flex; gap: 10px; flex-wrap: wrap; }
.scc-actions button, .scc-reopen {
  appearance: none; border: 1px solid #8d6f54; border-radius: 6px; background: #fff;
  color: #45280f; font: inherit; font-weight: 600; padding: 8px 14px; cursor: pointer;
}
.scc-actions button:hover, .scc-reopen:hover { background: #f2e4d8; }
.scc-actions button:focus-visible, .scc-reopen:focus-visible, .scc-dialog input:focus-visible {
  outline: 3px solid #265a99; outline-offset: 3px;
}
.scc-reopen { position: fixed; left: 12px; bottom: 12px; z-index: 99998; }
.scc-dialog {
  width: min(92vw, 540px); max-height: 85vh; overflow: auto;
  border: 1px solid #ddd; border-radius: 12px; background: #fff; padding: 22px;
  box-shadow: 0 20px 60px rgba(0,0,0,.2);
}
.scc-dialog::backdrop { background: rgba(0,0,0,.5); }
.scc-dialog h2 { color: inherit; font-size: 20px; margin: 0 0 12px; }
.scc-dialog label { display: block; padding: 12px 0; border-bottom: 1px dashed #ddd; }
.scc-dialog input { margin-right: 8px; }
.scc-dialog .scc-actions { justify-content: flex-end; }
.scc-status {
  position: fixed; right: 12px; top: 12px; z-index: 100000;
  max-width: min(90vw, 400px); background: #fff; border: 1px solid #aaa; border-radius: 6px; padding: 10px;
}
@media (max-width: 640px) {
  .scc-banner { gap: 12px; padding: 14px; }
  .scc-actions button { min-height: 44px; }
  .scc-dialog { padding: 16px; }
}
插件代码配图

安装后检查

  1. 使用测试站点和测试 Clarity 项目,在干净的浏览器会话中打开页面,先不点击任何按钮。检查初始化状态为双 denied,且未创建 _clck、_clsk。
  2. 分别测试“全部拒绝”“仅分析”“仅广告”“接受全部”,确认两个信号分别变化;刷新页面后再次检查保存的状态。
  3. 接受后重新打开“Cookie 设置”,选择全部拒绝。检查状态回到双 denied,此前的 Clarity Cookie 被移除;然后测试再次同意、快速连续修改和另一个标签页撤回。
  4. 测试过期记录、浏览器禁用存储、缓存插件、内容安全策略、广告拦截器与网络失败。检查桌面和手机上的弹窗、键盘操作以及隐私政策链接。

官方提供了下面的状态查询方式,可在浏览器开发者工具中运行。读取实际返回对象确认状态,不要仅凭提示条消失判断成功:

window.clarity('metadata', function (data, upgraded, consent) {
  console.log('Clarity consent:', consent);
}, false, true, true);

Clarity 的网络请求、Cookie 和服务端项目配置还需在目标测试环境实际检查。本教程中的 consentv2 调用不承诺完全停止数据发送,也不清除微软已经接收的历史数据;Cookie 同意模式同样不构成网站整体法律合规的保证。

参考资料

代码许可

以上代码按 GNU GPL 第 3 版(GPL-3.0-only)发布。原插件标识为 DMY Cookie Consent 1.0.0,原插件头部作者标识为 DMY。2026 年 10 月 2 日修订为 Site Clarity Consent 2.0.0,补充 ConsentV2、偏好到期、撤回与同步处理;再分发时请保留相应版权、许可与修改说明。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容