このブログでブログカードを作成できるようにしてみた。ブログカードというのは以下のようなやつのことだ。
このブログは Zola でビルドしているが、Zola 自体には特にこのような機能がないので自分で実装する必要がある。
例えば同じ静的サイトジェネレータである Hugo には GetRemote
という関数があり、これを通してリンク先のタイトル等を取得できるようになっているらしい。なので、あとは頑張ってCSSを書けばカードが作れるということになる。
Zola にもリンク先の html を取得できる load_data
という関数があるのだが、残念ながらこの関数は html を文字列として返すだけで、要素のパースは行ってくれない。
ということで Zola をフォークし、load_data
のオプションとして html をパースする機能を追加した。流石にあらゆる状況に対応することはできないので、当面は OGP が設定されている場合のみまともな結果を返すようにしている。ざっくり以下の関数を新規に追加し、対応するインターフェースを修正した。
fn load_html(html_data: String) -> Result<Value> {
let document = libs::scraper::Html::parse_document(&html_data);
let properties = ["title", "description", "image", "url", "type", "site_name"];
let mut m = Map::new();
for property in properties {
let meta_selector =
libs::scraper::Selector::parse(&format!(r#"meta[property="og:{}"]"#, property))
.map_err(|e| format!("{:?}", e))?;
let meta = match document.select(&meta_selector).next() {
Some(node) => match node.value().attr("content") {
Some(text) => text,
None => "",
},
None => "",
};
m.insert(property.to_string(), meta.into());
}
let html_content = m.into();
Ok(html_content)
}
ちょっと抽象度が低いので、Zola 本体にPRを送るのは躊躇われる。 リンク先へのリクエストと html のパースでビルド時間が伸びてしまうことも難点。
これを使って以下のような shortcode (linkcard.html
) を書いた
{% set data = load_data(url=url, required=false, format="html") %}
{% if data["title"] %}
{% set title = data["title"] %}
{% elif title %}
{% set title = title %}
{% else %}
{% set title = "" %}
{% endif %}
{% if data["site_name"] %}
{% set site_name = data["site_name"] %}
{% elif site_name %}
{% set site_name = site_name %}
{% else %}
{% set site_name = "" %}
{% endif %}
<figure class="blogcard">
<a href="{{ url }}" target="_blank" rel="noopener noreferrer" aria-label="記事詳細へ(別窓で開く)">
<div class="blogcard-content">
<div class="blogcard-image">
<div class="blogcard-image-wrapper">
{% if data["image"] %}
<img src={{ data["image"] }} alt={{ title }} width="100" height="100" loading="lazy">
{% elif img_url %}
<img src={{ img_url }} alt={{ title }} width="100" height="100" loading="lazy">
{% else %}
<div class="flex h-[120px] w-[120px] max-w-[230px] bg-gray-300 justify-center items-center text-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" class="h-8 w-8">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg>
</div>
{% endif %}
</div>
</div>
<div class="blogcard-text">
<div class="blogcard-title">{{ title }}</div>
<div class="blogcard-footer">{{ site_name }}</div>
</div>
</div>
</a>
</figure>
あまり網羅的なパーサーにはしていないので、所望の要素が取得できないときは、手動で title
等を設定できるようにしている。
これで、各記事に
{{ linkcard(url="https://yng87.page/blog/2021/t-test-sample-size/") }}
と書くことでカードを生成できるようになった。
ちなみに、ローカルで記事の見栄えを確認する際に zola serve
とすると、一つの記事を修正するたびに全ページのビルドが走り、ブログカードの生成に時間がかかってしまう。これは zola serve --fast
とすることで、回避できる。
Github Actions で Zola フォークのビルドから Cloudflare へのデプロイまでするようにした。
Cloudflare へのデプロイは公式ドキュメント通りにやれば良い
Workflow は以下。Zola のフォークを自前でビルドする必要があるが、毎回やると時間がかかるのでキャッシュを使うようにしている。
on:
push:
branches:
- main
paths-ignore:
- README.md
- LICENSE
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
name: Deploy to Cloudflare Pages
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get commit hash of forked Zola
id: gethash
run: |
echo "hash=$(curl --silent https://api.github.com/repos/yng87/zola/commits/html-parse | jq -r '.sha')" >> $GITHUB_OUTPUT
- name: Cache forked Zola
id: zola_cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/zola
key: ${{ steps.gethash.outputs.hash }}
- name: Cargo install forked Zola
if: steps.zola_cache.outputs.cache-hit != 'true'
run: |
cargo install \
--git https://github.com/yng87/zola.git \
--branch html-parse
- name: Build
run: zola build
- name: Publish
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: hoge
directory: fuga
gitHubToken: ${{ secrets.GITHUB_TOKEN }}