Logo

Enter Your GA4 Property ID (Numbers Only)

Published on
...
Authors

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:

  1. Google Analytics' gtag.js can only "write" data, not "read" historical data
  2. Static blogs have no backend and cannot directly call the GA4 Data API
  3. 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 & ServicesLibrary:

Google Analytics Data API

1.2 Create Service Account

Go to IAM & AdminService 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:

  1. Go to AdminProperty access management
  2. Click + Add users
  3. Enter service account email (e.g., [email protected])
  4. Role selection: Viewer
  5. 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 screenPageViews for 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:

  1. Network tab, find pageviews?path= request
  2. 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

  1. Workers Cache API (server-side):

    • Cache duration: 5 minutes
    • Purpose: Reduce GA4 API calls
    • Location: Cloudflare edge nodes
  2. 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

  1. Service account minimal permissions: Only granted GA4 Viewer permission
  2. Secrets encrypted storage: Sensitive info encrypted via wrangler secret
  3. CORS whitelist: Only allow specified domains to access API
  4. 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:

  1. Check if GA4_PROPERTY_ID is correct (numbers only)
  2. Check if service account is authorized in GA4 (Viewer permission)
  3. Use wrangler tail to 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:

  1. Log into Google Analytics, check if path format is correct
  2. GA4 data has 24-48 hour delay, new articles may temporarily have no data
  3. Confirm gtag.js is properly loaded and sending page_view events

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


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!

Enter Your GA4 Property ID (Numbers Only) | 原子比特之间