eyecatch

HonoXとmicroCMSでブログサイトを作った

作成: 3/18/2025, 1:31:20 PM, 最終更新: 3/18/2025, 1:31:20 PM

概要

nopです。この記事ではHono(HonoX)とmicroCMSでブログサイトを作ったものはいいものの、肝心の記事の内容に迷った筆者がブログサイト自体のことをだらだら書いていきます。正直あてにならないことばっかりだと思うので真面目な解説は他の記事を参照していただければと思います。

また、記事の内容がある程度Webアプリケーションの開発に慣れている人に向けて書かれてるため、初心者の方には難しくなっていると思われます。

開発環境

執筆時 2025/1/26

  • Hono: ^4.6.18
  • HonoX: ^0.1.33
  • microCMS: ^3.1.2
  • Tailwind CSS: ^4.0.0

Honoとは

Hono(公式サイト)とは`Fast, lightweight, built on Web Standards. Support for any JavaScript runtime.`なものだそうで、体感上Expressに近いものになっています.Expressを触ったことのない方に説明するなら、「API・Webなどのサーバを、データを受け取り、処理して、返すまでを流れに沿ってコーディングできるフレームワーク」になると個人的に思います。

microCMSとは

microCMS(公式サイト)とは`APIベースの日本製のヘッドレスCMS`という説明の通り、、説明の通りです。CMS(Contents Management System)とはWebサイトのコンテンツを管理・更新できるシステムのことです。WordPressのようなリッチな管理画面で、ブログの内容を執筆・管理することができ、そのコンテンツを自身のWebアプリケーション上にAPI経由で引っ張ってくることができます。microCMSは日本製で有名(所感)なCMSサービスです。


開発の流れ

HonoXプロジェクトの作成

Node.jsの環境が整ってる前提とします。

npm create hono@latest

ちなみに私はnpmではなくpnpm派です。どうでもいいですね。

Tailwind CSSの設定

Tailwind CSS公式のドキュメントを参考にセットアップしてください。

Viteの設定をいじります。

...
export default defineConfig(({ mode }) => {
  if (mode === "client") {
    return {
      build: {
        rollupOptions: {
          input: ["/app/global.css"],
        },
      },
      plugins: [client()],
    };
  } else {
    return {
      plugins: [honox({ devServer: { adapter } }), build()],
    };
  }
});

最後によくわかっていないおまじないをします。すでに完成したものから引っ張っているのでデフォルトとは違うかもしれませんが、追加したのはLinkの個所だけです。

import Link from "honox/server";

...
export default jsxRenderer(({ children, title }) => {
  return (
    <html lang="ja">
      <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta name="description" content="nopのブログサイト" />
        <title>{title}</title>
        <link rel="icon" href="/favicon.ico" />
        <Script src="/app/client.ts" async />
        <Link href="/app/global.css" rel="stylesheet" />
        <Style />
      </head>
      <body>{children}</body>
    </html>
  );
});

以上で設定は完了です。お疲れさまでした。

microCMSのSDKインストール

microCMS公式が配布しているSDKをインストールします。

npm install microcms-js-sdk

書いたやつと解説

app/global.d.ts

import {} from "hono";

type Head = {
  title?: string;
};

type Bindings = {
  MICROCMS_SERVICE_DOMAIN: string;
  MICROCMS_API_KEY: string;
};

declare module "hono" {
  interface Env {
    Variables: {};
    Bindings: Bindings;
  }
  interface ContextRenderer {
    (content: string | Promise<string>, head?: Head):
      | Response
      | Promise<Response>;
  }
}

環境変数の型定義を追加しました。

app/routes/index.tsx

import { env } from "hono/adapter";
import { createRoute } from "honox/factory";
import { createClient } from "microcms-js-sdk";
import Header from "../islands/Header";
import HoverPushCard from "../islands/HoverPushCard";
import { cache } from "hono/cache";
import PushCard from "../islands/PushCard";

export default createRoute(async (c) => {
  const { MICROCMS_SERVICE_DOMAIN, MICROCMS_API_KEY } = env<{
    MICROCMS_SERVICE_DOMAIN: string;
    MICROCMS_API_KEY: string;
  }>(c);

  const client = createClient({
    serviceDomain: MICROCMS_SERVICE_DOMAIN,
    apiKey: MICROCMS_API_KEY,
  });

  const contents = await client.get({ endpoint: "blogs" });
  const posts = contents.contents.map((content: any) => {
    return {
      id: content.id,
      title: content.title,
      eyecatch: content.eyecatch.url,
    };
  });

  return c.render(
    <div
      class={
        "w-screen min-h-screen px-3 lg:px-20 py-6 lg:py-8 bg-neutral-50 dark:bg-neutral-800 flex flex-col items-center gap-16"
      }
    >
      <div class={"w-full"}>
        <Header />
      </div>
      <div class={"flex flex-col gap-6"}>
        <div class={"w-fit"}>
          <PushCard>
            <div class={"py-2 px-4"}>
              <h1
                class={
                  "text-2xl font-semibold text-neutral-700 dark:text-neutral-100"
                }
              >
                Posts - {posts.length}
              </h1>
            </div>
          </PushCard>
        </div>
        {posts.map((post: any) => (
          <a href={`/posts/${post.id}`} class={"max-w-(--breakpoint-sm)"}>
            <HoverPushCard>
              <div class={"p-4"}>
                <img
                  src={post.eyecatch}
                  alt="eyecatch"
                  class={"h-auto max-w-full rounded-xl"}
                />
                <p
                  class={
                    "p-4 font-semibold text-xl text-neutral-700 dark:text-neutral-100"
                  }
                >
                  {post.title}
                </p>
              </div>
            </HoverPushCard>
          </a>
        ))}
      </div>
    </div>,
    { title: "nop's blog" }
  );
}, cache({ cacheName: "posts", cacheControl: "max-age=3600" }));

env関数を使って環境変数を引っ張り、microCMSのAPIを叩いてブログの内容を取得しました。

app/routes/index.tsxふわっと解説

const { MICROCMS_SERVICE_DOMAIN, MICROCMS_API_KEY } = env<{
    MICROCMS_SERVICE_DOMAIN: string;
    MICROCMS_API_KEY: string;
  }>(c);

env関数で環境変数を適切にとってきます。開発中は`.dev.vars`ファイルから読み取ります。

このブログサイトの場合だと、Cloudflare PagesにデプロイするためCloudflareのコントロールパネルから環境変数を設定しました。

const client = createClient({
  serviceDomain: MICROCMS_SERVICE_DOMAIN,
  apiKey: MICROCMS_API_KEY,
});

const contents = await client.get({ endpoint: "blogs" });
const posts = contents.contents.map((content: any) => {
  return {
    id: content.id,
    title: content.title,
    eyecatch: content.eyecatch.url,
  };
});

この部分はmicroCMSのSDKを用いて記事のデータをAPIから取得しています。先ほどの環境変数を引数に入れることでClientを生やし、その後APIにGETを叩き、記事データを取得します。今回の場合は`endpoint: "blogs"`となっていますが、どうやらこのエンドポイントはmicroCMSのコントロールパネルで変更できるようです。

return c.render(
...
);

`c.render`から先は完全にJSX、Tailwind CSSゾーンです。ブログを作成した当初はClineなどは認知していなかったため、すべて手打ちしました。

app/posts/[id].tsx

import { env } from "hono/adapter";
import { createRoute } from "honox/factory";
import { createClient } from "microcms-js-sdk";
import Header from "../../islands/Header";
import { html, raw } from "hono/html";
import PushCard from "../../islands/PushCard";

export default createRoute(async (c) => {
  const { id } = c.req.param();

  const { MICROCMS_SERVICE_DOMAIN, MICROCMS_API_KEY } = env<{
    MICROCMS_SERVICE_DOMAIN: string;
    MICROCMS_API_KEY: string;
  }>(c);

  const client = createClient({
    serviceDomain: MICROCMS_SERVICE_DOMAIN,
    apiKey: MICROCMS_API_KEY,
  });

  let post = null;
  try {
    post = await client.get({ endpoint: "blogs", contentId: id });
  } catch (e) {
    return c.redirect("/404");
  }

  if (!post) {
    return c.redirect("/404");
  }

  const publishedAt = new Date(post.publishedAt);
  const updatedAt = new Date(post.updatedAt);

  return c.render(
    <div
      class={
        "w-screen min-h-screen px-3 lg:px-20 py-6 lg:py-8 bg-neutral-50 dark:bg-neutral-800 flex flex-col items-center gap-16"
      }
    >
      <div class={"w-full"}>
        <Header />
      </div>
      <div class={"h-auto max-w-5xl flex flex-col items-center gap-6"}>
        <div class={"w-full"}>
          <PushCard>
            <div class={"grow p-4 flex flex-col items-center gap-6"}>
              <img
                src={post.eyecatch.url}
                alt="eyecatch"
                class={"h-auto w-full rounded-md"}
              />
              <h1
                class={
                  "text-2xl font-semibold text-neutral-700 dark:text-neutral-100 text-center"
                }
              >
                {post.title}
              </h1>
              <p class={"text-neutral-700 dark:text-neutral-100"}>
                作成: {publishedAt.toLocaleString()}, 最終更新:{" "}
                {updatedAt.toLocaleString()}
              </p>
            </div>
          </PushCard>
        </div>
        <div class={"w-full"}>
          <PushCard>
            <div
              class={"post px-6 lg:px-20 py-10 flex flex-col gap-4"}
            >{html`${raw(post.content)}`}</div>
          </PushCard>
        </div>
      </div>
    </div>,
    { title: post.title }
  );
});

`[id].tsx`にすることで、example.com/1234にアクセスされた際に、`const { id } = c.req.param();`のidに1234が入ります。

それ以外は`index.tsx`と大して変わらないのでスキップします。

最後に

ブログ書くのって大変ですね。思っていたよりもだいぶ疲れました。また機会があればよろしくお願いします。