Topics

Comment afficher le classement des articles de microCMS à partir de GA4 sans consommer d'API

  • column

Afficher un classement des articles est une demande que nous recevons souvent de la part de nos clients.
Lorsque vous souhaitez afficher le classement des articles de microCMS sur un site Jamstack, il est courant de créer une API dédiée.
Mais si votre quota d'API est épuisé et que vous souhaitez quand même mettre en œuvre cette fonctionnalité, vous pouvez aussi écrire directement le nombre de pages vues dans le contenu d'article existant.

Cette fois, nous vous présentons un exemple de mise en œuvre sur un site publié en SSG avec microCMS et Cloudflare Pages en utilisant cette approche !

Bien que Cloudflare soit devenu populaire de manière étrange auprès du grand public, c'est vraiment une demande de nos clients que nous testons depuis peu sur notre propre site ! https://www.liberogic.jp/topics/

Cas d'usage adaptés à cette approche

  • Vous souhaitez afficher un classement des 10 meilleurs articles dans la barre latérale ou le pied de page
  • Vous n'avez pas besoin d'une mise à jour en temps réel
  • Votre quota d'API microCMS est limité
  • Implémentation simple avec génération de site statique (SSG)

Étape 1 : Préparation de microCMS (extension de schéma et contrôle Webhook)

1-1. Extension du schéma API

Ajoutez deux champs à votre point de terminaison d'articles existant pour le classement.

  • pageView (nombre) : stocke les vues cumulées des 30 derniers jours obtenues à partir de GA4.
  • lastUpdatedPV (date/heure) : enregistre la date et l'heure de la dernière mise à jour des vues.

1-2. Configuration des permissions de clé API

Cochez PATCH dans les paramètres de la clé API que vous utilisez.

1-3. Configuration du Webhook (stratégie pour éviter les constructions répétées)

Pour éviter que des constructions répétées ne se déclenchent lors de la mise à jour du nombre de vues de chaque article, décochez « Contenu publié (opérations via API) » dans les déclencheurs du Webhook.

Étape 2 : Configuration de l'authentification dans Google Cloud Console

Pour accéder à partir de Cloudflare Workers, préparez un compte de service et l'API Data.

2-1. Activation de l'API

Dans le projet Google Cloud Console, activez l'API Google Analytics Data.

2-2. Création du compte de service et de la clé

Créez un compte de service pour l'intégration GA4 et copiez son adresse e-mail.

Dans l'onglet Clés, cliquez sur « Ajouter une clé » > « Créer une nouvelle clé », sélectionnez le type JSON et téléchargez-la.

2-3. Attribution des autorisations GA4

Dans « Gestion des accès » de la propriété GA4, ajoutez l'adresse e-mail notée à l'étape 2-2 en tant qu'utilisateur et accordez une autorisation de niveau « Lecteur » ou supérieur.

Étape 3 : Création et configuration de Cloudflare Workers (récupération des données GA4)

3-1. Création de Workers et configuration des variables d'environnement

Créez un nouveau Worker nommé ga4-ranking-updater.

Configurez les variables d'environnement à partir des paramètres. Enregistrez les clés en tant que secrets.

MICROCMS_API_KEY

Clé API microCMS

MICROCMS_API_URL

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

GA4_PROPERTY_ID

ID de propriété GA4

GA4_SERVICE_ACCOUNT_CREDENTIALS

Contenu complet du fichier de clé JSON

GA4_PRIVATE_KEY_BASE64

La valeur -----BEGIN PRIVATE KEY----- du private_key JSON, -----END PRIVATE KEY----- et \ supprimées

3-2. Worker de mise à jour des données GA4

Depuis « Éditer le code », définissez le contenu de worker.js comme suit.

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);
        }
    }
};

Logique principale du code Worker :

  1. Récupération anticipée de la carte : récupération anticipée de la carte des id et slug de tous les articles microCMS.
  2. Récupération des données GA4 : récupération des PV cumulés des 30 derniers jours.
  3. Logique de mise à jour : si des données existent dans GA4, mise à jour avec les nouveaux PV ; sinon, réinitialisation des PV à 0.

3-3. Configuration Cron (mise à jour des données)

Configurez le déclencheur Cron dans l'événement de déclenchement des paramètres sur 0 15 * * *. Exécution quotidienne à minuit, reflétant les données jusqu'à la veille dans microCMS.

Étape 4 : Création et configuration de Cloudflare Workers (déclencheur de compilation)

4-1. Création de Workers et configuration des variables d'environnement

Pour séparer la mise à jour des données du déploiement, créez un Workers dédié à la compilation de site avec un nom comme build-trigger.

Définissez l'URL du hook de compilation de Cloudflare Pages dans la variable d'environnement CLOUDFLARE_PAGES_BUILD_HOOK.

4-2. Code Worker (déclencheur de compilation)

Nous allons implémenter du code pour envoyer une requête POST au webhook de déploiement de Pages.

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. Configuration Cron (déploiement du site)

Nous configurons le déclencheur Cron dans les événements déclencheurs des paramètres avec 10 15 * * *. Comme nous voulons attendre la fin du traitement à l'étape 3 avant d'exécuter, nous commençons le déploiement à 0h10 chaque jour avec une marge de 10 minutes.

Conclusion

Comparé à la création d'une API dédiée pour le classement, la réactivité est légèrement inférieure, mais un délai de 10 minutes est tout à fait acceptable. Ne pas consommer d'API et pouvoir s'exécuter dans l'offre gratuite de Cloudflare, ainsi qu'une configuration simple et autonome avec Cloudflare, représentent de grands avantages !

Auteur de cet article

Passé du DTP au monde du web, il s'est avéré être un « sage des techniques » maîtrisant le markup, le frontend, la direction et l'accessibilité. Actif depuis la fondation de Liberogic, il est devenu une référence incontournable en interne. Récemment, il explore l'optimisation via des prompts IA, se demandant « Pourrions-nous déléguer davantage la conformité en accessibilité à l'IA ? ». Sa technologie et sa réflexion continuent d'évoluer.

Futa

Spécialiste en accessibilité web certifié par l'IAAP (WAS) / Ingénieur markup / Ingénieur frontend / Directeur web

Voir les articles de ce membre

Études de cas