Hello Astro
Intro
エンジニアは2~3年ごとにブログを作りなおす。私もその習性に従って新しいブログを作った。
前回のブログで新しいブログ作ったよ記事が2020年の5月だったので、まぁそれくらいの周期でやってくる。
技術まわりの備忘録を書いておいて、また3年経ったらこの記事を参照することになるだろう。
なお、筆者はフロント周りの技術に疎いので、雰囲気で書いていることを添えておく。
Astro
ベースの技術はAstroだ。 Hugoと同じように静的サイトを生成するライブラリで、基本的にビルド成果物をぽん置きするだけで動く。便利。
.astro
ファイルというHTMLとJSXを悪魔合体させたようなもの (そもそもJSXがJSとHTMLを悪魔合体させたようなものだが) が提供されており、
これを使ってコンポーネントベースでページを作る。
一方でReactやVueのようなUIフレームワークを使うこともできる。その場合でも astro build
するとjsファイルなしで静的サイトが生成される。
Getting Started
公式のGetting startedが丁寧に書いてあるのでそれに従う。厨二なのでyarnを使った。(最近はnpmへの揺り戻しが起こっているらしい)
yarn create astro
して、適当にプロンプトに答えるとastroプロジェクトが生成される。
yarn dev
でローカルサーバーが立つ。すばらしい。
(絵文字が豆腐になっているが、これはOSに絵文字に対応したフォントをインストールしてないからである。)
Nextのようにファイルベースルーティングを採用しているので、 content/blog
配下にmdファイルを置くとそのままブログ記事になる。
当然のこと (便利な世の中になった) ながら、ライブリロードが入っているので、mdファイルを編集すると即座にブラウザに反映される。
.astro入門
現在時点の公式ブログテンプレートにはなぜか draft
の対応が入っていない。ので、この対応をすることで .astro
の基本的な使い方を紹介する。
まずは src/content/config.ts
でfrontmatterに draft
があるということを定義する。デフォルトは false
(公開) にしておく。
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({ // Type-check frontmatter using a schema schema: z.object({ title: z.string(), // Transform string to Date object pubDate: z .string() .or(z.date()) .transform((val) => new Date(val)), updatedDate: z .string() .optional() .transform((str) => (str ? new Date(str) : undefined)), heroImage: z.string().optional(), draft: z.boolean().default(false), }),});
そして src/pages/blog/index.astro
で draft
が false
な記事のみをリストするように修正する。
---import BaseHead from '../../components/BaseHead.astro';import Header from '../../components/Header.astro';import Footer from '../../components/Footer.astro';import { SITE_TITLE } from '../../consts';import { getCollection } from 'astro:content';import FormattedDate from '../../components/FormattedDate.astro';
const posts = ( (await getCollection('blog')) (await getCollection('blog', ({ data }) => !data.draft)) .sort((a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf()))---
これで完了だ。便利。
[追記] src/pages/blog/[...slug].astro
にもdraft対応が必要。
export async function getStaticPaths() { return ( (await getCollection('blog')) (await getCollection('blog', ({ data }) => !data.draft)) .map((post) => ({ params: { slug: post.slug }, props: post, })) );}
Reactとの連携
そもそも .astro
ファイルでコンポーネントを使いつつ、propを渡して、、ということができるので、静的に決まるならReactは不要。とはいえミーハーなので動作確認くらいはしておく。
ドキュメントのUIフレームワークのページや@astrojs/reactを参照しつつ、Reactのコンポーネントを使ってみる。
まず astro add
する。
yarn astro add react
題材はなんでも良いが、動的な要素がないコンポーネントを実装したい。 今回はCalloutBlockを実装してみる。
src/components/SimpleCalloutBlock.tsx
を以下の内容で作る。色はAdobe Spectrumからもらった。
type SimpleCalloutBlockProps = { title: string; children: React.ReactNode;};
export function SimpleCalloutBlock(props: SimpleCalloutBlockProps) { const divheader = ( <div style={{ backgroundColor: "rgb(255, 221, 214)", // Red 200 padding: "0.25rem", fontWeight: "bold", }}>{props.title}</div> ) const divcontent = ( <div style={{ padding: "0.25rem", }}>{props.children}</div> )
return <> <div style={{ borderRadius: "0.25rem", border: "1px solid", borderColor: "rgb(213, 213, 213)", // Gray 300 borderLeft: "5px solid", borderLeftColor: "rgb(247, 92, 70)", // Red 800 }}> {divheader} {divcontent} </div> </>}
.astro
ファイルでは以下のように利用できる。
import SimpleCalloutBlock from '../../components/SimpleCalloutBlock.tsx';---<SimpleCalloutBlock title="This is Important">Danger, callouts will really improve your writing.</SimpleCalloutBlock>
さらにAstroはmdxをサポートしているので、なんとmdの中に直接tsxを書くこともできる
import SimpleCalloutBlock from '../../components/SimpleCalloutBlock.tsx';
<SimpleCalloutBlock title="This is Important">Danger, callouts will really improve your writing.</SimpleCalloutBlock>
レンダリング結果はこんな感じ。
SimpleCalloutBlock
はReactを使って作ったが、ビルドするとjsなしの静的なHTMLとCSSになる。便利。
Astro Islands
AstroにはAstroアイランドという概念がある。 これによりページの一部のコンポーネントのみインタラクティブ (jsあり) の領域を作ることできる。
アイランドはそれぞれ独立しており、それぞれは異なるUIフレームワークを使うことができる。 (Spotifyのマイクロフロントエンドみたいなのを想像している)
これも動作確認程度に動かしておく。
Reactで簡単なカウンターを src/components/SimpleButton.tsx
に実装する。
import React from "react"
export function SimpleButton() { const [count, setCount] = React.useState(0) return <button onClick={()=>setCount(count+1)}>Click me: {count}</button>}
そして利用側でこのように記述する。
import { SimpleButton } from '../../../components/SimpleButton';
<SimpleButton client:load />
client:load
を付けることで、このコンポーネントに限り、jsをクライアントで実行するように設定できる。
もし付けなければこれは静的なコンポーネントになるので、カウンターは動かない。
jsが読み込まれるタイミングを client:*
で制御できる。詳細はドキュメントを参照。
Cloudflare Pages
ホスティングはCloudflare Pagesを使う。 以前使っていたNetlifyでも良かったが、界隈でCloudflare Pagesが注目されていたのでそれにした。
使い勝手はNetlifyとほぼ同じ。GitHubと連携して、pushすると自動でビルド/デプロイされる。便利。
少し悩んだのが、カスタムドメインの設定。
conao3.comをNetlifyのDNSに置いて、そのままNetlifyを向くように設定していたので新しいastroのブログをどのURLで公開するか悩んだ。
ひとまずNetlifyに置いてあるドメインはそのままにしておいて、CNAME a.conao3.comでastroのブログを見るように設定した。 (a
は astro
の a
)
将来的にはNetlifyに置いてあるブログを h.conao3.com くらいで見れるようにしたらいいのかなと思う。 (h
は hugo
の h
)
ついでにドメインをNetlifyからCloudflareに引っ越しすると良いかもしれない。リダイレクトの設定が面倒なので、また時間が取れるときにやる。
まとめ
感想としてはAstroが便利すぎる。全部入りで速い。コンテンツ中心のウェブページを作るならこれが最初の選択肢になりそう。
ブログのソースはconao3/blog-astro-srcに置くことにした。 各ページから直接ソースのmdに飛べるようにしたら便利かもしれない。
ひとまずスモールスタートとして直接マークダウンを書いたが、Emacsユーザーとしてはox-hugoと同じように一つのorgファイルから複数のページを生成できるようにしたい。