客戶經常要求我們展示他們文章的排名。
在 Jamstack 網站上顯示來自 micorCMS 的文章排名時,通常會建立一個專用 API。
但是,如果您想實現該功能,但已沒有可用的 API 了,將頁面瀏覽量直接寫入現有文章內容還有這種方法。
這次,我們將介紹一個使用microCMS + Cloudflare Pages在SSG上發布的網站上採用這種方法的範例!
然而,Cloudflare 以一種奇怪的方式為公眾所知,但實際上,這是我們應客戶要求,在我們的網站上測試了一段時間的功能!https://www.liberogic.jp/topics/
這種方法在什麼情況下適用?
- 如果您想在側邊欄或頁腳顯示前 10 名排名
- 當不需要即時效能時
- 沒有空間容納microCMS API
- 如果您想要使用靜態網站產生 (SSG) 實作一個簡單的方案
步驟 1:準備微型內容管理系統(模式擴充和 webhook 控制)
1-1. 擴充 API 模式
在現有文章端點中新增兩個排名欄位。
pageView(數位):儲存從 GA4 取得的過去 30 天的累積頁面瀏覽量。lastUpdatedPV(日期和時間):記錄 PV 上次更新的日期和時間。
1-2. 設定 API 金鑰權限
在 API 金鑰的設定中PATCH勾選此方塊。
1-3. Webhook 設定(持續建置的變通方案)
為防止在更新每篇文章的頁面瀏覽量時發生多次構建,請將以下內容新增至 Webhook 觸發器:發佈內容(API 操作)取消選取。
步驟 2:在 Google Cloud 控制台中設定驗證
要從 Cloudflare Workers 訪問,服務帳戶和資料 API請準備以下物品。
2-1. 啟用 API
在您的 Google Cloud Console 專案中,Google Analytics Data API使能夠
2-2. 建立服務帳戶和金鑰
為 GA4 整合建立服務帳戶並複製電子郵件地址。
在“金鑰”標籤中,按一下“新增金鑰”,然後按一下“建立新金鑰”,然後建立並下載金鑰類型為 JSON 的金鑰。
2-3. GA4 許可授予
在 GA4 屬性的「存取管理」部分,將 2-2 中提到的電子郵件地址新增為用戶,並授予其「檢視者」或更高權限。
步驟 3:建立和設定 Cloudflare Workers(GA4 資料收集)
3-1. 建立工作進程和設定環境變數
勞工ga4-ranking-updater建立一個名為“
在設定中設定環境變數。將密鑰註冊為機密資訊。
|
MICROCMS_API_KEY |
microCMS API金鑰 |
|---|---|
|
MICROCMS_API_URL |
https://[ID].microcms.io/api/v1 |
|
GA4_PROPERTY_ID |
GA4 房產 ID |
|
GA4_SERVICE_ACCOUNT_CREDENTIALS |
JSON 金鑰檔案的全部內容 |
|
GA4_PRIVATE_KEY_BASE64 |
從 JSON 私鑰值 |
3-2. GA4 資料更新工作程序
點擊“編輯程式碼”,將 worker.js 的內容變更為以下內容:
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);
}
}
};
Worker 程式碼的主要邏輯:
- 提前拿到地圖所有microCMS文章
id和slug預先取得地圖。 - GA4 資料收集取得過去 30 天的累積頁面瀏覽量。
- 更新邏輯如果 GA4 中有數據,則使用新的 PV 更新它;否則,更新 PV。
0重置為。
3-3. Cron 設定(資料更新)
設定中的 Cron 觸發器觸發事件0 15 * * *它每天午夜運行,並反映microCMS中前一天的數據。
步驟 4:建立和設定 Cloudflare Workers(建置觸發器)
4-1. 建立工作進程與設定環境變數
為了將資料更新和部署分開,我們使用專門用於網站建置的 Worker。build-trigger創建時可取類似這樣的名稱:
環境變數CLOUDFLARE_PAGES_BUILD_HOOK將 Cloudflare Pages 建構鉤子 URL 設定為
4-2. 工作代碼(建置觸發器)
在 Pages 建構鉤子中POST實作發送請求的代碼。
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 設定(站點部署)
設定中的 Cron 觸發器觸發事件10 15 * * *由於我們希望在執行部署之前等待步驟 3 完成,因此我們將每天凌晨 12:10 開始部署,比原計劃晚 10 分鐘,以便留出一些餘地。
概括
雖然不如準備排名 API 那麼快捷,但如果耗時大約 10 分鐘,我認為是可以接受的。無需使用 API,並且可以在 Cloudflare 的免費套餐上運行,再加上 Cloudflare 的配置非常簡單,這些都是主要優勢!
他從桌面排版領域轉戰網頁設計,迅速成為一位技藝精湛的“大師”,精通標記語言、前端設計、方向指導和無障礙設計。自 Liberlogic 創立以來,他一直活躍於各個領域,如今已成為公司內部的活字典。最近,他沉迷於探索如何利用提示來提高效率,並思考著「我們能否更依賴人工智慧來實現無障礙設計?」他的技術和思維仍在不斷發展。
Futa
IAAP認證的Web無障礙專家(WAS)/標記工程師/前端工程師/網站總監