一般にITエンジニアは式年遷宮として開発環境や個人ブログ環境を定期的に整理・再実装する癖があるとされる。 例によって自分も数年ぶりにブログ環境を刷新したので書く。

TL;DR

  • 旧: Hugo + GitHub Pages (organization repo)
  • 新: Astro + Cloudflare Pages (private repo)
  • ついでに別 repo で運用していたブログ/スライド/職務経歴管理repoを統合
  • 普段の更新は git push だけでCloudflareが自動デプロイ

旧構成 (Hugo + GH Pages) と乗り換えた理由

旧構成は Hugo + GitHub Pages (organization repo reizist.github.io)。 静的サイトとして特に何も困ってはいなかったが、作った6-7年前当時、デザイン・設計を自力で行い運用していくほどのモチベーションがなかったのでHugo Templateに依存していた。

またGitHub Pages は public repo でないと無料で使えない (private にして公開するには Pro が必要)ことも考慮ポイントだった。個人的な実験場として機能させたかったのでcommit log等はprivate管理したかった。

Hugoは静的サイトジェネレータとしては非常に扱いやすかったが、表現上の制約は否めなかったため アイランドアーキテクチャなる、自由度高く扱えそうなAstroで推し進めた。

新構成: Astro on Cloudflare Pages

採用したのは Astro 6 + Cloudflare Pages。

  • Cloudflare Pages なら private repo でも無料で deploy できる ─ GH Pages の Pro 課金が不要
    • GitHub Actions すら書かなくていい。 develop に push したら Cloudflare 側が npm run build を走らせて deploy する
  • Astro は SSG として十分速いし、 後で island で動的要素を足せる余地がある
  • MDX が動くので、 Hugo shortcode を Astro component に置き換える戦略がそのまま取れる

ちなみに Cloudflare Pages は普通に高速で、 deploy も 1-2 分で終わる。ほんまに無料でええのんか

Hugo shortcode → MDX component

{{< youtube id="..." >}} のような Hugo shortcode は MDX では当然動かないので、 Astro component に置き換えた。

旧 (Hugo shortcode):

{{< youtube id="SLLgP5lKgDw" autoplay="true" >}}

新 (MDX component):

<Youtube id="SLLgP5lKgDw" autoplay />

<Youtube> / <Amazon> のような component を src/components/ に置いて、 MDX の frontmatter 直下で import する。

---
title: "..."
date: 2024-01-20
---

import Youtube from '../../../components/Youtube.astro';
import Amazon  from '../../../components/Amazon.astro';

<Youtube id="..." autoplay />

<Amazon asin="B0814Y9PYD" title="Flair Signature PRO2" />

動的 OGP — Satori + Resvg

SNS シェアのとき記事ごとに違う OGP 画像が出てほしいので、 build 時に PNG を自動生成するようにした。ひとまず作ったがもうちょい見栄えはなんとかしたい。

astro-og-canvas が文字化け

最初は手っ取り早く astro-og-canvas を使ったが、APIが弱くグラデーションなどの表現幅が少ないようだったのでSatori + @resvg/resvg-js に切り替えた。 React 風の object tree を渡すと SVG が返ってきて、 Resvg で PNG に焼く。

/og/[...slug].ts を Astro の static endpoint として用意して、 getStaticPaths で全記事分の PNG を build 時に static 生成。 Layout 側では og:image / twitter:image を絶対 URL で出す。

cover.jpg があれば背景に乗せる

タイトルだけの単調な OGP が物足りなくなったので、 cover.jpg を frontmatter で指定している記事は cover を全面背景にして暗グラデ overlay + tint wash + タイトル/brand を上重ね する hybrid 設計に変えた。

経歴書をyamlから生成

自分に関連する情報はこのrepoに集約させたかった。 毎度転職などのたびに職務経歴書をその時々のサービス(Findy/Laprasなど)で手動更新をかけていたが、 ようやくyamlからいつでも生成できる環境を作った。 こんなschemaで用意している。 現時点ではpdfは動的吐き出しに対応していないが、そのうちやる予定。

profile:
  name: Reiji Kainuma(海沼 玲史)
  age: 36歳
  location: 神奈川県
  currentCompany: Fez, inc
  highlights:
    - Next.js / FastAPI / LangChain / Google Cloud を用いたBIサービスの新規開発・運用開発
    - 生成AI時代のデータ分析プロセスの再定義(Semantic Layerからの生成AIの自動化、マーケティング分析の自動化)
    - (ターゲットプランニング〜市場分析〜ターゲット抽出〜定量振り返り)への寄与
    - 生成AIを使ったデータ開発(ベクトルデータを用いた新たな軸での商品抽出・ユーザー嗜好の傾向分析)

experiences:
  - start_date: 2023/04/01
    end_date: ""
    company: 株式会社フェズ
    type: 正社員
    projects:
      - name: Urumo BI
        start_date: 2023/10/01
        end_date: ""
        position: フルスタックエンジニア
        management: テックリード
        teamSize: 10-50人
        role: マネージャー
        tech: [Google Cloud, BigQuery, Looker, FastAPI, ChatGPT, LangChain, Claude]
        description: >-
          実店舗の購買データ(IDPOS)を用いたデータ分析によるセールスリフトに貢献するためのBIサービス Urumo BI の立ち上げ後チームを組織、PMチーム/開発チーム/データチームを掌握し日々の業務を行っています。小売・メーカーに対してそれぞれデータ分析(商品分析/顧客分析)、マーケティング用途(顧客抽出)を行う機能を提供し、日々利用状況をモニタリングしながらエンハンス・機能追加を取りまとめています。
        references:
          - type: slide
            label: Fez Cloud Next Tokyo 2024 登壇資料
            url: https://speakerdeck.com/fez_dev/fez-cloud-next-tokyo2024-hureikuautosetusiyond1-da-07
          - type: blog 
            label: 生成AI時代にあるべき開発/データ組織を考える
            url: https://zenn.dev/fez_tech/articles/e57ab59ef2b323
          - type: blog 
            label: 生成AI + BIのプロダクト新規開発について赤裸々に紹介します
            url: https://zenn.dev/fez_tech/articles/5955b9735a1d2a

About ページで実際にjsonを経歴書っぽくレンダリングしている。

Marp スライドを統合

別 repo で管理していた登壇スライドも、 ついでに同じ Astro repo に取り込んだ。 たとえば このスライドページもmarkdownをpushするだけで生成されるようになっている

src/content/slides/<slug>/index.md に Marp markdown を置いて、 build 時に Marp Core で SSR して .deck-viewer__stage の中に inline-SVG として埋め込む。 SpeakerDeck 風の viewer (左右クリック / 矢印キー / Space / Home/End / フルスクリーン / タッチ swipe) を素の JS で書いた。

スライド内の画像参照 (./profile.jpg のような相対パス) は build 時に絶対パス (/slides/<slug>/profile.jpg) に書き換える。 Marp の frontmatter directive (backgroundImage: url('xxx.svg')) も同じ regex で書き換えている。

slide cover を Marp CLI で作るときの罠

OGP は基本 Satori で生成しているが、 「実物のスライド 1 枚目をそのままサムネに使いたい」 ケースがある。 Marp CLI は --image png で 1 枚目 PNG を出してくれる。

ただし Marp CLI は内部で Chromium を使う。 Cloudflare Pages の build 環境には Chromium が入っていない (これは後でまた効いてくる)。 ので、 これは local で動かしてリポジトリに commit する運用にした。

npm run slides:covers   # local で Marp CLI が走り cover.png を生成
git add . && git commit -m "feat(slides): cover" && git push

普段は Satori 生成の /og/slides/<slug>.png で十分なので、 Marp 第一面を使いたいときだけ走らせる。

Cloudflare ビルドに Chromium がない問題

上記で言及した通り、 Cloudflare Pages の build 環境には Chromium が入っていない。 ので Chromium 必須のジョブは local で走らせて成果物を commit する運用に切り出している。

該当するのは2つ。

ジョブコマンド成果物
Resume PDF 生成 (/about /about/en を Puppeteer で焼く)npm run build:fullpublic/resume_{ja,en}.pdf
Slide cover PNG 生成 (Marp 1 枚目)npm run slides:coverssrc/content/slides/<slug>/cover.png

普段の運用は

git add . && git commit -m "..." && git push

だけで終わる。 Cloudflare 側で npm run build (中で OGP 生成 / 画像最適化 / sitemap も走る) が回って自動 deploy。 上の 2 つは数ヶ月に 1 回 (resume を更新する / 新スライドを上げる) のときだけローカルで叩くので、 ストレスはない。

別解として「Cloudflare Pages の代わりに GitHub Actions + Pages Deploy で Chromium 込み build する」 という選択肢もあったが、 Actions の queue 待ち / yaml 書く手間 / private repo の Actions 分数を考えると、 「Chromium ジョブだけ手元で叩いて commit」 のほうが楽だった。

記事作成Skills

たとえば technical writerのように参考になるSkill実装はいくつか見つけられるが、目的/スコープ/ターゲット/品質 を考慮したとき、書きたいものがそのまま出てくるということは基本的にない。 自分的には日本語能力の低さをカバーする目的でセクションの設計と概要を伝えてひとまずの初稿をmarkdownでもらえるような実装として自分に向けてチューニングしたものを配置している。 推敲コストが下がることを期待しつつ何回か回してみたい。

結論

  • private repo + 自動 deploy + GH Actions 不要 を欲しい人には Cloudflare Pages + Astro が刺さる
  • スライド / 職務経歴書などたまに必要になる情報を集約し公開しやすい仕組みにした。
  • 一通りのリプレイスは2-3h で完了した。

数年ぶりに環境刷新したので、 次のブログを書き始める敷居がだいぶ下がった。 これでまたしばらくは書きやすい状態が続くはず。