Enter Your GA4 Property ID (Numbers Only)
- Published on
- ...
- Authors

- Name
- Huashan
- @herohuashan
Background
As a Hugo static blog hosted on Cloudflare Pages, I've always wanted to display real page view data on article pages. But there were problems:
- Google Analytics' gtag.js can only "write" data, not "read" historical data
- Static blogs have no backend and cannot directly call the GA4 Data API
- Didn't want to rely on third-party services (like Busuanzi), wanted complete control over data
After research, I chose the Cloudflare Workers + Google Analytics Data API solution, which perfectly solved this problem.
Final Result
Page view count is displayed in article meta information (after date, reading time, and author):
2025-11-04 · 5 minutes · geekhuashan · 👁 123 views
Features:
- ✅ Display GA4's real global page views
- ✅ 5-minute two-tier caching (Workers + frontend)
- ✅ Support for 1k+, 10k+ formatting
- ✅ Graceful degradation, doesn't affect page loading
- ✅ Completely free (Cloudflare Workers free tier is sufficient)
Technical Architecture
Visitor Browser
↓ Request article page
Frontend JavaScript (page-views.js)
↓ 1. First display cache (localStorage, 5 minutes)
↓ 2. Async request API
Cloudflare Workers (pageviews.js)
↓ 1. Check Cache API (5 minutes)
↓ 2. Authenticate with service account
↓ 3. Call GA4 Data API
Google Analytics 4
↓ Return real page views
Frontend displays: 👁 123 views
Implementation Steps
Step 1: Create Google Cloud Service Account
1.1 Enable GA4 Data API
Visit Google Cloud Console, search and enable in APIs & Services → Library:
Google Analytics Data API
1.2 Create Service Account
Go to IAM & Admin → Service Accounts, create new service account:
- Service account name:
ga4-pageviews-reader - Role: No GCP project permissions needed, skip
Download JSON key file (keep it safe, don't upload to GitHub).
1.3 Authorize in GA4
Log into Google Analytics:
- Go to
Admin→Property access management - Click
+ Add users - Enter service account email (e.g.,
[email protected]) - Role selection: Viewer
- Uncheck "Notify new users by email"
1.4 Get GA4 Property ID
In GA4 admin page, go to Property settings, copy Property ID (numbers only, like 123456789).
Step 2: Create Cloudflare Workers
2.1 Workers Code Structure
Create workers/pageviews.js:
export default {
async fetch(request, env) {
// 1. Handle CORS
if (request.method === 'OPTIONS') {
return handleCORS(request);
}
// 2. Get path parameter
const url = new URL(request.url);
const path = url.searchParams.get('path');
// 3. Check cache
const cached = await getCachedData(path, env);
if (cached) return jsonResponse(cached, 200, request, true);
// 4. Get data from GA4 API
const pageViews = await fetchGA4PageViews(path, env);
const data = {
path: path,
views: pageViews,
updatedAt: new Date().toISOString(),
cached: false
};
// 5. Cache and return
await setCachedData(path, data, env);
return jsonResponse(data, 200, request);
}
};
Core Features:
- Authenticate using Google Cloud service account (JWT + RS256 signature)
- Call GA4 Data API to query
screenPageViewsfor specific pages - Use Cache API to cache for 5 minutes
- CORS whitelist protection
2.2 Configuration File wrangler.toml
name = "geekhuashan-pageviews"
main = "pageviews.js"
compatibility_date = "2024-01-01"
Step 3: Deploy Workers
3.1 Install Wrangler CLI
npm install -g wrangler
3.2 Login to Cloudflare
cd workers
wrangler login
Browser will automatically open authorization page, click Allow.
3.3 Set Environment Variables (Secrets)
Set GA4 Property ID:
wrangler secret put GA4_PROPERTY_ID
Set Google Cloud Credentials:
cat your-service-account.json | jq -c . | wrangler secret put GA4_CREDENTIALS
⚠️ Important: Secrets are encrypted and stored in Cloudflare, won't leak to Git repository.
3.4 Deploy
wrangler deploy
After success, Workers URL will be displayed:
✨ Deployed geekhuashan-pageviews
https://geekhuashan-pageviews.xxx.workers.dev
Step 4: Frontend Integration
4.1 HTML Structure (Hugo Template)
Modify layouts/partials/post_meta.html, add page view placeholder in meta info:
<span class="page-views" data-page-url="{{ .RelPermalink }}">
<span class="view-icon">👁</span>
<span class="view-count">0</span> views
</span>
4.2 JavaScript Call API
Create assets/js/page-views.js:
(function() {
'use strict';
const API_ENDPOINT = 'https://your-worker.workers.dev';
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
function initPageViews() {
const viewContainer = document.querySelector('.page-views');
if (!viewContainer) return;
const pageUrl = viewContainer.getAttribute('data-page-url');
const viewCountElement = viewContainer.querySelector('.view-count');
// 1. First display cache
const cachedData = getCachedViewCount(pageUrl);
if (cachedData && cachedData.views !== undefined) {
viewCountElement.textContent = formatViewCount(cachedData.views);
}
// 2. Async fetch latest data
fetchPageViews(pageUrl)
.then(function(data) {
if (data && data.views !== undefined) {
viewCountElement.textContent = formatViewCount(data.views);
setCachedViewCount(pageUrl, data);
}
})
.catch(function(error) {
console.warn('Failed to fetch page views:', error);
if (!cachedData) {
viewCountElement.textContent = '-';
}
});
}
async function fetchPageViews(pageUrl) {
const url = `${API_ENDPOINT}?path=${encodeURIComponent(pageUrl)}`;
const response = await fetch(url);
if (!response.ok) throw new Error(`API request failed: ${response.status}`);
return await response.json();
}
function formatViewCount(count) {
if (count >= 10000) {
return Math.floor(count / 1000) + 'k+';
} else if (count >= 1000) {
return (count / 1000).toFixed(1).replace('.0', '') + 'k+';
} else {
return count.toString();
}
}
// Cache functions omitted...
// Initialize when DOM loads
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initPageViews);
} else {
initPageViews();
}
})();
4.3 CSS Styles
Create assets/css/extended/page-views.css:
.page-views {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--secondary);
white-space: nowrap;
}
.page-views .view-icon {
font-size: 0.95em;
opacity: 0.8;
}
.page-views .view-count {
font-weight: 500;
color: var(--primary);
}
@media print {
.page-views {
display: none !important;
}
}
Testing and Verification
Local Testing
hugo server -D
open http://localhost:1313/your-article/
Open browser developer tools (F12), check:
- Network tab, find
pageviews?path=request - Check response format:
{
"path": "/your-article/",
"views": 123,
"updatedAt": "2025-11-04T07:22:54.892Z",
"cached": false
}
Test Workers API Directly
curl "https://your-worker.workers.dev?path=/test/"
Performance Optimization
Two-tier Caching Strategy
Workers Cache API (server-side):
- Cache duration: 5 minutes
- Purpose: Reduce GA4 API calls
- Location: Cloudflare edge nodes
localStorage (client-side):
- Cache duration: 5 minutes
- Purpose: Reduce Workers API requests
- Location: Visitor browser
Cost Analysis
Cloudflare Workers Free Plan:
- ✅ 100,000 requests/day
- ✅ 10ms CPU time/request
- ✅ Unlimited outbound traffic
Actual consumption estimate (assuming 1000 PV/day):
- 1000 PV ÷ 5-minute cache = ~200 requests/day
- Far below free tier limit
Google Analytics Data API:
- ✅ Completely free
- ✅ No request limit
Total cost: $0 🎉
Security Considerations
Implemented Security Measures
- Service account minimal permissions: Only granted GA4 Viewer permission
- Secrets encrypted storage: Sensitive info encrypted via
wrangler secret - CORS whitelist: Only allow specified domains to access API
- Rate limiting: Prevent abuse through caching mechanism
Sensitive Information Protection
⚠️ Never commit the following to Git:
- JSON key file (
*.json) - GA4 Property ID
- Service account email
Add to workers/.gitignore:
*.json
!package.json
!wrangler.toml
.env
*.key
Possible Issues
Issue 1: API Returns 500 Error
Cause: GA4 API call failed
Troubleshooting:
- Check if GA4_PROPERTY_ID is correct (numbers only)
- Check if service account is authorized in GA4 (Viewer permission)
- Use
wrangler tailto view real-time logs
Issue 2: CORS Error
Cause: CORS whitelist in Workers doesn't include your domain
Solution: Modify ALLOWED_ORIGINS in pageviews.js:
const ALLOWED_ORIGINS = [
'https://geekhuashan.com',
'http://localhost:1313'
];
Redeploy: wrangler deploy
Issue 3: Page Views Show 0
Cause: No data for this page path in GA4
Troubleshooting:
- Log into Google Analytics, check if path format is correct
- GA4 data has 24-48 hour delay, new articles may temporarily have no data
- Confirm gtag.js is properly loaded and sending
page_viewevents
Extended Optimization
1. Bind Custom Domain
Configure Workers Route in Cloudflare Dashboard:
- Route:
api.geekhuashan.com/pageviews* - Worker:
geekhuashan-pageviews
Update frontend API endpoint:
const API_ENDPOINT = 'https://api.geekhuashan.com/pageviews';
2. Display Other Metrics
Modify GA4 API query in Workers to get:
- Active users (
activeUsers) - Average session duration (
averageSessionDuration) - Bounce rate (
bounceRate)
3. Use KV Storage
Upgrade Cache API to Cloudflare KV for more stable caching:
# wrangler.toml
[kv_namespaces]
binding = "PAGEVIEWS_KV"
id = "your-kv-namespace-id"
Summary
Through the Cloudflare Workers + Google Analytics Data API solution, I successfully added real page view statistics to my static blog, with key advantages:
✅ Completely free: Cloudflare and GA4 free tiers are sufficient ✅ Excellent performance: Two-tier caching + edge computing, low latency globally ✅ Accurate data: Get real data directly from GA4 ✅ Complete control: No third-party dependencies, code and data in your hands ✅ Easy maintenance: Serverless architecture, no server management needed
If you're also using Hugo + Cloudflare Pages, I highly recommend trying this solution!
Reference Resources
- Cloudflare Workers Documentation
- Google Analytics Data API Documentation
- Wrangler CLI Documentation
- Hugo Template Syntax
Project Source Code
Complete code is open source on GitHub (sensitive info removed):
# File structure
workers/
├── pageviews.js # Workers main function
├── wrangler.toml # Deployment config
└── package.json # npm dependencies
themes/your-theme/
├── assets/
│ ├── js/page-views.js # Frontend script
│ └── css/extended/page-views.css # Styles
└── layouts/partials/
├── post_meta.html # Display location
└── extend_footer.html # Script loading
Welcome to Star and Fork! ⭐
Updated on 2024-11-04: Initial publication, welcome discussion!
Related Posts
Clean and Rebuild
Comprehensive Lighthouse performance optimization guide
SEO Optimization in the AI Era - From Search Engines to AI Agents
AI is changing how information is accessed. This article introduces how to optimize blogs for AI agents (ChatGPT, Perplexity), including Schema.org structured data, FAQ markup, and robots.txt configuration.
Bidirectional Links System Demo - Backlinks
Demonstrating the bidirectional links feature implemented in Hugo blog, inspired by knowledge management approaches in Obsidian and Roam Research.