Topics

Wie man microCMS-Artikelrankings aus GA4 ausgibt, ohne die API zu verwenden

  • column

Kunden bitten uns häufig, die Platzierung ihrer Artikel in den Suchergebnissen anzuzeigen.
Wenn Artikelrankings von micorCMS auf einer Jamstack-Website angezeigt werden sollen, ist es üblich, eine eigene API zu erstellen.
Sollten Sie es jedoch implementieren wollen, aber keine API mehr verfügbar sein,Schreiben Sie Seitenaufrufe direkt in den bestehenden Artikelinhalt.Es gibt auch diesen Ansatz.

Dieses Mal stellen wir Ihnen ein Beispiel für diesen Ansatz an einer Website vor, die auf SSG mit microCMS + Cloudflare Pages veröffentlicht wurde!

Cloudflare ist der breiten Öffentlichkeit jedoch auf eine ungewöhnliche Weise bekannt geworden, dabei testen wir diese Technologie bereits seit einiger Zeit auf unserer Website – auf Wunsch unserer Kunden!https://www.liberogic.jp/topics/

Wann ist diese Vorgehensweise geeignet?

  • Wenn Sie eine Top-10-Rangliste in der Seitenleiste oder Fußzeile anzeigen möchten
  • Wenn Echtzeitleistung nicht erforderlich ist
  • Für das microCMS-API-Kontingent ist kein Platz.
  • Wenn Sie eine einfache Implementierung mit statischer Seitengenerierung (SSG) wünschen

Schritt 1: Bereiten Sie Ihr microCMS vor (Schema-Erweiterung und Webhook-Steuerung)

1-1. Erweiterung des API-Schemas

Fügen Sie dem bestehenden Artikel-Endpunkt zwei Ranking-Felder hinzu.

  • pageView(Zahl): Speichert die kumulierten Seitenaufrufe der letzten 30 Tage, ermittelt über GA4.
  • lastUpdatedPV(Datum und Uhrzeit): Speichert Datum und Uhrzeit der letzten Aktualisierung des PV.

1-2. Festlegen der API-Schlüsselberechtigungen

In den Einstellungen für den zu verwendenden API-SchlüsselPATCHKreuzen Sie das Kästchen an.

1-3. Webhook-Einstellungen (eine Umgehungslösung für kontinuierliche Builds)

Um zu verhindern, dass beim Aktualisieren der Seitenaufrufe für jeden Artikel mehrere Builds auftreten, fügen Sie dem Webhook-Trigger Folgendes hinzu:Veröffentlichung von Inhalten (API-Operationen)"Häkchen entfernen."

Schritt 2: Konfigurieren Sie die Authentifizierung in der Google Cloud Console.

Um von Cloudflare Workers aus zuzugreifen,ServicekontoUndDaten-APIBereiten Sie Folgendes vor.

2-1. API aktivieren

In Ihrem Google Cloud Console-Projekt,Google Analytics Data APIAktivieren

2-2. Erstellen eines Dienstkontos und eines Schlüssels

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

Klicken Sie im Menüpunkt „Schlüssel“ auf „Schlüssel hinzufügen“ und dann auf „Neuen Schlüssel erstellen“. Erstellen und laden Sie anschließend einen Schlüssel vom Typ JSON herunter.

2-3. GA4-Berechtigungserteilung

Fügen Sie im Abschnitt „Zugriffsverwaltung“ Ihrer GA4-Property die unter 2-2 genannte E-Mail-Adresse als Benutzer hinzu und erteilen Sie ihr mindestens die Berechtigung „Betrachter“.

Schritt 3: Cloudflare Workers erstellen und konfigurieren (GA4-Datenerfassung)

3-1. Erstellen von Mitarbeitern und Festlegen von Umgebungsvariablen

Arbeiterga4-ranking-updaterErstellen Sie eine neue mit einem Namen wie

Legen Sie die Umgebungsvariablen in den Einstellungen fest. 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-Objekt-ID

GA4_SERVICE_ACCOUNT_CREDENTIALS

Der gesamte Inhalt der JSON-Schlüsseldatei

GA4_PRIVATE_KEY_BASE64

Aus dem JSON-Wert private_key-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\\nENTFERNT

3-2. GA4 Datenaktualisierungs-Worker

Klicken Sie auf „Code bearbeiten“ und ändern Sie den Inhalt von worker.js 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);
        }
    }
};

Hauptlogik des Worker-Codes

  1. Besorgen Sie sich im Voraus eine Karte.Alle microCMS-Artikelid Und slugKarte vorab laden.
  2. GA4-Datenerfassung: Ruft die kumulierten Seitenaufrufe der letzten 30 Tage ab.
  3. AktualisierungslogikFalls in GA4 Daten vorhanden sind, aktualisieren Sie diese mit dem neuen PV; andernfalls aktualisieren Sie den PV.0Zurücksetzen auf.

3-3. Cron-Einstellungen (Datenaktualisierung)

Cron-Trigger in den Triggerereignissen der Einstellungen0 15 * * *Es wird täglich um Mitternacht ausgeführt und spiegelt die Daten des Vortages in microCMS wider.

Schritt 4: Cloudflare Workers erstellen und konfigurieren (Build-Trigger)

4-1. Erstellen von Mitarbeitern und Festlegen von Umgebungsvariablen

Um Datenaktualisierungen und -bereitstellungen zu trennen, verwenden wir Worker, die speziell für den Website-Aufbau zuständig sind.build-triggerErstellen Sie es mit einem Namen wie:

In UmweltvariablenCLOUDFLARE_PAGES_BUILD_HOOKLegen Sie die Build-Hook-URL für Cloudflare Pages fest auf

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

Im Pages-Build-HookPOSTImplementieren Sie den Code, der die Anfrage sendet.

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-Einstellungen (Standortbereitstellung)

Cron-Trigger in den Triggerereignissen der Einstellungen10 15 * * *Da wir den Abschluss von Schritt 3 abwarten möchten, bevor wir die Bereitstellung durchführen, starten wir die Bereitstellung jeden Tag 10 Minuten später um 0:10 Uhr.

Zusammenfassung

Auch wenn es weniger schnell geht als die Einrichtung einer Ranking-API, halte ich eine Dauer von etwa 10 Minuten für akzeptabel. Die Kosteneffizienz, keine API zu nutzen, die Möglichkeit, es im kostenlosen Tarif von Cloudflare auszuführen, sowie die einfache Konfiguration mit Cloudflare sind große Vorteile!

Verfasst von

Er wechselte von Desktop-Publishing in die Webwelt und entwickelte sich schnell zu einem wahren Meister seines Fachs mit umfassenden Kenntnissen in Markup, Frontend-Design, Webentwicklung und Barrierefreiheit. Seit der Gründung von Liberogic ist er in verschiedenen Bereichen aktiv und gilt im Unternehmen mittlerweile als wandelndes Lexikon. In letzter Zeit beschäftigt er sich intensiv mit Effizienzsteigerungen durch den Einsatz von Eingabeaufforderungen und fragt sich: „Können wir uns bei der Barrierefreiheit stärker auf KI verlassen?“ Seine Technologien und sein Denken entwickeln sich stetig weiter.

Futa

IAAP-zertifizierter Spezialist für Webzugänglichkeit (WAS) / Markup-Ingenieur / Frontend-Entwickler / Webdirektor

Artikel dieses Mitarbeiters ansehen

Fallstudie