English

Zolaだけでやりきるブログカード生成

ブログカードを作成できるようにした

このブログでブログカードを作成できるようにしてみた。ブログカードというのは以下のようなやつのことだ。

t検定で必要なサンプルサイズを求める方法

このブログは 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 でビルドする

Github Actions で Zola フォークのビルドから Cloudflare へのデプロイまでするようにした。

Cloudflare へのデプロイは公式ドキュメント通りにやれば良い

Use
Use Direct Upload with continuous integration · Cloudflare Pages docs

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 }}

参考にしたサイト

Hugoでついに外部URLのブログカードを作れるようになった【自作ショートコード】
Hugoでついに外部URLのブログカードを作れるようになった【自作ショートコード】 | Hugoブログテーマ「Salt」
Hugoだけでやりきるブログカード生成
Hugoだけでやりきるブログカード生成