Nos clients nous demandent souvent d'afficher le classement de leurs articles.
Lorsqu'on affiche le classement des articles provenant de microCMS sur un site Jamstack, il est courant de créer une API dédiée.
Toutefois, si vous souhaitez l'implémenter mais qu'aucune API n'est plus disponible,Intégrez directement les pages vues dans le contenu des articles existants.Il existe aussi cette approche.
Cette fois-ci, nous allons présenter un exemple de cette approche sur un site publié sur SSG utilisant microCMS + Cloudflare Pages !
Cependant, Cloudflare est devenu connu du grand public d'une manière étrange, mais il s'agit en fait de quelque chose que nous testons sur notre site web depuis un certain temps déjà, en réponse aux demandes de nos clients !https://www.liberogic.jp/topics/
Quand cette approche est-elle appropriée ?
- Si vous souhaitez afficher un classement des 10 meilleurs dans la barre latérale ou le pied de page
- Lorsque les performances en temps réel ne sont pas requises
- L'API microCMS n'a pas sa place.
- Si vous souhaitez une implémentation simple utilisant la génération de sites statiques (SSG)
Étape 1 : Préparer le microCMS (extension de schéma et contrôle par webhook)
1-1. Extension du schéma de l'API
Ajoutez deux champs de classement au point de terminaison d'article existant.
pageView(Nombre) : Stocke le nombre cumulé de pages vues au cours des 30 derniers jours, obtenu à partir de GA4.lastUpdatedPV(Date et heure) : Enregistre la date et l'heure de la dernière mise à jour du PV.
1-2. Configuration des autorisations de la clé API
Dans les paramètres de la clé API que vous souhaitez utiliserPATCHCochez la case.
1-3. Paramètres Webhook (une solution de contournement pour les builds continus)
Pour éviter que plusieurs compilations ne se produisent lors de la mise à jour du nombre de vues de chaque article, ajoutez ce qui suit au déclencheur Webhook :Publication de contenu (opérations API)"Décocher".
Étape 2 : Configurer l’authentification dans la console Google Cloud
Pour y accéder depuis Cloudflare Workers,Compte de serviceetAPI de donnéesPréparez les éléments suivants.
2-1. Activation de l'API
Dans votre projet Google Cloud Console,Google Analytics Data APIActiver
2-2. Création d'un compte de service et d'une clé
Créez un compte de service pour l'intégration GA4 et copiez l'adresse e-mail.
Dans l'onglet Clés, cliquez sur « Ajouter une clé », puis sur « Créer une nouvelle clé », puis créez et téléchargez une clé de type JSON.
2-3. Octroi d'autorisation GA4
Dans la section « Gestion des accès » de votre propriété GA4, ajoutez l'adresse e-mail notée en 2-2 en tant qu'utilisateur et accordez-lui les privilèges « Visionneur » ou supérieurs.
Étape 3 : Créer et configurer les Cloudflare Workers (acquisition de données GA4)
3-1. Création de nœuds de calcul et définition des variables d'environnement
Ouvriersga4-ranking-updaterCréez-en un nouveau avec un nom comme
Configurez les variables d'environnement dans les paramètres. Enregistrez la clé comme secret.
|
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 |
L'intégralité du contenu du fichier de clé JSON |
|
GA4_PRIVATE_KEY_BASE64 |
À partir de la valeur de la clé privée JSON |
3-2. Agent de mise à jour des données GA4
Cliquez sur « Modifier le code » et remplacez le contenu du fichier worker.js par le texte suivant :
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:
- Obtenez une carte à l'avance: Tous les articles microCMS
idetslugPrécharger la carte. - Acquisition de données GA4: Obtient le PV cumulé des 30 derniers jours.
- Logique de mise à jourSi des données sont présentes dans GA4, mettez-les à jour avec le nouveau PV ; sinon, mettez à jour le PV.
0Réinitialiser à.
3-3. Paramètres Cron (mise à jour des données)
Les déclencheurs Cron sont définis dans les événements de déclenchement des paramètres.0 15 * * *Il s'exécute chaque jour à minuit et reflète les données de la veille dans microCMS.
Étape 4 : Créer et configurer les Cloudflare Workers (déclencheurs de génération)
4-1. Création de nœuds de calcul et définition des variables d'environnement
Pour séparer les mises à jour de données et les déploiements, nous utilisons des Workers dédiés à la construction du site.build-triggerCréez-le avec un nom comme :
Dans les variables d'environnementCLOUDFLARE_PAGES_BUILD_HOOKDéfinissez l'URL du hook de construction des pages Cloudflare sur
4-2. Code du travailleur (déclencheur de construction)
Dans le hook de construction des pagesPOSTImplémentez le code qui envoie la requête.
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. Paramètres Cron (déploiement du site)
Les déclencheurs Cron sont définis dans les événements de déclenchement des paramètres.10 15 * * *Étant donné que nous souhaitons attendre la fin de l'étape 3 avant d'exécuter le déploiement, nous commencerons le déploiement 10 minutes plus tard, à 00h10, chaque jour.
résumé
Bien que moins immédiat que la mise en place d'une API de classement, un délai d'une dizaine de minutes me semble acceptable. L'avantage de ne pas consommer d'API et de pouvoir l'exécuter gratuitement avec Cloudflare, ainsi que la simplicité de configuration offerte par Cloudflare, sont des atouts majeurs !
Il est passé de la PAO au web et est rapidement devenu un expert dans son domaine, maîtrisant le balisage, la conception d'interfaces, la direction artistique et l'accessibilité. Actif dans divers secteurs depuis la création de Liberogic, il est aujourd'hui une véritable encyclopédie vivante au sein de l'entreprise. Récemment, il s'est passionné pour l'amélioration de l'efficacité grâce aux invites, se demandant : « Peut-on davantage s'appuyer sur l'IA pour l'accessibilité ? » Ses technologies et sa réflexion sont en constante évolution.
Futa
Spécialiste certifié en accessibilité web (WAS) IAAP / Ingénieur en balisage / Ingénieur front-end / Directeur web