profile

CMSごとサイトを作り替えました

Published at

2025/01/05

Updated at

2025/01/05

Written by

marromugi

新しくウェブサイトを作り直しました。
前はヘッドレスCMSを使っていたのですが、今回のリニューアルで自分でCMSごと作り替えてみました。エディタ周りなどの実装がどんな感じなので触って見たかったのが本音ですが、今回の記事ではその内容を振り返ってみます。

技術構成

プロジェクト自体は admin / client / api の3つのアプリと、article という1つのライブラリで monorepo で組んでいます。

フロントエンド

フロントは 管理画面 / クライアント画面どちらも Nextjs で実装しています。管理画面では Page Router を、クライアント画面では App Router を採用しています。
Remix と迷ったのですが、ささっと実装したかった & React-Router との統合周りのキャッチアップができていなかったので Next を採用しました。
データ取得周りは Hono RPCSWR を併用して実装しています。公式にもサンプルがあるのですが、ステータスコードのよる分岐があると都度判定をしないといけなかったため(半年ほど前なので現在は不明ですが)、SWR に渡している関数内で 20X 系であれば success, それ以外は error とするよう実装しています。
また、API の型であるAppType は api でビルドしたものを使用しています。
import { AppType } from '@my-service/api'
import { hc } from 'hono/client'

export const client = hc<AppType>(process.env.NEXT_PUBLIC_API_URL ?? '', {
  init: {
    credentials: 'include',
    mode: 'cors'
  }
})

export const useQuery = <T extends GetRequestClient>(
  client: T,
  args: ApiParameters<T['$get']> & { key?: string },
  options?: {
    swr?: SWRConfiguration<
      ApiResponseContent<T['$get']>,
      ApiResponse<T['$get'], Exclude<StatusCode, SuccessStatusCode>>
    >
    fetch?: ClientRequestOptions<ApiResponseContent<T['$get']>>
    enabled?: boolean
  }
) => {
  return useSWR(...
}
クライアントのキャッシュは記事のステータス更新 or 記事保存時に invalidate しています。

API

Hono を採用しています。認証まわりなどミドルウェアを簡単に実装でき、尚且つビルドインの機能も多い & シンプルで個人的にすごく気に入っています。
API周りの型を他のアプリ(admin / client)でも使用できるように、tsup を用いてビルドしています。
Cloudflare Workers 上で実行しています。

DB

db は D1 を利用しており、ORM には prisma を採用しています。無料枠も多く、コスト面を重視して採用しました。D1 はトランザクションが利用できないなどの点があるので、フォールバックの実装などが少し面倒ですが、そこまで複雑な仕様にならない予定だったため問題ないかなあと思い採用しました。
実態は sqlite なので、ローカル環境も簡単に構築でき、実装はストレスフリーでした。

ストレージ

Cloudflare R2 を利用しています。こちらも安さ重視です。

OGP

OGP の作成には、Cloudflare Browser Rendering API を利用しています。
流れ的には以下の感じです。puppeteer を利用するのに、Cloudflare Browser Rendering API が必要です。(Cloudflare Browser Rendering API の利用には Cloudflare の有料プランに加入する必要があります)
  • Puppeteer 上で HTML をレンダリングしスクショ
  • スクショを R2 に保存
  • パスを D1 上の投稿に紐付け

エディタ

エディタの実装には Tiptap を採用しています。
一番実装に時間がかかった部分だと思います。基本的なエディタ機能に関しては Tiptap 側で Starter Kit を用意してくれているので概ね問題ありませんでした。
しかし、コピペによる画像アップロードやコードエディタの実装に関しては自前でやる必要があり、一部の実装に関しては Tiptap の元である ProseMirror のAPIにアクセスして実装する必要があります。
image.png
ちなみに最初は Tiptap を使わず自前でできないかと思って実装してみたのですが、Caret 周りの操作やブラウザの挙動の把握などが大変で断念しました(無謀)
Tiptap の extensions はライブラリとして切り出しており、 admin / api のどちらでもパースできるようにしています。

テスト

今回かなり突貫だったので、あまりテストを書かずに実装してしまいました。。
基盤自体は用意しており、Vitest を利用しています。
DB に関しても、都度作成・破棄して実際にデータのやり取りをできるようにしています。@cloudflare/vitest-pool-workers/config を使って、マイグレーションファイルを env に割り当て、cloudflare:testapplyD1Migrations でマイグレートすることができます。
// vitest.config.ts

import path from 'node:path'
import {
  defineWorkersProject,
  readD1Migrations
} from '@cloudflare/vitest-pool-workers/config'

export default defineWorkersProject(async () => {
  const migrationsPath = path.join(__dirname, 'migrations')
  const migrations = await readD1Migrations(migrationsPath)

  return {
    test: {
      setupFiles: ['./src/test/setup.ts'],
      poolOptions: {
        workers: {
          singleWorker: true,
          wrangler: {
            configPath: './wrangler.toml'
          },
          miniflare: {
            bindings: { TEST_MIGRATIONS: migrations }
          }
        }
      },
      testTimeout: 10000
    }
  }
})
// setup.ts
import { applyD1Migrations, env } from "cloudflare:test";

await applyD1Migrations(env.DB, env.TEST_MIGRATIONS);

LLM

OpenAI の API を利用して、gpt-4o-mini を利用しています。
このアプリには翻訳機能を載せており、ボタンを押すと全記事を特定の言語に翻訳してくれます。HTMLの構造が崩れる場合があり、少し改善が必要ですが翻訳自体はある程度問題なさそうです。

おわり(今年の抱負)

今年は1ヶ月に1投稿しようと思います。何かしらプロダクトも2個はリリースしようと思います。