Topics

GA4-artikelranglijst van microCMS uitvoeren zonder API-verbruik

  • column

Klanten vragen regelmatig om artikelranglijsten weer te geven.
Bij Jamstack-sites met microCMS is het gebruikelijk een eigen API aan te maken voor artikelranglijsten.
Maar als je geen API-quota meer hebt en toch wilt implementeren, kun je ook PV-getallen rechtstreeks naar bestaande artikelinhoud schrijven als benadering.

Deze keer presenteer ik een voorbeeld van de implementatie van deze benadering op een site die met microCMS + Cloudflare Pages via SSG wordt gepubliceerd!

Cloudflare is op een eigenaardige manier bij het grote publiek bekend geworden, maar dit is eigenlijk ook iets wat klanten van ons hebben gevraagd en wat we onlangs op onze eigen site testen! https://www.liberogic.jp/topics/

Situaties waarin deze benadering geschikt is

  • Wanneer je ongeveer TOP 10-ranglijsten wilt weergeven in de zijbalk of voettekst
  • Wanneer real-time informatie niet echt nodig is
  • Wanneer je microCMS-API-quota beperkt is
  • Eenvoudig implementeren met statische sitegeneratie (SSG)

Stap 1: microCMS voorbereiden (schemaumbreiding en Webhook-beheer)

1-1. API-schema uitbreiden

We voegen twee velden voor rangschikking toe aan het bestaande artikeleindpunt.

  • pageView (getal): slaat de cumulatieve paginaweergaven van de afgelopen 30 dagen op, opgehaald uit GA4.
  • lastUpdatedPV (datum en tijd): registreert wanneer de paginaweergaven voor het laatst zijn bijgewerkt.

1-2. API-sleutelbevoegdheden configureren

Schakel PATCH in de instellingen van de API-sleutel die u gebruikt.

1-3. Webhook instellen (strategie om herhaalde builds te voorkomen)

Schakel "Publicatie van inhoud (via API-bewerking)" uit in de Webhook-triggers om te voorkomen dat de build herhaaldelijk wordt geactiveerd wanneer paginaweergavenaantallen worden bijgewerkt.

Stap 2: Verificatie-instellingen in Google Cloud Console

Om toegang te krijgen vanaf Cloudflare Workers, stellen we een serviceaccount en een Data API in.

2-1. API's inschakelen

Schakel Google Analytics Data API in voor uw project in Google Cloud Console.

2-2. Serviceaccount en sleutel maken

Maak een serviceaccount voor GA4-integratie en kopieer het e-mailadres.

Klik op het tabblad 'Sleutels' en selecteer 'Sleutel toevoegen' > 'Nieuwe sleutel maken'. Maak een sleutel van het type JSON en download deze.

2-3. GA4-machtigingen toekennen

Voeg in 'Toegangsbeheer' van uw GA4-eigenschap het e-mailadres van stap 2-2 toe als gebruiker en verleen 'Viewer'-machtigingen of hoger.

Stap 3: Cloudflare Workers maken en configureren (GA4-gegevens ophalen)

3-1. Cloudflare Workers aanmaken en omgevingsvariabelen configureren

Maak een nieuwe Cloudflare Worker aan met een naam zoals ga4-ranking-updater.

Configureer omgevingsvariabelen vanuit instellingen. Registreer sleutels als geheim.

MICROCMS_API_KEY

microCMS API-sleutel

MICROCMS_API_URL

https://[ID].microcms.io/api/v1

GA4_PROPERTY_ID

GA4-eigenschap-ID

GA4_SERVICE_ACCOUNT_CREDENTIALS

Volledige inhoud van het JSON-sleutelbestand

GA4_PRIVATE_KEY_BASE64

De waarde van -----BEGIN PRIVATE KEY----- , -----END PRIVATE KEY----- en verwijderd uit de private_key-waarde in de JSON

3-2. GA4-gegevens bijwerken Worker

Wijzig de inhoud van worker.js vanuit "Code bewerken" als volgt.

const MICROCMS_ENDPOINT_NAME = '[記事のエンドポイント名]'; 

let contentIdToSlugMap = {};

// ----------------------------------------------------------------------
// 1. microCMSから全記事のIDとスラッグを取得し、マップを作成する
// ----------------------------------------------------------------------
async function fetchContentMap(env) {
    const apiEndpoint = `${env.MICROCMS_API_URL}/${MICROCMS_ENDPOINT_NAME}`;
    const slugToIdMap = {};
    let offset = 0;
    const limit = 100;

    while (true) {
        const url = `${apiEndpoint}?fields=id,slug&limit=${limit}&offset=${offset}`;
        const response = await fetch(url, {
            headers: { 'X-MICROCMS-API-KEY': env.MICROCMS_API_KEY },
        });
        if (!response.ok) {
            throw new Error(`microCMS Map Fetch Error: ${response.status} ${await response.text()}`);
        }
        const data = await response.json();
        
        data.contents.forEach(item => {
            if (item.slug && item.id) {
                slugToIdMap[item.slug] = item.id;
            }
        });
        if (data.contents.length < limit || data.totalCount <= (offset + limit)) {
            break;
        }
        offset += limit;
    }
    
    contentIdToSlugMap = slugToIdMap; 
    console.log(`microCMSから合計 ${Object.keys(slugToIdMap).length} 件の記事IDマップを取得しました。`);
}

// ----------------------------------------------------------------------
// 2. JWT 認証ヘルパー関数群 (GA4 アクセストークン取得用)
// ----------------------------------------------------------------------
// Base64Url エンコード/デコード
const base64UrlEncode = (data) => btoa(String.fromCharCode(...new Uint8Array(data))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');
const base64UrlDecode = (data) => Uint8Array.from(atob(data.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));

async function importPrivateKey(keyBase64) {
    // 秘密鍵本体をデコードし、DER形式のバイナリにする
    const binaryDer = base64UrlDecode(keyBase64);

    return crypto.subtle.importKey(
        'pkcs8',
        binaryDer,
        { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
        false,
        ['sign']
    );
}

// GA4 Service Accountの認証情報からAccess Tokenを取得するメイン関数
async function getAccessToken(credentialsString, privateKeyBodyBase64) {
    // 認証情報JSONをパースし、client_emailを取得
    const creds = JSON.parse(credentialsString);
    const serviceAccountEmail = creds.client_email;

    const now = Math.floor(Date.now() / 1000);
    const expiry = now + 3600; // 有効期限: 1時間後
    const header = { alg: 'RS256', typ: 'JWT' };
    const payload = {
        iss: serviceAccountEmail,
        scope: '<https://www.googleapis.com/auth/analytics.readonly>',
        aud: '<https://oauth2.googleapis.com/token>',
        exp: expiry,
        iat: now,
    }

    // 2. 署名するデータ(Header.Payload)を作成
    const encodedHeader = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)));
    const encodedPayload = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)));
    const signatureInput = `${encodedHeader}.${encodedPayload}`;

    // 3. 秘密鍵をインポートし、JWTに署名
    const key = await importPrivateKey(privateKeyBodyBase64);
    const signature = await crypto.subtle.sign(
        { name: 'RSASSA-PKCS1-v1_5' },
        key,
        new TextEncoder().encode(signatureInput)
    );
    const encodedSignature = base64UrlEncode(new Uint8Array(signature));

    // 4. JWTを完成させる
    const jwt = `${signatureInput}.${encodedSignature}`;

    // 5. Googleトークンエンドポイントへリクエスト
    const tokenResponse = await fetch('<https://oauth2.googleapis.com/token>', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
            grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            assertion: jwt,
        }),
    });

    if (!tokenResponse.ok) {
        throw new Error(`Token request failed: ${tokenResponse.status} ${await tokenResponse.text()}`);
    }

    const tokenData = await tokenResponse.json();
    return tokenData.access_token; // アクセストークンを返す
}

// ----------------------------------------------------------------------
// 3. GA4 データ取得関数
// ----------------------------------------------------------------------
async function fetchGa4Data(accessToken, propertyId) {
    const apiEndpoint = `https://analyticsdata.googleapis.com/v1beta/properties/${propertyId}:runReport`;
    
    const response = await fetch(apiEndpoint, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            dateRanges: [{ startDate: '30daysAgo', endDate: 'yesterday' }], // 昨日から30日間で取得する(必要に合わせて変更)
            dimensions: [{ name: 'pagePath' }],
            metrics: [{ name: 'screenPageViews' }],
            orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }],
            limit: 100, // 100件以降の記事はPV0として処理する(必要に合わせて変更)
        }),
    });

    if (!response.ok) {
        throw new Error(`GA4 API Error: ${response.status} ${await response.text()}`);
    }

    const data = await response.json();
    
    const pvData = data.rows?.map(row => ({
        pagePath: row.dimensionValues[0].value, 
        pageView: parseInt(row.metricValues[0].value, 10), 
    })) || [];
    
    return pvData;
}

// ----------------------------------------------------------------------
// 4. microCMS コンテンツID解決関数
// ----------------------------------------------------------------------
// `/blog/記事スラッグ/`という構造の場合(必要に合わせて変更)
function resolveContentIdFromPath(pagePath) {
    try {
        const cleanPath = new URL('<https://dummy.com>' + pagePath).pathname;
        const parts = cleanPath.split('/').filter(p => p.length > 0); 

        // 1. '/blog/ID' の構造であるか確認
        if (parts.length < 2 || parts[0] !== 'blog') {
            return null; 
        }
        
        const slug = parts[1];
        
        // マップを使って、スラッグからコンテンツIDを取得
        const actualContentId = contentIdToSlugMap[slug];

        if (!actualContentId) {
             // マップに存在しない(microCMSに記事が存在しない)場合は無視
             console.log(`[IGNORE] Slug ${slug} not found in microCMS map.`);
             return null;
        }

        // microCMSのコンテンツIDを返す
        console.log(`[RESOLVED] Slug ${slug} -> ID: ${actualContentId}`);
        return actualContentId;

    } catch (e) {
        console.error(`Path parsing error for ${pagePath}: ${e}`);
        return null;
    }
}

// ----------------------------------------------------------------------
// 5. microCMS コンテンツ更新関数
// ----------------------------------------------------------------------
async function updateMicroCMS(pvData, env) {
    let updatedCount = 0;
    const errors = [];
    
    // 1. GA4データをスラッグをキーとするマップに変換
    const ga4SlugPvMap = {};
    pvData.forEach(item => {
        const slug = item.pagePath.split('/').filter(p => p.length > 0)[1];
        if (slug) {
            ga4SlugPvMap[slug] = item.pageView;
        }
    });

    // 2. microCMSの全記事マップ (contentIdToSlugMap) をループ
    for (const slug in contentIdToSlugMap) {
        if (contentIdToSlugMap.hasOwnProperty(slug)) {
            
            const actualContentId = contentIdToSlugMap[slug];
            
            // 2で取得したGA4のデータにあればその値、なければ 0 を設定 (リセット)
            const newPageView = ga4SlugPvMap[slug] || 0; 

            // PATCHリクエストのURLを構築
            const microcmsUrl = `${env.MICROCMS_API_URL}/${MICROCMS_ENDPOINT_NAME}/${actualContentId}`;
            
            const updatePayload = {
                pageView: newPageView, 
                lastUpdatedPV: new Date().toISOString()
            };
            
            const response = await fetch(microcmsUrl, {
                method: 'PATCH', 
                headers: {
                    'Content-Type': 'application/json',
                    'X-MICROCMS-API-KEY': env.MICROCMS_API_KEY,
                },
                body: JSON.stringify(updatePayload),
            });

            if (response.ok) {
                updatedCount++;
            } else {
                errors.push({ contentId: actualContentId, status: response.status, body: await response.text() });
            }
        }
    }
    
    if (errors.length > 0) {
        console.error(`microCMS Update Errors: ${JSON.stringify(errors)}`);
    }

    console.log(`microCMSのコンテンツ ${updatedCount} 件を更新しました。`);
    return updatedCount;
}

// ----------------------------------------------------------------------
// 6. Workerのメインハンドラー (Cron Triggers用)
// ----------------------------------------------------------------------
export default {
    async scheduled(controller, env, ctx) {
        try {
            console.log('--- GA4 Ranking Updater Started ---');
            
            // 1. microCMSからIDマップを事前取得
            await fetchContentMap(env);

            // 2. GA4 Access Tokenの取得
            const accessToken = await getAccessToken(
                env.GA4_SERVICE_ACCOUNT_CREDENTIALS,
                env.GA4_PRIVATE_KEY_BASE64
            );

            // 3. GA4データ取得
            const pvData = await fetchGa4Data(accessToken, env.GA4_PROPERTY_ID);
            console.log(`GA4から ${pvData.length} 件のデータを取得しました。`);
            
            // 4. microCMSコンテンツの更新
            const updatedCount = await updateMicroCMS(pvData, env);
            
            console.log(`microCMSのコンテンツ ${updatedCount} 件を更新しました。`);
            console.log('--- GA4 Ranking Updater Finished ---');

        } catch (error) {
            console.error('致命的なエラーが発生しました:', error);
        }
    }
};

Belangrijkste logica van Worker-code:

  1. Vooraf kaarten ophalen: Haal vooraf de id en slug kaarten van alle microCMS-artikelen op.
  2. GA4-gegevens ophalen: Haal de cumulatieve paginaweergaven van de afgelopen 30 dagen op.
  3. Updatelogica: Werk de paginaweergaven bij met nieuwe GA4-gegevens, of zet ze op 0 als er geen gegevens beschikbaar zijn.

3-3. Cron-configuratie (gegevensupdate)

Stel de Cron-trigger in op 0 15 * * * in het triggergebeurtenis van uw configuratie. Dit wordt elke dag om 00:00 uur uitgevoerd en de gegevens tot gisteren worden naar microCMS doorgevoerd.

Stap 4: Cloudflare Workers maken en instellen (buildtrigger)

4-1. Workers aanmaken en omgevingsvariabelen instellen

Maak een Workers-script met de naam build-trigger of vergelijkbaar, om de gegevensupdate en implementatie gescheiden te houden.

Stel in de omgevingsvariabelen CLOUDFLARE_PAGES_BUILD_HOOK in op de buildhaak-URL van Cloudflare Pages.

4-2. Workercode (buildtrigger)

We implementeren code die een POST-verzoek naar de build hook van Pages stuurt.

export default {
  async scheduled(controller, env, ctx) {
      // 環境変数からビルドフックURLを取得
      const buildHookUrl = env.CLOUDFLARE_PAGES_BUILD_HOOK;

      if (!buildHookUrl) {
          console.error("FATAL: CLOUDFLARE_PAGES_BUILD_HOOK environment variable is not set.");
          return;
      }

      // 2. ビルドフックURLにPOSTリクエストを送信
      const response = await fetch(buildHookUrl, {
          method: 'POST',
          headers: {
              'Content-Type': 'application/json',
          },
      });

      // 3. 結果の確認
      if (response.ok) {
          console.log("✅ Cloudflare Pages Build Triggered Successfully.");
      } else {
          console.error(`❌ Build Trigger Failed! Status: ${response.status} ${response.statusText}`);
      }
  },
};

4-3. Cron-configuratie (site-implementatie)

In het triggergebeurtenis van de instellingen stellen we de Cron-trigger in op 10 15 * * *. Omdat we willen wachten tot de verwerking in stap 3 is voltooid, starten we de implementatie voorzichtig 10 minuten later op 00:10 uur elke dag.

Samenvatting

Vergeleken met het instellen van een speciale API is de directheid minder, maar ongeveer 10 minuten is zeker acceptabel. Het is voordelig omdat we geen API-quota verbruiken en we het kunnen uitvoeren in de gratis laag van Cloudflare. En de eenvoudige configuratie die volledig binnen Cloudflare werkt, is een groot voordeel!

Auteur van dit artikel

Vanuit DTP de wereld van het web in gestapt en merkte al snel dat hij markering, frontend, directie en accessibility allemaal beheerst — een echte 'meester van techniek'. Sinds de oprichting van Liberogic een multitalent en inmiddels een levend naslagwerk in het bedrijf. Tegenwoordig is hij geïnteresseerd in de vraag "Kunnen we accessibility-implementatie meer aan AI overlaten?" en experimenteert hij graag met efficiëntie via prompts. Zowel technisch als mentaal nog volop in ontwikkeling.

Futa

IAAP-gecertificeerd webtoegankelijkheidsspecialist (WAS) / Opmaakingenieur / Frontend-ingenieur / Webdirecteur

Artikelen van deze medewerker bekijken

Casestudies