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 |
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 :
- Récupération anticipée de la carte : récupération anticipée de la carte des
idetslugde tous les articles microCMS. - Récupération des données GA4 : récupération des PV cumulés des 30 derniers jours.
- 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 !
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