const DEFAULT_CACHE_TTL_SECONDS = 600; function jsonResponse(data, init = {}) { return new Response(JSON.stringify(data), { ...init, headers: { 'Content-Type': 'application/json; charset=utf-8', ...init.headers, }, }); } function getCorsHeaders(request, env) { const origin = request.headers.get('Origin') || ''; const allowedOrigin = env.ALLOWED_ORIGIN || 'https://blog.biss.click'; const allowOrigin = origin === allowedOrigin ? origin : allowedOrigin; return { 'Access-Control-Allow-Origin': allowOrigin, 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Vary': 'Origin', }; } function normalizeBaseUrl(value) { return String(value || 'https://umami.biss.click').replace(/\/+$/, ''); } function getShareId(env) { if (env.UMAMI_SHARE_ID) return env.UMAMI_SHARE_ID; if (!env.UMAMI_SHARE_URL) return ''; try { const url = new URL(env.UMAMI_SHARE_URL); const parts = url.pathname.split('/').filter(Boolean); const shareIndex = parts.indexOf('share'); return shareIndex >= 0 ? parts[shareIndex + 1] || '' : parts.at(-1) || ''; } catch { return env.UMAMI_SHARE_URL.split('/').filter(Boolean).at(-1) || ''; } } async function getShareData(env) { const baseUrl = normalizeBaseUrl(env.UMAMI_BASE_URL); const shareId = getShareId(env); if (!shareId) { throw new Error('Missing UMAMI_SHARE_ID or UMAMI_SHARE_URL'); } const response = await fetch(`${baseUrl}/api/share/${shareId}`, { headers: { Accept: 'application/json' }, }); if (!response.ok) { throw new Error(`Umami share lookup failed: ${response.status}`); } const data = await response.json(); if (!data.websiteId || !data.token) { throw new Error('Umami share response did not include websiteId or token'); } return data; } async function fetchUmamiStats(env, shareData) { const baseUrl = normalizeBaseUrl(env.UMAMI_BASE_URL); const websiteId = env.UMAMI_WEBSITE_ID || shareData.websiteId; const startAt = Number(env.STATS_START_AT || 0); const endAt = Date.now(); const url = `${baseUrl}/api/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}`; return fetch(url, { headers: { Accept: 'application/json', 'x-umami-share-context': '1', 'x-umami-share-token': shareData.token, }, }); } async function getStats(env) { const shareData = await getShareData(env); const response = await fetchUmamiStats(env, shareData); if (!response.ok) { throw new Error(`Umami stats failed: ${response.status}`); } const stats = await response.json(); return { visitors: stats.visitors ?? 0, pageviews: stats.pageviews ?? 0, visits: stats.visits ?? 0, updatedAt: Date.now(), }; } export default { async fetch(request, env, ctx) { const corsHeaders = getCorsHeaders(request, env); if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders }); } if (request.method !== 'GET') { return jsonResponse({ error: 'Method not allowed' }, { status: 405, headers: corsHeaders }); } const cacheTtl = Number(env.CACHE_TTL_SECONDS || DEFAULT_CACHE_TTL_SECONDS); const cache = caches.default; const cacheKey = new Request( `https://site-stats-cache.local/umami-site-stats/${getShareId(env) || 'default'}`, ); const cached = await cache.match(cacheKey); if (cached) { return new Response(cached.body, { status: cached.status, headers: { ...Object.fromEntries(cached.headers), ...corsHeaders, 'X-Stats-Cache': 'HIT', }, }); } try { const stats = await getStats(env); const response = jsonResponse(stats, { headers: { ...corsHeaders, 'Cache-Control': `public, max-age=${cacheTtl}`, 'X-Stats-Cache': 'MISS', }, }); ctx.waitUntil(cache.put(cacheKey, response.clone())); return response; } catch (error) { return jsonResponse( { error: 'Failed to fetch stats', message: error instanceof Error ? error.message : String(error), }, { status: 502, headers: corsHeaders }, ); } }, };