QLITRE DIALY

HonoとmicroCMSでブログを作ってみた感想

2023年10月22日

今回はタイトルの通りHono🔥とmicroCMSでブログを作ってみて、その感想を書いていく、というような内容です。

Honoとは

yusukebeさんが開発しているWEBフレームワークです。

Hono - [炎] means flame🔥 in Japanese - is a small, simple, and ultrafast web framework for the Edges. It works on any JavaScript runtime: Cloudflare Workers, Fastly Compute@Edge, Deno, Bun, Vercel, Netlify, Lagon, AWS Lambda, Lambda@Edge, and Node.js.

Fast, but not only fast.

https://hono.dev/

Honoと私の出会い

引用したのはいいのですが、何ができるのか、ということはよくわかっていませんでした。

かなり話がそれるのですが、元々の始まりは、yusukebeさんが開発しているramen-apiというプロジェクトにラーメン画像をプッシュしたことでした。このあたりの詳細は以前にブログに書きました。

https://www.qlitre-dialy.ink/post/visit-to-jiro-meguro-from-api-inspire

面白いことやってる人が世の中にいるなーと思いながらTwitterをフォローしてみると、Honoというイケてるフレームワークを開発しているらしいことが分かる。なんか凄そう。だけど、具体的に何ができるか分からないままでいました。

そんななか、9月にServerless Days Tokyoというイベントで、Honoのワークショップが開催される、という告知がありました。

https://tokyo.serverlessdays.io/

これは良いチャンスだと思い、参加をしてきました。

結果的にかなり面白いワークショップで、短い時間でしたが、Honoというフレームワークの理解が深まりました。

特にテキストやjsonを返すだけでなく、JSX形式でエッジで動かす、という体験ができたのが、大きな収穫でした。

ワークショップの詳細についてはyusukebeさんのブログに詳しいです。

https://yusukebe.com/posts/2023/blogging-chatgpt-plugin/

ブログを作ってみた

せっかく、ワークショップに参加したので、何かやってみたい。

JSX形式で動かせるということは、そのままブログを作れるかな?って思ってやってみました。

microCMSを普段から触っているので、記事はmicroCMSに投稿、Hono JSXで表示という構成でやってみる。

それで出来上がったのがこちらのブログです。

https://hono-microcms-blog.qlitre.workers.dev/

一覧ページ:

記事詳細ページ:

一応シンタックスハイライトやmicroCMSでのリッチエディタ入稿に対応しています。

スタイルはとりあえずこの日記サイトを模写するような形でやりました。

その他にも、ページネーションやカテゴリ、タグ別のフィルタなども備えていて、思っていたよりも本格的な物ができてしまった、という感想です。Honoで、大体何でもできるんじゃないか、という気がします。

まだ実装していないのですが、draft表示やkeyword検索なんかの、microCMSを扱う上でちょっと躓きがちな実装も比較的シンプルにできるだろうな、という感触もあります。

基本的な構成について

Next.jsっぽい感じで、コンポーネントに分けて開発をしていきました。

例えば一覧ページでは、まず記事一覧データを取得して、<HomeContent />にデータを渡します。

app.get('/', async (c) => {
    const client = createClient({
        serviceDomain: c.env.SERVICE_DOMAIN,
        apiKey: c.env.API_KEY,
    })
    const listData = await client.getList<Post>({ endpoint: 'post' })
    const posts = listData.contents
    const props = {
        posts: posts,
        paginationMaterial: {
            totalCount: listData.totalCount,
            currentPage: 1,
        },
        siteData: {
            title: config.siteTitle,
            description: config.siteDescription,
            ogpType: "website" as const,
            ogpUrl: config.siteURL,
        },

    }
    return c.html(<HomeContent {...props} />)
})

そうして、<HomeContent />の中でベースとなるレイアウトを呼び出しつつ、子コンポーネントを組み込んでいく、と言う風にしました。

export const Layout = (props: SiteData) => html`<!DOCTYPE html>
<html>
  <head>
  <meta charset="utf-8" />
  <title>${props.title}</title>
  <link rel="stylesheet" href="/static/css/style.css" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="description" content="${props.description}" />
  <meta name="author" content="${config.author}" />
  <!-- OGP -->
  <meta property="og:title" content="${props.title}">
  <meta property="og:description" content="${props.description}">
  <meta property="og:url" content="${props.ogpUrl}">
  <meta property="og:site_name" content="${config.siteTitle}">
  <meta property="og:image" content="${props.ogpImage}">
  <meta property="og:image:width" content="1200">
  <meta property="og:image:height" content="630">
  <meta property="og:type" content="${props.ogpType}">
  <meta property="article:author" content="${config.twitterURL}">
  <meta name="twitter:card" content="summary_large_image">
  <meta name="twitter:creator" content="${config.twitterID}">
  </head>
  <body>
    ${<Header></Header>}
    ${props.children}
    ${<Footer></Footer>}    
  </body>
</html>`

export const HomeContent = (props: {
  siteData: SiteData,
  posts: Post[],
  paginationMaterial: PaginationMaterial,
  category?: Category,
  tag?: Tag
}) => (
  <Layout {...props.siteData}>
    <div class="container">
      <Breadcrumbs category={props.category} tag={props.tag}></Breadcrumbs>
      <ArticleList posts={props.posts}></ArticleList>
      <Pagination totalCount={props.paginationMaterial.totalCount}
        currentPage={props.paginationMaterial.currentPage}
        categoryId={props.paginationMaterial.categoryId}
        tagId={props.paginationMaterial.tagId}>
      </Pagination>
    </div>
  </Layout>
)

このあたりの構成についてはyusukebeさんのZennの記事を参考にしました。

https://zenn.dev/yusukebe/articles/c9bc1aa389cbd7

スタイルについて

スタイルをどうしようか、という点については悩みました。

Honoの場合style.cssに記述していく形になるのですが、ちょっと管理が大変そうだなーという予感がありました。

Next.jsやNuxt.jsの素晴らしい点の一つはコンポーネントごとにスタイルも分けて管理できるシステムがあることです。

Honoでも同じことができるのかもしれないですが、情報がつかみきれてなかったので、何かしら工夫をしなければいけません。

これがベストプラクティスかどうかは確証がつかめませんが、疑似的に汚染されない形でスタイルを分ける方法をとりました。

コンポーネントファイル名は一意になります。そのため、コンポーネント内のクラス名を{ファイル名}__{クラス名}という風に統一して記述するようにすればスタイル名の衝突は起こりません。

例えば記事一覧の中の記事の部品であるArticle.tsxの場合。

// Article.tsx
import type { Post } from '../types/blog';
import { TagInline } from './TagInline';
import { jstDatetime } from '../utils/jstDatetime';

type Props = {
    post: Post
}

export const Article = ({ post }: Props) => {
    return (
        <div>
            <a class="Article__titleLink" href={`/post/${post.id}`}>
                <h1 class="Article__title">{post.title}</h1>
            </a>
            <p class="Article__publishedAt">{jstDatetime(post.publishedAt, "YYYY年MM月DD日")}</p>
            <p class="Article__description">{post.description}</p>
            <TagInline category={post.category} tags={post.tag} />
            <a href={`/post/${post.id}`} class="Article__linkButton">続きを読む</a>
        </div>
    );
};

そうしてコンポーネント名と同じ名前のcssファイルを作ります。

/* assets/static/css/components/Article.css */
.Article__publishedAt {
    margin-top: var(--spacing-2);
    color: var(--c-text-secondary);
    font-size: var(--font-size-sm);
}

.Article__description {
    line-height: 1.6;
    margin-top: var(--spacing-2);
    color: var(--c-text-secondary);
    font-size: var(--font-size-md);
}

.Article__titleLink {
    &:hover {
        text-decoration: underline;
    }
}

.Article__title {
    line-height: 1.6;
    font-family: var(--font-family-heading);
    font-weight: bold;
    font-size: var(--font-size-xl);
}


.Article__linkButton {
    margin-top: var(--spacing-8);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--font-size-sm);
    padding: var(--spacing-1) var(--spacing-2);
    transition: background-color 0.2s;
    color: var(--c-teal-500);
    background-color: var(--c-white);
    border: var(--border-1);
    border-radius: var(--radius-sm);

    &:hover {
        background-color: var(--c-teal-100);
    }
}

あとはstyle.cssでこのcssファイルをimportすればコンポーネントごとにスタイルを分離して開発を進めることができます。

感想とか

作ってみた感想についてまとめていきます。

ultrafastは伊達じゃない

なんていうか色々速いです。嘘だろ…って感じです。

  • yarn deployすると大体5秒くらいで本番環境に反映される
  • deployが速すぎるので、devコマンドいらないんじゃね…っていう意見もみたことがある
  • deploy速度だけでなく肝心のサイトの表示速度も爆速

かなり楽しい

これは大事なことで、触っていてかなり楽しいです。

Next.jsやNuxt.jsでもブログを作った経験があるのですが、自由度が高い、という感じがしました。

一貫して標準的なWEBの機能を提供しているという記述も見たことがあり、その枠内で好きなことをやればよし、という雰囲気があります。

上で書いたスタイルの件も含めて自分の頭でいろいろ考える必要があるのですが、それがとても「作ってる」という感覚がして楽しい。

ルーティングが楽

あと個人的にはルーティングの記述が非常にシンプルで管理がしやすいと思いました。

例えば記事詳細ページに行くにはindex.tsx内でこうするだけでいいです。

app.get('/post/:slug', async (c) => {
    const slug = c.req.param('slug')
    const client = createClient({
        serviceDomain: c.env.SERVICE_DOMAIN,
        apiKey: c.env.API_KEY,
    })
    const listDetail = await client.getListDetail<Post>({ endpoint: 'post', contentId: slug })
    const props = {
        post: listDetail,
        siteData: {
            title: listDetail.title,
            description: listDetail.description,
            ogpType: "article" as const,
            ogpImage: listDetail.thumbnail?.url,
            ogpUrl: config.siteURL + `post/${slug}`,
        },
    }
    return c.html(<DetailContent {...props} />)
})

ルーティングはフォルダ分けして行うケースが一般的だと思っていたのですが、Honoはindex.tsx内で全て記述できます。

ルートが増えるに伴ってファイルが増えると管理が複雑になる…というのはあります。

Honoは同じファイルにあるので、かなり管理がしやすい。

ソースコード

https://github.com/qlitre/hono-microcms-blog-sample

ブログの始め方

もしブログを試してみたい方がいましたら以下の手順で始められると思います。

githubのREADME.mdに合わせて、microCMSのAPI設定をします。

その後、ソースコードをcloneもしくはダウンロードして展開してください。

プロジェクトのルートディレクトリに移動してnpm installもしくはyarn installします。

example-wrangler.tomlファイルをwrangler.tomlにファイル名を変えます。

[vars]の部分を自身の環境に合わせて編集します。

[vars]
API_KEY = "Your-microCMS-API-KEY"
SERVICE_DOMAIN = "Your-microCMS-service-domain"

ここに記述した内容が環境として働き、ソースコード内で、c.envして値を取り出せます。

app.get('/', async (c) => {
    const client = createClient({
        // 環境変数の取り出し
        serviceDomain: c.env.SERVICE_DOMAIN,   
        apiKey: c.env.API_KEY,
    })
    ...
}

settings/siteSettings.tsもご自身の環境に合わせて編集ください。

// 1ページあたりの記事数
export const BLOG_PER_PAGE = 5

export const config = {
    // Headerなどに表示
    siteTitle: 'Hono microCMS Blog',
    siteURL: 'https://hono-microcms-blog.qlitre.workers.dev/',
    // footer、meta descriptionに表示
    author: 'qlitre',
    // meta descriptionに表示
    siteDescription: 'Hono microCMS Blog Sample',
    // HeaderのGitHUBリンク
    repos: 'https://github.com/qlitre/hono-microcms-blog-sample',
    // Aboutページのリンク
    about: 'https://qlitre.me',
    twitterURL: 'https://twitter.com/kuri_tter',
    twitterID: '@kuri_tter',
}

あとはnpm run dev、もしくはyarn devしてブログをお楽しみください。

おわりに

以上、Honoでブログを作ってみた感想でした。

この日記サイトはNextで動かしてるんですけど、Honoに切り替えるか、ちょっと悩んでいます。ではでは。