大学のプログラミングサークルのWebサイトをAstro×SolidJSで制作したので技術スタックを紹介
はじめに
私の所属するプログラミングサークルRICORA Programming TeamのWebサイトを@3w36zj6、@sou1118と一緒にAstro×SolidJSでリニューアルしたので、その技術スタックを紹介します。
以下、リニューアルしたWebサイトのリポジトリとURLです。
技術スタック
ランタイム
パッケージマネージャーとテストツールとしてBunを利用し、実際のビルドはNode.jsで行うようにしました。
Bunは、Node.jsに変わる次世代JavaScriptランタイムです。同様のものとしてはNode.jsの作者の開発するDenoがあります。Denoは、Node.jsでの反省を活かし独自のモジュールシステムを持つなど、よりセキュアで前衛的な設計になっているのが特徴です。これに対しBunは、Node.jsとの互換性を重視しつつ、利便性と高速性を目指して開発されています。また、Denoと同様にBunは、JavaScriptランタイムに加えてパッケージマネージャーやテストランナー、バンドラーといった様々なツールを提供しています。
なお、開発当初はビルドを含めて全てをBunで行っていましたが、開発が進むにつれ原因不明のエラー等が発生するようになり辛くなったため、ビルドなど互換性が重要となる部分ではNode.jsを利用するようにしました。
フレームワーク
タイトルにあるように、本WebサイトではSSG用メタフレームワークとしてAstroを採用しました。
Astroはアイランドアーキテクチャを特徴とするメタフレームワークであり、Next.jsやFresh、Lumeなどと良く比較されます。アイランドアーキテクチャとは、ページを静的に生成した後、インタラクティブな要素のみを静的に生成されたHTMLを有効活用しながらハイドレートするアーキテクチャのことです。ハイドレートする対象をインタラクティブな要素に限定し、かつハイドレートにはサーバー側で生成されたHTMLを有効活用するため、通常のSSRに比べハイドレーションコストが低くなります。このアーキテクチャの基礎となる技術としては、ReactのPartial Hydrationなどがあります。実際にNext.jsのApp RouterでもServer Componentsを利用して似たような仕組みが利用されています。
またAstroは、11tyやHugoなどのSSGと異なり、コンポーネントを積み上げることでページを構築する特徴があります。これにより、宣言的にウェブサイトを構築できます。
- 静的サイトの生成をメインに設計されているので、シンプル
- アイランドアーキテクチャによって、必要なタイミングで必要な個所にのみJavaScriptを適用できるので、パフォーマンスの向上が期待される
- MDXによるコンテンツ管理の仕組みがフレームワークレベルで提供されている
- Viteベースなので高速(Turbopack版Next.jsも高速だが)
であるため、本Webサイトのような静的サイトの開発に適していると考え採用しました。
また、UIフレームワークとしてはSolidJSを採用しました。
SolidJSは、仮想DOMを持たないReactのようなフレームワークです。Reactと比べて、Fine-Grained Reactivityによりパフォーマンスが高く、コードがシンプルになるという特徴があります。個人的に今一番推しているフレームワークです。
Reactにするか悩みましたが、Reactのエコシステムをあまり利用する予定がなく、また開発の規模も小さいため何かあれば自分で実装すれば良いと考え、SolidJSにしました。
スタイル
CSSフレームワークとしてはTailwind CSSを採用しました。
Tailwind CSSはユーティリティファーストを掲げるCSSフレームワークであり、flex
や pt-4
といったユーティリティクラスの組み合わせのみでスタイリングを行います。Tailwind CSSはバニラCSSに比べ、
- デザイントークンに基づく値の制約によって、一貫性のあるデザインを実現できる
- CSSの影響範囲が明確になる
- クラス名を考える時間が消える
- HTMLとCSSのファイルを行き来する必要がなくなる
といった特徴があり、高速に開発を進められるので採用しました。Tailwind CSSの生まれた背景や特徴については、以下のリンクに詳しくまとめられています。
また、カラーパレットとしてRadix Colorsを採用しました。これは後述するPark UIで利用されており、今回初めて使用しました。Radix Colorsは、それぞれの色のscaleをどのような場面で使うべきかがドキュメントに記載されている他、ダークモード対応を考慮して色が設計されているため、非常に使いやすかったです。
コンポーネントライブラリ
UIコンポーネントライブラリとしては、Ark UIとPark UIを採用しました。
Ark UIは、Chakra UIの開発するヘッドレスUIコンポーネントライブラリであり、React、Vue、SolidJSに対応しています。SolidJSで利用できる数少ないUIコンポーネントライブラリの1つです。Ark UI以外では、Kobalteも有名です。
Park UIは、Ark UIのメンテナが開発するshadcn/uiのようなもので、スタイル済みのコンポーネントをコピペすることでデザインシステムを一から作らずに済みます。
記事の執筆
MDX
弊サークルの旧Webサイトでは、文法が簡単であることやPlaintextであるためGitでの管理がしやすいこと、移行が容易であることなどの理由から、Markdownを採用していました。しかし、Markdownはその簡易さがゆえに表現力に限界があるという欠点も持ち合わせています。そこで、今回はMarkdownを基にしつつ、Markdown内でJSXを記述できるようにするMDXを採用しました。MDXはMarkdown(CommonMark)の記法が一部使えないものの、概ねCommonMarkに準じています。
MDXでは、AstroやSolidJSで記述されたコンポーネントをファイル内で呼び出して使用できるほか、Custom Components機能などを通じて、通常のMarkdownよりも高い表現力を実現しています。また、Markdownとの高い互換性により、旧Webサイトの記事データをそのまま使用できるという大きな利点がありました。
さらに、今回制作したWebサイトでは通常のMDXの記法に加え、remark-gfmを使いGFMの記法も使えるようにしました。
今回制作したWebサイトで実際に利用できるMDXの記法は、次のリンクから確認できます。
シンタックスハイライト
コードブロックのシンタックスハイライターにはShikiを採用しました。
シンタックスハイライターとしてはPrism.jsやhighlight.jsが有名ですが、リポジトリの更新の活発さ[1]と、VS Codeと同じシンタックスハイライトの文法であるTextMateを採用していることによる将来性[2]の2点を考慮し、Shikiを採用しました。
また、コードブロックのタイトルやDiff表示、行ハイライトなどの機能の追加には、筆者が以前作成したrehype-custom-codeを利用しました。
コードブロックの例
実際のコードブロックの表示は以下のリンクから確認できます。
コールアウト
GitHubで利用可能なアラートのような記法をMDXで利用できるように、remarkのプラグインを自前で実装しました。
> [!note] タイトル
> 本文
次が実際のプラグインのソースコードです。
このプラグインでは、先ほどの"コールアウトの例"のコードを次のように変換します。
<callout calloutType="note" calloutIsFoldable="false">
<callout-title calloutType="note">title here</callout-title>
<p>body here</p>
</callout>
ここで、callout
タグと callout-title
タグに対してそれぞれ Callout
、CalloutTitle
コンポーネントをCustom Componentsとして割り当てることで、コールアウトが表示されるよう実装しました。
ここで作成したプラグインをベースに、より詳細なカスタマイズを可能としたremarkのプラグインをライブラリとして公開したので、良ければ利用してみてください(宣伝)。次がそのリポジトリです。
コールアウトの例
リンクカード
リンクカードはZennと同様に、URLを貼り付けるだけで自動的にOG画像やタイトル、ディスクリプションを取得して表示するように実装しました。
コールアウトと同様に、空白行に挟まれたURLを link-card
タグとして変換するRemarkのプラグインを自前で実装し、それに対応する LinkCard
コンポーネントを作成しました。
OG画像やタイトル、ディスクリプションの取得にはunfurlを利用しました。
リンクカードの例
埋め込み
埋め込みはZennと同様に、埋め込みに対応したURLを貼り付けるだけで自動的に埋め込みを表示するように実装しました。
埋め込みにはoEmbedを利用しました。oEmbedは、サードパーティのサイト上でURLの埋め込み表現を可能にするためのフォーマットです。多くのサイトで利用されており、容易に埋め込みを実装できます。
oEmbedの情報の取得にはunfurlを利用しました。この際、unfurlのoEmbedの型定義が一部不足していたため、unfurlにPRを送りました。
数式
LaTeXのレンダリングにはKaTeXを採用しました。MathJaxの方が対応しているLaTeXの記法が多いため、数式を沢山書く場合はMathJaxの方が良いかもしれません。KaTeXはMathJaxと比較してレンダリングが高速であるため、本WebサイトではKaTeXを採用しました[3]。
アイコン
アイコンフレームワークとしては、Iconifyを採用しました。Zennでもお馴染みのTwemojiも含め、さまざまなオープンソースのアイコンを集めたものであり、これを入れるだけで個別にアイコンをインストールする必要がなくなります。
OG画像の生成
OG画像の生成には、HTMLとCSSからSVGを生成するSatoriを採用しました。これはVercelが開発したもので、HTMLとCSSの知識で簡単に画像を生成できます。使えるCSSは制限されていますが、基本的なものは使えるので、OG画像の生成には十分です。
また、OG画像内の文章の分かち書きにはBudouxを採用しました。これはGoogleが開発したもので、日本語の文章を分かち書きできます。
実際のOG画像の例
検索
ブラウザで完結する全文検索ライブラリとして、Pagefindを採用しました。同様の全文検索ライブラリとしてはtinysearchやFlexSearchなどがありますが、これらはデフォルトで日本語に対応していないため採用しませんでした。
フォント
本文のフォントにはLINE Seed JPを、コードブロックにはJuiseeを採用しました。
CI / CD
GitHub Actions / GitHub Pages
CI / CDにはGitHub Actionsを利用しました。CIはTypeScriptの型チェックとBunによる単体テスト、ビルドテスト、Prettierによるフォーマットチェック、ESLintによるリントチェックを行うようにしました。なお、今後はE2Eテストも追加する予定です。
デプロイにはGitHub Pagesを利用しました。機能は最小限ですが、無料かつGitHubだけで完結するので便利です。
本サイトは学生団体の活動紹介を目的とするため、長期的な運用と管理が求められます。しかし学生団体である特性上、世代交代に伴う引継ぎが避けらません。そこで、可能な限りGitHubのサービスのみを利用することで、アプリケーションの引継ぎがGitHub上での招待のみで完結するようにしました。
textlint
文章校正にはtextlintを採用しました。ルールセットにはtextlint-rule-preset-ja-technical-writingを使用し、意図的にルールを無視する場合のみtextlint-filter-rule-commentsで一時的に無効にする運用をしています。
ここで、@textlint/textlint-plugin-markdownはMDXに対応していないため[4]、JSXがリント対象となってしまい、またルールをコメントで無効化するために、以下のような記述をしなければならないといった課題がありました。
This is error text.
{/* <!-- textlint-disable --> */}
This is ignored text by rule.
Disables all rules between comments
{/* <!-- textlint-enable --> */}
This is error text.
そこで、textlintでMDXをサポートするためのプラグインとして@textlint-plugin-mdxを作成しました。
詳細は以下の記事をご覧ください。
Danger
当リポジトリではIssue駆動開発とConventional Commitsを採用しています。そこでDangerというツールを利用して、作成されたPRのタイトルや本文のフォーマットのチェックを自動化しました。
DangerではDangerfileというスクリプトでルールを管理します。以下は実際に使用しているDangerfileです。
依存関係の更新
依存関係の自動更新には、Renovateを採用しました。package.json
や .tool-versions
に記載された依存関係のバージョンを更新するPRを自動で作成してくれるため、自動的に依存関係を最新の状態に保つことができます。また、マイナーおよびパッチバージョンの更新については自動でマージするなど、柔軟な設定が可能なため非常に便利です。
同様のツールとしてはDependabotがありますが、RenovateはDependabotよりも柔軟な設定が可能である他、より多くの依存関係を記載するファイルのパターンに対応しているため採用しました。実際に今回制作したWebサイトでは、package.json
にnpmパッケージの依存関係を、 .tool-versions
にBunとNode.jsのバージョンを記載していますが、Dependabotは .tool-versions
に対応していません[5]。
開発環境
開発環境の構築には、Dev Containersを利用しました。これにより、GitHub Codespacesを利用すればブラウザのみで開発環境を構築できるようになります。
Webサイト開発を通じたOSSへの貢献
このWebサイトの開発で利用したいくつかのOSSに対して、バグの報告やPRを送ることができました。
今後の展望
本WebサイトはGitHubリポジトリのIssue駆動で開発を進めており、記事執筆時点で多くのIssueが立っています。今後はこれらIssueを1つずつ解決していく予定です。
おわりに
具体的な実装にはあまり触れられませんでしたが、以上が弊サークルのWebサイトで採用した技術スタックです。
一からWebサイトを制作するのは大変ですが、地味で面倒な部分の多くをAstroがカバーしてくれるので、比較的楽に実装できました。自作Webサイトは、独自のMDX記法を実装できるなど、自由度が高いので面白いです。是非皆さんもAstro×SolidJSでWebサイトを実装してみてください。
-
highlight.jsのこちらのIssueでTextMateのサポートについてが議論されています。 ↩︎
-
MathJaxもMathJax 3以降は十分高速であるため、数式を沢山書く場合はMathJaxも良いと思われます。ただし、MathJax 2は非常に遅いため注意が必要です。 ↩︎
-
@textlint/textlint-plugin-markdownのREADMEにある説明はMDXをMarkdownとして扱うときの設定例であり、MDXそのものをサポートしているわけではありません。 ↩︎
-
https://github.com/dependabot/dependabot-core/issues/1033 ↩︎
Discussion