Silent Foreign Perspective

ブログ式年遷宮 2025年春


前回記事で書いた通り新しくしたくなったのでやった。

以前の技術スタックの問題点

実装したときに書いたものはここ

  • SvelteKit
    • SSGでtrailingSlashが無効にできないとか
      • ファイル名が+page.svelte固定なのがよくなさそう
    • リロードすると404になるとか
      • これは本当になんでかよくわかってない
  • SMUI
    • ぜんぜんメンテされとらん
    • 別にボタンとサイトタイトルにしか使ってない
  • MicroCMS
    • そんなに不満ない、最近料金プランの大改訂とかあって不安なくらい
    • リッチテキストエディタで画像ドラッグアンドドロップして見えるようにしてくれるのすごいと思う
    • 見出しのリンクIDがランダム英数字なのはちょっとキモい
  • Vercel
    • ビルドがちょい遅い
      • そもそもブログなのにSSRしか使えてなかったのが問題

移行後の技術スタック

AIエージェント:Cline + Claude 3.7 Sonnet thinking

そろそろ触れないと仕事がなくなっちゃうなと思いお試しで触ってみた。既に広く知られている機能を実装するとか、SvelteKitの旧プロジェクトのルートに沿って対応するページを作成させるとか、時間だけかかる部分はかなりお任せできる印象。ruleファイルとかは全然整備してないけど、とりあえず何度もミスったら止まって判断を求めろという指示は言っておいて損がない気がする。ただ個人開発で利用するのは流石に金銭的なハードルが高い。

パッケージマネージャ:Bun

なんとなく使ってみたけどインストールがマジで爆速でちょっとクセになるかも。実行時に--bunオプションとかは付けてないので裏は普通にnodeが動いてそうだけどあんまり気にしてない。

フレームワーク(断念):HonoX

前回記事でこれやりたいな~とか言っていたが結局全く使いこなせなかった。Bun + HonoXみたいな構成だったがmicrocms-js-sdkが動かず、解決できなかった。たぶんCJSとESMの問題とかで、完全に自分のJavaScript力不足のせいではある。

フレームワーク:Astro

検索すると変な3次元男の群れ出てくるの勘弁してくれ!!!
HonoXがダメになりさてどうするかというところで、静的生成で既に広く使われているフレームワークということで選んだ。MDX、ToC、静的ファイルエンドポイントからrobots.txt、sitemapまで欲しい機能が至れり尽くせりですごい。ドキュメントは充実しているけど、.astroで使えるもの、.tsで使えるものなどの違いがわからなかったりして戸惑う部分はあった。もちろんSSGが問題なくできる。

コンテンツ管理:MicroCMS(継続)-> MDX

あんまり書く気しない(マークダウンより普通にWYSIWIGが好き)のだけどせっかくMDXのレンダリングが用意されてるのに使わないのもなーと思って変なことをした。

  1. MicroCMSで記事を管理
  2. ローカルでAPIを叩きダウンロード
  3. レスポンスをMDXに変換
  4. Astroのビルド、デプロイ

というフローになった。これで記事のMDXが手元に残るのでもしサービスが終わってしまっても安心。
2-3を担当するスクリプトはAIに書かせた。このぐらいのだと利用ライブラリ選んで何回か会話往復するだけで出来てかなり魂が震える。あとBun使っちゃった都合でCLI動くか不安だったのでpkgでバイナリにまとめさせたりした。
画像やYouTube埋め込みなんかはまだMicroCMSのものをそのまま使っている。ただ抜き取りスクリプトで使ったrich-editor-to-markdown-parserはMDX用ではなくiframeみたいなタグは無視して消したりしてきて、そこを回避するのもAIにやらせたりしてた。

デザイン:AstroのビルトインCSS

shadcn/uiみたいな選択肢もあったがTailwind CSS逆張りオタクのためスルー。スコープ付きCSSがべた書きできるやつ、Vue.jsもSvelteもあるけどかなりとっつきやすくて好き。 最初はRadix UI / Themes入れてたけど結局ボタンしか使わなかったので、Chrome DevToolsでボタンのスタイルコピーしてべた書きに移行、使う機能だけ実装して何とかした。最終的なコンポーネントは以下。

// Button.astro
---
interface Props {
  variant?: "outline" | "filled";
  disabled?: boolean;
  class?: string;
}

const { variant = "outline", disabled = false, class: className, ...rest } = Astro.props;
---

<button class:list={["button", variant, className]} disabled={disabled} {...rest}>
  <slot />
</button>

<style>
  .button {
    all: unset;
    text-indent: initial;
    -webkit-tap-highlight-color: transparent;
    cursor: default;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    user-select: none;
    vertical-align: top;
    font-family:
      -apple-system, BlinkMacSystemFont, "Segoe UI (Custom)", Roboto, "Helvetica Neue", "Open Sans (Custom)", system-ui, sans-serif, "Apple Color Emoji",
      "Segoe UI Emoji";
    font-style: normal;
    text-align: center;
    height: 32px;
    border-radius: 4px;
    box-shadow: inset 0 0 0 1px #9000a56e;
    color: #730086c1;
    gap: 8px;
    font-size: 14px;
    line-height: 20px;
    padding-left: 12px;
    padding-right: 12px;
    font-weight: 500;
    touch-action: manipulation;
  }
  .button.outline {
    box-shadow: inset 0 0 0 1px #9000a56e;
    color: #730086c1;
  }
  .button.outline:hover {
    background-color: #c000c008;
  }
  .button.outline:focus-visible {
    outline: 2px solid #cf91d8;
    outline-offset: -1px;
  }
  .button.outline:active {
    background-color: #cc00cc14;
  }
  .button.filled {
    background-color: #ab4aba;
    color: #fff;
  }
  .button:disabled,
  .button:disabled:hover,
  .button:disabled:focus-visible,
  .button:disabled:active {
    cursor: not-allowed;
    color: #08003145;
    box-shadow: inset 0 0 0 1px #10003332;
    background-color: transparent;
  }
</style>

実際これでCSSのバンドルサイズとかも減るしいいんじゃないかと思う(追記:大量にCSS変数が定義されているデザインライブラリ使うよりは、という話で、1クラス1プロパティのTailwindよりはデカくなりそう)。結局既存ライブラリ参考にヘッドレスUIにスタイル当てるみたいなもんだし。あと余談として、Drawer使いたいと思ったんだけどRadix UIになく、仕方なくshadcn/uiが使ってるvaulで実装することまで決めてAIに「vaulを使ってレスポンシブなドロワーコンポーネントを作って」と指示投げたらインポートだけしてvaul未使用のコンポーネントをお出ししてきたのはかなりウケた。vaulも結構機能過多だったし結果オーライ。

ホスティング:Cloudflare Pages

ドメインもCloudflareだし、将来的に画像を手元に持ってくるにしても転送料無料なのが嬉しいなと思ったため(絶対に気にする必要ないけど)。SSGになったのでwrangler deployがファイルをアップしてるだけっぽく一瞬で終わるのも嬉しい。一括リダイレクト([project-name].pages.devから独自ドメインへ)の設定だけちょっと見つからず迷った。

新しく実装した機能

OGP

みてみてみて!!!

テンプレだけどこれが出るとテンション上がるので嬉しい。ちゃんとBudouXも入れた。入れる前は下みたいな感じ、本当にJavaみたいになって面白かった。

実装はここここを大いに参考にした。特にwbrが機能しなかったあたりでめっちゃ詰まったので先人に感謝。あとAstroの静的ファイルエンドポイントは.tsxに対応していないので画像のコンポーネントを返すやつと画像自体のエンドポイントはファイルを分けないといけないあたりで1敗したりした。今考えるとAstroがReactのレンダリングどうやってるかとかに思いを馳せればすぐ気付けそうな気もするが。
アイコン部分は全然上手くいかなくて、試行錯誤した結果がこれ。

// ogImage.tsx 抜粋
<div style={{ display: 'flex', flexDirection: 'row' }}>
    <div style={{
        display: 'flex',
        position: 'relative',
        boxSizing: 'border-box',
        backgroundColor: 'black',
        width: '34px',
        height: '34px',
        clipPath: 'polygon(10% 100%, 52% 0, 100% 42%)',
    }}>
        <div style={{
            position: 'absolute',
            backgroundColor: 'yellow',
            top: 0,
            left: 0,
            width: '100%',
            height: '100%',
            clipPath: 'polygon(7% 97%, 49% 3%, 97% 39%)',
        }}>
        </div>
    </div>
    <div style={{ fontSize: 32, marginLeft: '12px', fontFamily: 'Roboto' }}>Silent Foreign Perspective</div>
</div>

無理やりなclipPathが泣ける。本当はもっと輪郭が黒く出るつもりだったけど全然上手くいかずだったので妥協。画像埋め込みも縮小が入って汚くなるし、そもそもtsxでnode:fsとか使えずローカル画像の埋め込みが出来なかったりしたのでこんな感じ。誰か教えてくれ。

ページ内検索

Pagefindは前から知っていて、SvelteKitでもある程度機能することは確認できていたのだけどSSRだとルーティングとプリレンダリングされるファイルの場所が違ったりしてややこしかったので見送ってた。今回SSG導入できたので満を持して実装。本番デプロイしたての時は不安定だったが今見たら普通に動いてて謎。

Iframeの遅延読み込み(Remark Plugin)

これは前も実装していて、MicroCMSからのレスポンスをcheerioで事前処理とかしてた。今回はMicroCMS -> MDX -> HTMLになるのでどこで処理を差し込むか迷ったけど、何となくチャレンジしてみたくて2段階目でやった。

// remark-tweak-iframe.ts
import type { Root } from "mdast";
import type { MdxJsxFlowElement } from "mdast-util-mdx";
import { visit } from "unist-util-visit";

export default function remarkTweakIframe() {
    return (tree: Root) => {
        visit(tree, "mdxJsxFlowElement", (node: MdxJsxFlowElement) => {
            if (node.name === "iframe") {
                node.attributes.push({
                    type: 'mdxJsxAttribute',
                    name: 'loading',
                    value: 'lazy',
                });
            }
        });
    };
}

AstroのRemark / Rehypeプラグイン機能に助けられて最終的なコード量はかなり少なくなった。けどここに辿り着くまでに結構調べものした。なんかrun devだとこれらの機能が初回しか走らないとか色々あったような気がする。

おわり

あとは画像のスタイル調整したりとか、既存の記事のアンカーを修正するとか本当につまらないことしか残ってない感じ。なんか労働でインプット / アウトプットちゃんとやらないといけなそうな雰囲気もあるので練習がてらコードと記事を書いてみた。こんなんで練習になってるか?
AIの手助けもあって1週間かからず終わったのは結構すごいと思う。労働を奪ってくれ