Klanten vragen vaak om een artikelranglijst op hun website, toch?
Bij een Jamstack-site waarop een ranglijst van microCMS-artikelen wordt getoond, is het gebruikelijk om een speciale API te bouwen.
Maar als het API-quotum al op is en u toch wilt implementeren, is er ook een andere aanpak: de paginaweergaven rechtstreeks in de bestaande artikelcontent schrijven.
In dit artikel laten we zien hoe we deze aanpak hebben geïmplementeerd op een site die met microCMS en Cloudflare Pages als SSG wordt gepubliceerd!
Cloudflare is inmiddels bekend bij een breed publiek, maar ook dit is een aanpak die we een tijdje geleden op onze eigen site hebben getest, eveneens op verzoek van klanten! https://www.liberogic.jp/topics/
Wanneer deze aanpak geschikt is
- Wanneer u een ranglijst van ongeveer de top 10 in de zijbalk of footer wilt tonen
- Wanneer realtime actualiteit niet essentieel is
- Wanneer het API-quotum van microCMS onvoldoende ruimte biedt
- Als u het eenvoudig wilt implementeren met Static Site Generation (SSG)
Stap 1: Microcms voorbereiden (schema uitbreiden en webhook-besturing)
1-1. Het API-schema uitbreiden
Voeg twee velden voor de ranglijst toe aan het bestaande artikel-endpoint.
pageView(getal): slaat het totale aantal paginaweergaven van de afgelopen 30 dagen op, zoals opgehaald uit GA4.lastUpdatedPV(datum en tijd): registreert het moment waarop de paginaweergaven voor het laatst zijn bijgewerkt.
1-2. De API-sleutelrechten instellen
Schakel in de instellingen van de te gebruiken API-sleutel PATCH in.
1-3. De webhook instellen (oplossing voor herhaalde builds)
Om te voorkomen dat er steeds opnieuw een build wordt gestart wanneer het aantal paginaweergaven van een artikel wordt bijgewerkt, verwijdert u het vinkje bij "Publicatie van content (via API-bewerking)" in de webhooktriggers.
Stap 2: Authenticatie-instellingen in Google Cloud Console
Om toegang te krijgen vanuit Cloudflare Workers, bereid je een serviceaccount en een Data API voor.
2-1. API inschakelen
Schakel in het Google Cloud Console-project de Google Analytics Data API in.
2-2. Serviceaccount en sleutel aanmaken
Maak een serviceaccount aan voor de GA4-koppeling en kopieer het e-mailadres.
Klik op het tabblad Sleutels op "Sleutel toevoegen" en vervolgens op "Nieuwe sleutel maken". Kies het sleuteltype JSON, maak de sleutel aan en download deze.
2-3. GA4-rechten toekennen
Voeg in het GA4-property onder "Toegangsbeheer" het e-mailadres uit stap 2-2 toe als gebruiker en ken het recht "Lezer" of een hoger recht toe.
Stap 3: Cloudflare Workers aanmaken en instellen (ophalen van GA4-data)
3-1. Het aanmaken van de Worker en het instellen van omgevingsvariabelen
Maak een nieuwe Worker aan met een naam zoals ga4-ranking-updater.
Stel de omgevingsvariabelen in via de instellingen. Registreer de sleutels als secrets.
MICROCMS_API_KEY | microCMS API-sleutel |
|---|---|
MICROCMS_API_URL | https://[ID].microcms.io/api/v1 |
GA4_PROPERTY_ID | GA4-eigenschap-ID |
GA4_SERVICE_ACCOUNT_CREDENTIALS | De volledige inhoud van het JSON-sleutelbestand |
GA4_PRIVATE_KEY_BASE64 | De waarde van private_key uit de JSON, zonder |
3-2. GA4-gegevensupdate Worker
Vervang via "Code bewerken" de inhoud van worker.js door het volgende.
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);
}
}
};
Belangrijkste logica van de Workercode:
- Vooraf ophalen van de mapping: Haal vooraf de mapping van
idenslugop voor alle artikelen in microCMS. - GA4-gegevens ophalen: Haal het cumulatieve aantal paginaweergaven van de afgelopen 30 dagen op.
- Updatelogica: Als er gegevens in GA4 beschikbaar zijn, wordt de waarde bijgewerkt met de nieuwe paginaweergaven. Als die er niet zijn, wordt het aantal paginaweergaven teruggezet naar
0.
3-3. Cron-instelling (gegevensupdate)
Stel de Cron-trigger in via de triggerinstellingen op 0 15 * * *. Deze wordt dagelijks om 00:00 uur uitgevoerd en verwerkt de gegevens tot en met de vorige dag in microCMS.
Stap 4: Cloudflare Workers aanmaken en configureren (buildtrigger)
4-1. Workers aanmaken en omgevingsvariabelen instellen
Om het bijwerken van gegevens en het deployen te scheiden, maakt u een Workers-script voor de sitebuild met de naam build-trigger.
Stel in de omgevingsvariabelen CLOUDFLARE_PAGES_BUILD_HOOK in met de build hook-URL van Cloudflare Pages.
4-2. Workercode (buildtrigger)
Er wordt code geïmplementeerd die een POST-verzoek naar de build hook van Pages stuurt.
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-instelling (sitedeployment)
Stel bij de triggergebeurtenis van de instelling de Cron-trigger in op 10 15 * * *. Omdat de uitvoering moet wachten tot de verwerking van stap 3 is voltooid, start de deployment ruim van tevoren, dagelijks om 00:10 uur 's nachts, dus tien minuten later.
Conclusie
Vergeleken met het aanmaken van een API voor de ranglijst is de actualiteit minder, maar een vertraging van ongeveer tien minuten is naar mijn mening goed acceptabel. Er wordt geen API-quota verbruikt en de uitvoering past binnen het gratis tegoed van Cloudflare. Daarnaast is de opzet eenvoudig, omdat alles binnen Cloudflare plaatsvindt. Dat zijn volgens mij de grote voordelen!
Van DTP naar het web: al snel werkte hij aan markup, frontend, projectregie en accessibility, en beheerst hij alles. Actief bij Liberogic sinds de beginjaren en nu een levende encyclopedie binnen het bedrijf. Recentelijk verdiept hij zich, met prompts als hulpmiddel, in de vraag: "Kunnen we voor accessibility niet meer op AI leunen?" Technologie en denkwijze blijven zich ontwikkelen.
Ayumu Futamata
IAAP-gecertificeerd webaccessibility-specialist (WAS) / Markup-engineer / Frontend-engineer / Webdirecteur