Hexo Icarus 顯示瀏覽量(三):Hack Icarus 頁面,串接瀏覽量 API

上一講透過 Google Analytics 加上 AWS Lambda,我們完成了透過 Page Path 來取得 View Count 的 API,接下來便是將瀏覽量顯示在 Hexo Icarus 主題中的預覽及內文了。

由於 Icarus 本身提供的設定檔支援的是不蒜子,和我使用 Google Analytics 作為瀏覽量資料庫的方式不同,只能另尋他法。

在瀏覽一些其他的實作方法後,發現有些方式能夠透過修改 Icarus 的模板來加上自訂內容,然而在當下專案我所使用的 hexo 5.4 和 icarus 4.3 並沒有發現類似的模板可供修改,直接進入 node_modules 修改 Icarus 產出的頁面顯然會在重新安裝時被覆蓋,不是個太好的辦法。

因為我想加入的自訂內容僅僅是「瀏覽數」而已,所以在不變更 Hexo、Icarus 的版本前提下,我選擇了一個可能也不太「乾淨」的方法來達成目的,Hack Hexo Icarus 的頁面的 HTML 來插入內容。

方案選擇及其原理

在我開發前端以及撰寫爬蟲的經驗中,時常會需要在頁面上透過 XPath 或 CSS Selector 等方式定位某個元件的內容,然後做樣式的修改或是模擬點擊等操作。

那麼,只要有方法可以讓我取得 HTML 的 DOM,或是插入一段 JavaScript 的程式碼即可達成目的。

插入 HTML Element

剛好在 Hexo 5.0.0 版本之後有一個新功能稱作 Injector,讓我們可以將程式碼塞入 HTML 的 <head><body> 當中。

如此一來,我們只要準備好一段 JavaScript 的程式碼,在裡面產生瀏覽數的 HTML Element,然後塞入適當的位置,這個功能便算是完成了。

注入 JavaScript File

透過 Hexo 的 Injector,我們可以在專案目錄的 source/ 下新增一個資料夾叫做 js,來存放要被注入的檔案。

所以我新增了一個獲取瀏覽數並產生 HTML 元件的檔案,存成 source/js/view-count.js,裡面可以先很簡單的寫一段 console.log 來驗證功能是否成功。

接著新增一個 scripts/ 的資料夾在專案目錄下(如果不存在的話),在裡面新增叫做 inject-view-count.js 的檔案,裡面寫入 hexo.extend.injector.register('body_end', '<script src="/js/view-count.js"></script>');

Hexo 會讀取 scripts/ 下的內容並執行。

注入 JavaScript 程式碼在 body 的末端

當我們將 Hexo 跑起來時,便能看到 view-count.js 被塞入 HTML 中,並且裡面的內容被成功執行!

撰寫 view-count.js:串接 API 及產生瀏覽數 Element

成功 Hack 進頁面的 HTML 後,再來就是將功能完成,第一步是定位需要顯示瀏覽數的元件。

在觀察 Icarus 的頁面呈現後,我發現有兩個地方會顯示發表及更新時間、字數等資訊,也就是在這些元件後方,是我預計加上瀏覽數的位置。

這兩個地方都是以卡片呈現,差別在於第一個地方是瀏覽多篇文章的分頁,另一個則是點選文章後的內文。

顯示多篇文章的分頁

兩處位置的共通點,在 Icarus 中都是以卡片的形式呈現,也都可以透過 .card:has(h1.title) 的 Selector 語法來找到這些卡片,在文章內文中可以選取到一張卡片,而分頁中則能選取到多張。

選擇到卡片之後,再來就是定位要插入瀏覽量的位置,可以透過卡片下的 .level-left class 來取得這個區塊,在建立好瀏覽量元素後 .appendChild 在此區塊的後面。

文章資訊區塊

最後,我們需要獲取這篇文章的 Page Path 當成參數送往我們所撰寫的 View Count API。這在瀏覽單篇文章的時候沒有什麼問題,只要透過 window.location.pathname 獲取當前頁面的 Path 即可。

然而在文章分頁的地方需要換種做法,我們知道文章卡片的標題有超連結可以前往內文,因此使用 .title a 來定位標題的超連結內容,就能取得我們所需 Path。

如此一來,無論在分頁或內文,都能夠在文章資訊的區塊看到瀏覽量了。

附錄:完整 view-count.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
document.addEventListener('DOMContentLoaded', async () => {
const articleCards = selectArticleCards();
const pagePathList = extractPagePathList(articleCards);
initViewCountElements(articleCards);
await updateCardsViewCount(pagePathList, articleCards);
});

const selectArticleCards = () => {
return document.querySelectorAll('.card:has(h1.title)');
};

const extractPagePathList = (articleCards) => {
return Array.from(articleCards).map(getPagePath);
};

const initViewCountElements = (articleCards) => {
for (const card of articleCards) {
const viewCountDiv = document.createElement('div');
viewCountDiv.className = 'view-count';
viewCountDiv.textContent = `瀏覽量 --`;
card.querySelector('.level-left')?.appendChild(viewCountDiv);
}
};

const updateCardsViewCount = async (pagePathList, articleCards) => {
const response = await fetch('https://my-view-count-api.lambda-url.ap-southeast-1.on.aws/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pagePathList }),
});
const { pageCountMap } = await response.json();

for (const card of articleCards) {
const pagePath = getPagePath(card);
const viewCountDiv = card.querySelector('.view-count');
if (!viewCountDiv) continue;
viewCountDiv.textContent = `瀏覽量 ${pageCountMap[pagePath] || '--'}`;
}
};

const getPagePath = (card) => {
const link = card.querySelector('.title a');
if (link) return link.getAttribute('href'); // Only article list has link
return window.location.pathname; // Article itself
};

參考資料

  1. Hexo - Injector

  2. 不蒜子

  3. Zhh Blog - Hexo Icarus + Golang + Google Analytics