Conao3 Note

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への揺り戻しが起こっているらしい)

Terminal window
yarn create astro

して、適当にプロンプトに答えるとastroプロジェクトが生成される。

Terminal window
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.astrodraftfalse な記事のみをリストするように修正する。

---
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 する。

Terminal window
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>

レンダリング結果はこんな感じ。

This is Important
Danger, callouts will really improve your writing.

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のブログを見るように設定した。 (aastroa)

将来的にはNetlifyに置いてあるブログを h.conao3.com くらいで見れるようにしたらいいのかなと思う。 (hhugoh)

ついでにドメインをNetlifyからCloudflareに引っ越しすると良いかもしれない。リダイレクトの設定が面倒なので、また時間が取れるときにやる。

まとめ

感想としてはAstroが便利すぎる。全部入りで速い。コンテンツ中心のウェブページを作るならこれが最初の選択肢になりそう。

ブログのソースはconao3/blog-astro-srcに置くことにした。 各ページから直接ソースのmdに飛べるようにしたら便利かもしれない。

ひとまずスモールスタートとして直接マークダウンを書いたが、Emacsユーザーとしてはox-hugoと同じように一つのorgファイルから複数のページを生成できるようにしたい。