Topics

Ranking von microCMS-Artikeln aus GA4 ohne API-Verbrauch ausgeben

  • column

Die Anzeige von Artikel-Rankings ist ein häufiger Kundenwunsch.
Bei Jamstack-Websites mit microCMS ist es üblich, ein spezielles API zu erstellen, um Artikel-Rankings anzuzeigen.
Wenn Sie jedoch kein API-Kontingent mehr haben, aber die Implementierung dennoch wünschen, können Sie auch PV-Zahlen direkt in den vorhandenen Artikel-Inhalt schreiben.

Dieses Mal stellen wir ein Implementierungsbeispiel für eine Website vor, die mit microCMS + Cloudflare Pages über SSG veröffentlicht wird!

Cloudflare ist zwar auf seltsame Weise zur Allgemeinheit durchgedrungen, aber das ist eigentlich etwas, das wir auf Wunsch unserer Kunden vor kurzem auf unserer Website getestet haben! https://www.liberogic.jp/topics/

Fälle, in denen dieser Ansatz geeignet ist

  • Wenn Sie etwa TOP 10 Rankings in der Seitenleiste oder Fußzeile anzeigen möchten
  • Wenn Echtzeit-Anforderungen nicht so hoch sind
  • Wenn das API-Kontingent von microCMS begrenzt ist
  • Wenn Sie mit statischer Website-Generierung (SSG) einfach implementieren möchten

Schritt 1: Vorbereitung von microCMS (API-Schema-Erweiterung und Webhook-Steuerung)

1-1. Erweiterung des API-Schemas

Fügen Sie zwei Felder für das Ranking zum bestehenden Artikel-Endpunkt hinzu.

  • pageView (Zahl): Speichert die kumulativen Aufrufe der letzten 30 Tage, die von GA4 abgerufen wurden.
  • lastUpdatedPV (Datum/Uhrzeit): Erfasst das Datum und die Uhrzeit, wann der Seitenabruf zuletzt aktualisiert wurde.

1-2. Konfiguration der API-Schlüssel-Berechtigungen

Aktivieren Sie in den Einstellungen des verwendeten API-Schlüssels das Kontrollkästchen für PATCH.

1-3. Webhook-Konfiguration (Strategie zur Vermeidung wiederholter Builds)

Um zu verhindern, dass wiederholte Builds ausgelöst werden, wenn die Seitenabrufe für jeden Artikel aktualisiert werden, deaktivieren Sie das Kontrollkästchen für Inhalte veröffentlicht (über API-Vorgänge) in den Webhook-Triggern.

Schritt 2: Authentifizierungseinrichtung in der Google Cloud Console

Für den Zugriff über Cloudflare Workers werden ein Service-Konto und eine Daten-API bereitgestellt.

2-1. API aktivieren

Aktivieren Sie Google Analytics Data API im Projekt der Google Cloud Console.

2-2. Dienstkonto und Schlüsselerstellung

Erstellen Sie ein Dienstkonto für die GA4-Integration und kopieren Sie die E-Mail-Adresse.

Erstellen Sie einen neuen Schlüssel über die Schaltfläche „Schlüssel hinzufügen" auf der Registerkarte „Schlüssel", wählen Sie den Schlüsseltyp JSON und laden Sie den Schlüssel herunter.

2-3. GA4-Berechtigungen erteilen

Fügen Sie in der Zugriffsverwaltung der GA4-Eigenschaft die in Schritt 2-2 notierte E-Mail-Adresse als Benutzer hinzu und weisen Sie ihr mindestens die Berechtigung „Viewer" zu.

Schritt 3: Erstellung und Konfiguration von Cloudflare Workers (GA4-Datenbeschaffung)

3-1. Erstellung von Workers und Konfiguration von Umgebungsvariablen

Wir erstellen einen neuen Worker mit einem Namen wie ga4-ranking-updater.

Stellen Sie die Umgebungsvariablen in den Einstellungen ein. Registrieren Sie den Schlüssel als Geheimnis.

MICROCMS_API_KEY

microCMS-API-Schlüssel

MICROCMS_API_URL

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

GA4_PROPERTY_ID

GA4-Eigenschafts-ID

GA4_SERVICE_ACCOUNT_CREDENTIALS

Gesamter Inhalt der JSON-Schlüsseldatei

GA4_PRIVATE_KEY_BASE64

Der Wert von private_key aus JSON mit entfernten -----BEGIN PRIVATE KEY-----, -----END PRIVATE KEY----- und \

3-2. GA4-Datenaktualisierungs-Worker

Bearbeiten Sie den Inhalt von worker.js in „Code bearbeiten" wie folgt.

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

Wichtigste Logik des Worker-Codes:

  1. Vorkarte abrufen: Die id und slug aller Artikel aus microCMS werden voraus abgerufen.
  2. GA4-Daten abrufen: Die kumulierten Seitenaufrufe der letzten 30 Tage werden abgerufen.
  3. Aktualisierungslogik: Falls Daten in GA4 vorhanden sind, werden die Seitenaufrufe aktualisiert; andernfalls werden die Seitenaufrufe auf 0 zurückgesetzt.

3-3. Cron-Einstellung (Datenaktualisierung)

In den Trigger-Ereignissen der Einstellungen wird der Cron-Trigger auf 0 15 * * * gesetzt. Dies führt täglich um 0:00 Uhr aus und reflektiert die Daten bis zum Vortag in microCMS.

Schritt 4: Erstellung und Konfiguration von Cloudflare Workers (Build-Trigger)

4-1. Erstellung von Workers und Einstellung von Umgebungsvariablen

Um Datenaktualisierung und Bereitstellung zu trennen, wird ein dedizierter Worker für das Website-Build mit einem Namen wie build-trigger erstellt.

In der Umgebungsvariablen CLOUDFLARE_PAGES_BUILD_HOOK wird die Build-Hook-URL von Cloudflare Pages festgelegt.

4-2. Worker-Code (Build-Trigger)

Wir implementieren Code, um POST-Anfragen an den Build-Hook von Pages zu senden.

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-Konfiguration (Site-Deployment)

Wir stellen den Cron-Trigger in den Triggerereignissen der Konfiguration auf 10 15 * * * ein. Da wir warten möchten, bis die Verarbeitung in Schritt 3 abgeschlossen ist, starten wir das Deployment mit etwas Spielraum jeden Tag um 00:10 Uhr (10 Minuten später).

Fazit

Im Vergleich zur Erstellung einer speziellen Ranking-API ist die Echtzeit-Verfügbarkeit geringer, aber etwa 10 Minuten sind vollkommen akzeptabel. Der Vorteil liegt in der Kosteneffizienz – keine API-Nutzung, ausführbar im kostenlosen Cloudflare-Plan – und in der einfachen, mit Cloudflare abgeschlossenen Architektur!

Dieser Artikel wurde geschrieben von

Von DTP in die Web-Welt – und dann Markup, Frontend, Projektleitung und Accessibility alles gemeistert: ein "Technik-Weise". Seit den Anfangstagen von Liberogic vielseitig tätig und mittlerweile eine lebende Wissensquelle im Unternehmen. Derzeit fasziniert von der Frage "Können wir Accessibility-Umsetzung noch stärker mit KI unterstützen?" und erforscht Optimierungsmöglichkeiten durch gezieltes Prompt-Engineering. Technisch wie gedanklich immer noch in Entwicklung.

Futa

IAAP-zertifizierter Web Accessibility Specialist (WAS) / Markup Engineer / Frontend Engineer / Web Director

Artikel dieses Mitarbeiters ansehen

Fallstudien