文章摘要
Stars Harbor Deepseek
拼命加载中···

本教程已于2026年1月15日针对umami3的部分调整进行了更新,如您仍在使用umami2请及时升级或参考其它教程

另感谢:梦爱吃鱼的新版数据追踪代码

本文教程仅适用于通过vercel自建的umami追踪,通过服务器自托管的仅提供部分第三方教程,敬请谅解

搭建数据源

vercel部署umami

此步骤中针对storage的选择部分因vercel有过更新调整,因此实际情况与本教程存在一些差异,但整体思路类似,可简单参考。后续将视情况对此步骤教程稍加调整

  1. fork一份官方的仓库

  2. 登录vercel控制台(若此前尚未注册vercel,请自行借助搜索引擎搜索教程,此处不做赘述)
  3. 在控制台首页顶栏中找到「Storage」,点击进入
    solitude-about_umami-1.webp
  4. 点击「Create Database」新建数据库,选择「Postgres」
    solitude-about_umami-2.webp
  5. 设置名称并确认
    solitude-about_umami-3.webp
  6. 进入数据库控制台,此时页面上会显示一个用于连接数据库的字符串,点击Show Secret按钮后复制双引号中的内容(无需包含双引号)
    solitude-about_umami-4.webp
  7. 新建项目,选择刚刚fork的umami仓库
  8. 设置必要环境变量(数据库)DATABASE_URL,数据库值设置为第5步完成后复制的长字符串
  9. (可选)非必要环境变量TRACKER_SCRIPT_NAME,可设置umami跟踪器名称,以避免默认名称被广告拦截器拦截导致追踪失败。该处可任意填写自己喜欢的值
  10. 等待部署完成即可
  11. (可选)绑定自定义域名,此处不做赘述
  12. 访问umami地址,初次登录时默认用户名为 admin ,默认密码为 umami,可在登录后自行修改

Cloudflare-Workers搭建数据接口

以下教程来自「梦爱吃鱼」,特此鸣谢!原文地址:

  1. 前往 Hoppscotch 获取token
    solitude-about_umami-5.webp
  2. 成功后返回token信息
    solitude-about_umami-6.webp
  3. 前往Cloudflare,创建一个Workers,设置好名称,确认部署并等待
  4. 部署完成,点击右上角编辑代码,修改worker.js的内容(注意,部分内容需修改为你的数据!!!)
    const CONFIG = {
      baseUrl: '你的umami地址',
      token: '老地方获取token',
      websiteId: '需要监听的站点ID'
    };
    
    // 时间范围配置
    const TIME_CONFIGS = {
      today: () => {
        const now = new Date();
        return {
          start: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
          end: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
          unit: 'hour'
        };
      },
      yesterday: () => {
        const now = new Date();
        return {
          start: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1),
          end: new Date(now.getFullYear(), now.getMonth(), now.getDate()),
          unit: 'day'
        };
      },
      lastMonth: () => {
        const now = new Date();
        return {
          start: new Date(now.getFullYear(), now.getMonth() - 1, 1),
          end: new Date(now.getFullYear(), now.getMonth(), 1),
          unit: 'month'
        };
      },
      total: () => {
        return {
          start: new Date(2000, 0, 1),
          end: new Date(),
          unit: 'year'
        };
      },
      now: () => {
        const now = new Date();
        return {
          start: new Date(now.getTime() - 15 * 60 * 1000), // 最近15分钟
          end: now,
          unit: 'hour'
        };
      }
    };
    
    // 获取统计数据
    async function fetchStats(timeRange) {
      const config = TIME_CONFIGS[timeRange]();
      if (!config) return { visits: 0, pageviews: 0 };
      
      const params = new URLSearchParams({
        startAt: Math.floor(config.start.getTime()),
        endAt: Math.floor(config.end.getTime()),
        unit: config.unit
      });
      
      const url = CONFIG.baseUrl + '/api/websites/' + CONFIG.websiteId + '/stats?' + params.toString();
      
      try {
        const response = await fetch(url, {
          headers: {
            'Authorization': 'Bearer ' + CONFIG.token,
            'Content-Type': 'application/json'
          }
        });
    
        if (!response.ok) {
          throw new Error('HTTP错误!状态码: ' + response.status);
        }
    
        const data = await response.json();
    
        // 提取访问数据(兼容不同API返回格式)
        const extractMetric = (key) => {
          // 对于实时数据,特殊处理:即使主数据字段为0,也检查comparison字段
          if (timeRange === 'now' && data.comparison?.[key]) {
            const comparisonValue = parseInt(data.comparison[key]) || 0;
            if (comparisonValue > 0) {
              return comparisonValue;
            }
          }
      
          // 主数据字段
          if (data[key] !== undefined && data[key] !== null) {
            return typeof data[key] === 'object' ? (data[key].value || data[key].count || 0) : parseInt(data[key]) || 0;
          }
      
          // 指标字段
          if (data.metrics?.[key]) {
            return data.metrics[key].value || data.metrics[key].count || 0;
          }
      
          // 比较数据字段
          if (data.comparison?.[key]) {
            return parseInt(data.comparison[key]) || 0;
          }
      
          return 0;
        };
    
        return {
          visits: extractMetric('visits'),
          pageviews: extractMetric('pageviews')
        };
      } catch (error) {
        console.error('获取统计数据失败: ' + error.message);
        return { visits: 0, pageviews: 0 };
      }
    }
    
    // 处理请求
    async function handleRequest(request) {
      try {
        // 设置CORS头
        const corsHeaders = {
          'Access-Control-Allow-Origin': '*',
          'Access-Control-Allow-Methods': 'GET, OPTIONS',
          'Access-Control-Allow-Headers': 'Origin, Content-Type, Accept'
        };
    
        // 处理OPTIONS请求
        if (request.method === 'OPTIONS') {
          return new Response(null, { headers: corsHeaders });
        }
    
        // 并行获取所有统计数据
        const timeRanges = ['today', 'yesterday', 'lastMonth', 'total', 'now'];
        const statsData = await Promise.all(timeRanges.map(range => fetchStats(range)));
    
        // 构建统计数据对象
        const stats = Object.fromEntries(timeRanges.map((range, index) => [range, statsData[index]]));
    
        // 返回统计数据
        const responseData = {
          today_uv: stats.today.visits,
          today_pv: stats.today.pageviews,
          online_users: stats.now.visits,
          yesterday_uv: stats.yesterday.visits,
          yesterday_pv: stats.yesterday.pageviews,
          last_month_pv: stats.lastMonth.pageviews,
          total_uv: stats.total.visits,
          total_pv: stats.total.pageviews
        };
    
        return new Response(JSON.stringify(responseData), {
          headers: {
            ...corsHeaders,
            'Content-Type': 'application/json; charset=utf-8'
          }
        });
      } catch (error) {
        return new Response(JSON.stringify({ error: error.message }), {
          status: 500,
          headers: {
            'Content-Type': 'application/json; charset=utf-8'
          }
        });
      }
    }
    
    // 注册事件监听器
    addEventListener('fetch', event => {
      event.respondWith(handleRequest(event.request));
    });
  5. 完成编辑后,保存并部署
  6. (可选)绑定自定义域名。在workers中点击设置,在域和路由项下添加自定义域名(受cloudflare限制,域名必须已托管至cloudflare才可绑定)

将数据引入about页面

修改./source/_data/about.yml文件中的统计部分

tj: # 统计
  provider: custom # 51la/custom
  url: 你的数据接口地址
  img: https://7.isyangs.cn/1/65eb2e9109826-1.png # 背景
  desc: # 卡片左下角描述

配置完成~

写在最后

下次一定还拖更