0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ヘッドレスCMSを自作した — 組み込む手間を減らしたい!& 書く画面を見やすくしたい!

0
Posted at

ヘッドレスCMS genko.me を個人で開発しています。

自分のサイトのブログや取り組んだことを更新するために作り始めました。この記事では、genko.me を自分のサイトに組み込むとどれくらいの手間で済むのかと、そのために設計で悩んだことを書きます。

なぜ作ったか

ブログやお知らせをヘッドレスCMSで管理すると、困るのは2か所です。

1つは組み込み。記事一覧、詳細、ページ送り、公開前のプレビュー。どのサイトでも同じものを毎回書くことになります。

もう1つは書く画面。API は開発者が触りますが、原稿を書くのは管理画面です。個人のブログなら、書くのも自分です。長い文章を書いて、表を入れて、コードを貼って、読み返して直す。その時間が気持ちよくないと、書くこと自体が続きません。

genko.me は、この2つを両方とも軽くすることを目標にしています。

組み込みはここまで短い

Next.js(App Router)のサイトに、ブログの一覧と詳細を出すまでの流れです。

管理画面で blog という API を作り、タイトル(title)と本文(body)の2項目を用意します。記事を1件公開して API キーを発行したら、サイトのプロジェクトで CLI を実行します。

npx @genko-me/cli init

ワークスペース ID と API キーを入力し、同期する API に blog を選ぶと、SDK と表示部品が追加され、接続ファイルが生成されます。

ファイル 役割
lib/schema.ts 管理画面のスキーマから生成した型と API 定義
lib/client.ts ワークスペースへ接続するクライアント
genko.config.json 次回の型同期に使う設定

一覧ページはこれだけです。

app/blog/page.tsx
import { client } from "@/lib/client";
import { List, ListItem, Pagination, Error, Null } from "@genko-me/react/next";

export default function BlogPage() {
  return (
    <main>
      <h1>ブログ</h1>
      <List api={client.blog} limit={6} orders="-publishedAt" href="/blog/{id}">
        <article><h2><ListItem id="title" /></h2></article>
        <Pagination />
        <Error template />
        <Null>公開されている記事はありません。</Null>
      </List>
    </main>
  );
}

詳細ページも同じ形です。

app/blog/[id]/page.tsx
import { client } from "@/lib/client";
import { View, ViewItem, Error, Null } from "@genko-me/react/next";

type Props = {
  params: Promise<{ id: string }>;
  searchParams: Promise<Record<string, string | string[] | undefined>>;
};

export default async function BlogDetail({ params, searchParams }: Props) {
  const { id } = await params;
  return (
    <View api={client.blog} id={id} searchParams={searchParams}>
      <h1><ViewItem id="title" /></h1>
      <ViewItem id="body" />
      <Error template />
      <Null>記事が見つかりません。</Null>
    </View>
  );
}

取得・ページ送り・エラー・0件の表示は部品が持っているので、ページに書くのは「どこに何を出すか」だけです。

記事の ID は、管理画面の記事設定で自分で決められます。table-of-contents のような読める ID にしておけば、/blog/{id} の URL もそのまま読める形になります。

表示部品は React・Vue・Svelte 向けがあり、Vite で作ったアプリでもそのまま使えます。Astro や Nuxt、PHP・Ruby・Go などから API を直接呼ぶ手順も ドキュメント にまとめています。

登録せずに先に動きを見たい場合は、お知らせページのテンプレートがあります。git clone して npm run dev すれば、同梱のサンプル記事で一覧・詳細・カテゴリー絞り込みが動きます。

書く画面

エディタは Tiptap で作りました。画面の静けさは「しずかなインターネット」を参考にさせてもらっています。

原稿エディタ。id と打つとコードに変わり、選んだ語を書式の帯で太字に、「+」から表を挿入してセルに書き込む

開発者が自分のブログを書く場面を想定して、次のことを大事にしています。

  • Markdown の手癖が通る。 `code`、~~取り消し~~、[文字](URL) と打てば、その場で装飾に変わります
  • Markdown では面倒なものが楽に書ける。 表は行や列をその場で足せます。コードブロックは言語を選べます。画像は原稿の中でトリミングできます
  • 書いている途中で迷わない。 保存状態を常に出し、保存していない変更がある状態で閉じようとすると引き止めます。予約公開と編集履歴もあります
  • スマホでも直せる。 日時や選択肢は、スマホでは OS 標準の選択を開きます

画像を入れる。「+」から画像を選び、フォルダからドラッグして落とすと設定パネルが開く。代替テキストを入れて適用するとアップロードされる

記事設定。「公開予定」を選んで公開開始の日時を選ぶと、見出しに「2026/09/30 09:00から」と出る。URL の節で記事の ID を table-of-contents に変え、閉じて保存する

書く画面は、アカウントを作らずにそのまま触れます。書いた内容はブラウザの中にだけ残り、サーバーには送られません。

作るときに悩んだこと

どれも「使う側がここを気にしなくていいように」決めたことです。

本文の画像を、差し替えても追従させる

リッチテキストの本文は HTML の文字列で返します。サイト側は受け取った HTML を置くだけです。

悩んだのは本文の中の画像です。記事を書いたあとで画像を差し替えたとき、本文に古い URL が焼き付いていると、全部の記事を直して回ることになります。

そこで、本文の中の画像は保存時には内部の ID で持ち、配信するときに URL へ解決するようにしました。画像を差し替えれば、その画像を使っている記事は、次に取得したときから新しい画像になります。

使う側がやることは「保存しておいた古い HTML を使い回さず、コンテンツを取得し直す」だけです。

公開前のプレビューを、公開ページと同じ部品で出す

下書きのプレビューのために、専用のページや API を別に作るのは面倒です。見た目も公開ページとずれます。

genko.me では、管理画面のプレビュー URL を次のように設定します。

https://your-site.example/blog/{id}?draftKey={key}

上の詳細ページの View に searchParams を渡しておけば、draftKey 付きで開かれたときだけ下書きを取りに行きます。プレビュー用の Route Handler や Proxy は不要です。 下書きは常にキャッシュせず、通常の表示は cache={60} のように秒数を指定したときだけキャッシュします。

draftKey は URL に残るので、共有してしまった場合に備えて、記事ごとにキーを作り直せるようにしています。

予約公開を、予約時刻ちょうどに出す

予約公開は、定期実行の処理で「公開」に切り替えるのが素直な作りです。ただそれだと、切り替えの間隔ぶん公開が遅れます。

genko.me では、配信するときの問い合わせそのものが「予約時刻を過ぎた記事は公開として扱う」ようになっています。公開終了日時も同じで、過ぎた瞬間から配信されなくなります。定期実行は、あとから状態を「公開」「公開終了」に確定させるだけです。

予約の日時は、アカウントに設定したタイムゾーンの時刻として入力します。夏時間のある地域では、切り替わりの前後で同じ「9時」が違う瞬間を指すので、変換ではオフセットを2回取り直して、境目でずれないようにしています。

管理画面で項目を変えても、型が追いつく

管理画面でフィールドを足したり変えたりすると、サイト側の型が古くなります。

npx @genko-me/cli pull

を実行すると、スキーマを取得し直して lib/schema.ts を作り直します。pull --watch にすると、開発中は変更を拾い続けます。

AI に組み込みを任せる

Claude Code や Cursor に「genko.me でお知らせページを作って」と頼めるように、ドキュメントを AI 向けにも出しています。

  • 各ページの URL の末尾に .md を付けると Markdown で返る
  • 索引の https://docs.genko.me/llms.txt と、全文の llms-full.txt
  • ドキュメントを検索できる MCP サーバー

MCP は CLI から登録できます。

npx @genko-me/cli mcp --claude-code

--cursor や --codex にも対応しています。登録すると、AI が作業の途中で genko.me の手順を検索して参照できます。

いまの MCP サーバーは、ドキュメントを検索して読むためのものです。今後は、MCP から記事の作成・更新など、管理画面での操作もできるようにする予定です。AI に「この記事を下書きで作って」「予約公開にして」と頼めるところまで広げていきます。

おわりに

ブログやお知らせの「毎回同じものを作る部分」と「書く時間」を、両方とも軽くしたくて作っています。自分のサイトにブログを足したい、書く画面にこだわりたい、という人に試してもらえたらうれしいです。


この記事は Zenn にも同じ内容で投稿しています。
https://zenn.dev/ichi_107/articles/572c998f57017f

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?