🐕

Astro + zenn-markdown-htmlで始める個人ブログ

2023/03/24に公開

不意に思い立ってAstroで個人ブログを作成しました。Astroのドキュメントは充実していますし、同じようなAstroでブログサイトを作成するような記事も既に多くあったのですが意外とはまりポイントが多かったのでどなたかの役にたてばと思い記事にまとめさせていただきました。

使用技術

FW

最初はsvelte使ってみたかったのでsvelteにしようと思ったんですが、AstroならReactでもVueでもsvelteでも何でも使えると聞いたのでAstroを採用しました。

CSS

少し前までCSS-in-JSの話題が盛り上がっていたのは観測していたのだけど、その後どうなったのかまで追いきれておらず、なるべく簡単にデザイン調整したいのと採用実績があったので今回はtailwindを採用しました。

markdown

ブログの唯一の要件としてzennのようにローカルのエディタでmarkdownで記事を執筆するというのがあったので最初はmicroCMSにマークダウンの記事を入稿して...みたいなことを考えていたのですがmarkdownをそのままmicroCMSに入稿することはできず、やるならばアプリ側でmarkdown形式の本文からhtmlの生成をする必要があり少し面倒でした。

Astroはプロジェクト内のmarkdownを簡単に読みこむことができるため、zennがOSSとして公開してくれているzenn-markdown-htmlと組み合わせることで簡単にmarkdownで執筆した記事をhtmlに変換することができたため採用しました。

deploy

デプロイ先はCloudflareを採用しました。ここは特に理由はないのですがCloudflareのサービスを今後できる限り採用していきたいと考えているので脳死で採用しました。記事の執筆が終わったらリポジトリをpushするだけで公開できるので記事の公開は非常にシンプルで簡単です。

プロジェクトのセットアップ

VSCodeで開発する場合、拡張機能が用意されているのでVSCodeを使用する方は真っ先にインストールすることをおすすめします。

Astroプロジェクトの作成

以下、コマンドでプロジェクトを作成します。TypeScriptを使用するかや依存関係をインストールするかなど聞かれるのでよしなに回答する。

npm create astro@latest <project名>

作成が完了すると以下のようなディレクトリ構造になっているはず。

tree . -L 1
.
├── README.md
├── astro.config.mjs
├── node_modules
├── package-lock.json
├── package.json
├── public
├── src
└── tsconfig.json

tailwind

以下コマンドを実行し、astroのコマンド経由でtailwindをインストールします。

npx astro add tailwind

いくつか対話形式で聞かれますが全部Yesで大丈夫です。インストールが完了するとtailwind.config.cjs が作成されているのと、astro.config.mjsに以下のようにintegrationsが追加されていると思います。

astro.config.mjs
import { defineConfig } from 'astro/config';

import tailwind from "@astrojs/tailwind";

// https://astro.build/config
export default defineConfig({
  integrations: [tailwind()]
}); 

prettier-plugin-tailwindcssが対応しているようなのだけど、有効にする方法がわからなかったので断念。わかる方いたらコメントください🙇

prettierはastroファイル内では何も設定しなくても有効なので手動では今回入れてません。

ブログ記事のコレクション定義

Astroでmarkdownを扱うのにContent Collectionsという機能が用意されており、src/content配下に任意のディレクトリを作成し、markdownファイルを配置することでmarkdownファイルを一つのコレクションとして扱うことができます。

例えば、src/content/blog配下にhello.mdを作成した場合、blogコレクションとしてhello.mdを扱うことができるようになります。そして、markdownファイルを型安全に使用するためにsrc/content配下にconfig.tsを作成しコレクション定義をすることができます。

config.ts
import { z, defineCollection } from 'astro:content';

// zodで各項目のバリデーションを設定
const blogCollection = defineCollection({
  schema: z.object({
    title: z.string().max(100).min(1), // titleは1文字以上100文字以下
    tags: z.array(z.string()).max(5).min(1), // タグは1つ以上5個まで
    date: z.string().regex(/^\d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[12]\d|3[01])$/), // yyyy-mm-dd形式
  }),
});

// `blog` という名前で上記のコレクション定義を登録
export const collections = {
  'blog': blogCollection,
};

https://zenn.dev/taichi221228/articles/9d78500757d49f#:~:text=4 のやつ 🥺-,バグ,-?

ブログ一覧の取得

以下のようにastro:contentgetCollection()を使用して記事一覧を取得する関数を作成する。

blog.ts
import { getCollection } from "astro:content";

export interface Blog {
  title: string;
  tags: string[];
  date: string;
  slug: string;
  body: string;
}

export async function getAllBlogs(): Promise<Blog[]> {
  const blogs = await getCollection("blog"); // 設定ファイルで指定したキーを設定
  return blogs
    .map((blog) => ({ ...blog.data, slug: blog.slug, body: blog.body }))
    .sort(
      (a, z) =>
        new Date(z.date).getMilliseconds() - new Date(a.date).getMilliseconds()
    );
}

コレクション定義でblogという名前でコレクションを定義したのでgetCollection()の引数に文字列でblogを渡すことでblogコレクションの記事一覧を取得することができる。取得したblog情報は

{
    id: "20230321.md"; // ファイル名
    slug: "20230321"; // 拡張子を除いた部分がslugとなる
    body: string; // 本文
    collection: "blog"; // 定義したコレクション名
    data: {
        date: string;
        title: string;
        tags: string[];
    };
}

のような構造になっているのでいい感じに加工することができる。

zennのように公開フィールドをboolで定義しておけば、ここでfilterなどすることで公開にしている記事のみを一覧として取得することが可能です。

ブログ詳細ページ(動的ページ)の作成

Astroの動的ページはsrc/pages配下に[slug].astroのように[]でページ生成に必要なパラメーター名を定義してファイル名に含めます。

getStaticPaths

動的ページファイル内にはgetStaticPaths()を定義します。

import { getAllBlogs } from "../lib/blog";

export interface Props {
	title: string;
  tags: string[];
  date: string;
  body: string;
}

export async function getStaticPaths() {
  const blogs = await getAllBlogs()
  return blogs.map(blog => {
    return {
      params: {
        slug: blog.slug,
      },
      props: {
        title: blog.title,
        tags: blog.tags,
        date: blog.date,
        body: blog.body
      }
    }
  })
}

戻り値にはファイル名で定義したparamsとpropsの二つを含めたオブジェクトを返すようにします。

markdownをhtmlに変換する

とりあえず、以下のモジュールを追加します。

npm i zenn-markdown-html
npm i zenn-content-css

変換にはzenn-markdown-htmlを使用しましたが、build時に関数が見つからないエラーでハマったのですが以下の記事で対応策を書いていただいてました。感謝

https://zenn.dev/rorisutarou/articles/ec3871ec55693d

import lib from 'zenn-markdown-html';

// build時にそのまま使うとエラーになるため修正
type MarkdownHtml = (text: string, options?: MarkdownOptions) => string
type MarkdownHtmlAtBuild = { default: MarkdownHtml }

let markdownHtml: MarkdownHtml = lib

if(typeof lib !== 'function') {
  markdownHtml = (lib as MarkdownHtmlAtBuild).default
}

const { title, tags, date, body } = Astro.props
const html = markdownHtml(body)

<Blog html={html} />

変換したhtmlはBlogコンポーネントにpropsとして渡してレンダリングする。

Blog.tsx
import 'zenn-content-css';

type Props = {
  html: string
}
export const Blog: React.FC<Props> = ({ html }) => {
  return (
    <div
      // "znc"というクラス名を指定する
      className="znc "
      // htmlを渡す
      dangerouslySetInnerHTML={{
        __html: html,
      }}
    />
  )
}

astroのままでもできると思いますが、tsxの例をそのまま使用したのでReactを追加してます。

npx astro add react

埋め込みコンテンツ対応

TwitterやリンクURLの埋め込み表示をしたい場合は以下のように第二引数にオプションを指定することで可能。ただし、非商用のみ使用可能なので注意してください。

const content = markdownHtml(post.body, {
  embedOrigin: "https://embed.zenn.studio",
});

参考

埋め込み表示には以下のscriptタグの記載も必要。

<head>
  <!-- Astroのscript最適化処理をキャンセルするためにis:inlineを追加 -->
  <script is:inline src="https://embed.zenn.studio/js/listen-embed-event.js"
  ></script>
</head>

注意したいのはis:inlineの記載がないとCORSエラーが発生してしまうのでis:inlineの記載をする必要がある。

参考

markdownの文字化け対応

初歩的かもしれないのですがmarkdownの中身が文字化けしてかなりはまったので書いておきます。もしmarkdownが文字化けする場合はheadタグに文字コードを指定することで解決するはず。

<head>
  <meta charset="UTF-8" />
</head>

参考

Cloudflare Pagesにデプロイする

GitHubのリポジトリと連携することでpushするとサイトが公開できるようにできます。基本的に詰まるところは特にないのですがnodeのバージョンだけ執筆時点で12系でビルドがこけてしまったので今回は環境変数にnodeのバージョンを指定しています。

NODE_VERSION 17.9.1

(前述してますがこのときzenn-markdown-htmlの件でビルドがこけて初めて認識しました。。)

問題なければこれでブログサイトが公開できているはずです!
お疲れ様でした🎉

(おまけ)Google Analyticsを導入する

最後にサイトのアクセス数やどの記事がそのくらいアクセスあったかなどの分析をするためにGoogle Analyticsを導入することができます。

以下の記事がかなり丁寧に書いてくれているので興味がある方は見てみてください。

https://www.kevinzunigacuellar.com/blog/google-analytics-in-astro/

まとめ

以上、Astroを使用した個人ブログサイトの作成について以下の内容についてまとめました。

  • Astroを使用した静的サイトの作成の仕方について
  • Astroにtailwindの導入の仕方について
  • Astroにzenn-markdown-htmlを組み込んでマークダウン記事の動的ページの作成方法について

今回、とりあえずmarkdownで執筆した記事を公開できるようにしただけなのでSEO対策やOGG画像の生成などやってみたいこともまだいろいろあるのでちょこちょこ改修していこうかと思います。

感想としてはAstroはReactやVueといった異なるフレームワークを同居させることができるので手元で試したいフレームワークの利用とかが手軽にできていいなと思いました。個人ブログのような自分しか管理しない手元の環境にはぴったりだなと思います。

svelteを触りたいと思っているので次コンポーネント作成するときにはsvelteを触ってみようかなと思います。

今回は以上です!ありがとうございました!

今回の成果物

リポジトリ
https://github.com/JY8752/my-blog

作成したブログ
https://jy-panda.com/

GitHubで編集を提案

Discussion