Forestry (TinaCMS)

Git-basedのヘッドレスCMS。JAMstackワークフローに最適化された開発者ファーストのCMS。

CMSヘッドレスGit-basedJAMstackTypeScriptReact静的サイト
ライセンス
Apache 2.0
言語
TypeScript/React
料金
オープンソース版無料

CMS

Forestry (TinaCMS)

概要

Forestryは、Git-basedのヘッドレスCMSとして開発者に愛されていましたが、2023年4月22日にサービスを終了し、その後継としてTinaCMSが開発されています。

詳細

Forestry.ioは、JAMstackワークフローに最適化されたGit-basedのヘッドレスCMSとして、多くの開発者に支持されていました。Gitリポジトリと直接連携し、MarkdownやYAMLファイルを管理する直感的なインターフェースを提供していました。しかし、Forestryチームは次世代のビジョンとしてTinaCMSの開発に注力することを決定し、2023年4月にForestryのサービスを終了しました。

TinaCMSは、Forestryの良い点を引き継ぎながら、より高度な機能を提供しています。特に注目すべきは、ビジュアル編集機能とTypeScriptによる型安全性です。React製のモダンなアーキテクチャを採用し、Next.jsやGatsbyなどの静的サイトジェネレータとのシームレスな統合を実現しています。GitHubやGitLabとの統合により、開発者フレンドリーなワークフローを維持しながら、非技術者でも使いやすいビジュアルエディタを提供しています。

メリット・デメリット

メリット

  • Git-basedワークフロー: バージョン管理と開発フローが統合
  • ビジュアル編集: リアルタイムプレビューとclick-to-edit機能
  • 型安全性: TypeScriptによる完全な型サポート
  • オープンソース: カスタマイズ可能でベンダーロックインなし
  • モダンな開発体験: React/TypeScriptベースの最新技術スタック
  • 柔軟な認証: ローカル開発とクラウド認証の両方をサポート
  • GraphQL API: 効率的なデータフェッチング

デメリット

  • Forestryからの移行が必要: 既存Forestryユーザーは移行作業が発生
  • 学習コスト: 新しいコンセプトとAPIの習得が必要
  • React専用: 他のフレームワークとの統合は限定的
  • 初期設定の複雑さ: 環境構築に技術的知識が必要
  • エコシステムの規模: Forestryと比べてまだ発展途上

主要リンク

使い方の例

TinaCMSプロジェクトの初期化(Forestry移行)

# 新規プロジェクト作成
npx create-tina-app@latest my-site

# 既存Forestryプロジェクトの移行
npx @tinacms/cli init
# "Migrate your .forestry folder?"に"y"と回答

# 開発サーバー起動
npm run dev

スキーマ定義(TypeScript)

// .tina/schema.ts
import { defineSchema } from "@tinacms/cli";

export default defineSchema({
  collections: [
    {
      label: "Blog Posts",
      name: "post",
      path: "content/posts",
      fields: [
        {
          type: "string",
          label: "Title",
          name: "title",
          required: true,
        },
        {
          type: "string",
          label: "Body",
          name: "body",
          isBody: true,
          ui: {
            component: "textarea",
          },
        },
        {
          type: "reference",
          label: "Author",
          name: "author",
          collections: ["author"],
        },
      ],
    },
  ],
});

データベース設定(Git Provider)

// tina/database.ts
import { createDatabase, createLocalDatabase } from "@tinacms/datalayer";
import { GitHubProvider } from "tinacms-gitprovider-github";
import { MongodbLevel } from "mongodb-level";

const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true";

export default isLocal
  ? createLocalDatabase()
  : createDatabase({
      gitProvider: new GitHubProvider({
        branch: process.env.GITHUB_BRANCH,
        owner: process.env.GITHUB_OWNER,
        repo: process.env.GITHUB_REPO,
        token: process.env.GITHUB_PERSONAL_ACCESS_TOKEN,
      }),
      databaseAdapter: new MongodbLevel({
        collectionName: "tinacms",
        dbName: "tinacms",
        mongoUri: process.env.MONGODB_URI,
      }),
    });

Next.jsでの統合

// pages/_app.tsx
import { TinaEditProvider } from 'tinacms/dist/edit-state'
import TinaCMS from 'tinacms'

function MyApp({ Component, pageProps }) {
  return (
    <TinaEditProvider
      editMode={
        <TinaCMS
          apiURL={process.env.NEXT_PUBLIC_TINA_API_URL}
          {...pageProps}
        >
          {(livePageProps) => <Component {...livePageProps} />}
        </TinaCMS>
      }
    >
      <Component {...pageProps} />
    </TinaEditProvider>
  )
}

export default MyApp

バックエンド認証設定

// pages/api/tina/[...routes].ts
import { TinaNodeBackend, LocalBackendAuthProvider } from "@tinacms/datalayer";
import { TinaAuthJSOptions, AuthJsBackendAuthProvider } from "tinacms-authjs";
import databaseClient from "../../../tina/__generated__/databaseClient";

const isLocal = process.env.TINA_PUBLIC_IS_LOCAL === "true";

const handler = TinaNodeBackend({
  authProvider: isLocal
    ? LocalBackendAuthProvider()
    : AuthJsBackendAuthProvider({
        authOptions: TinaAuthJSOptions({
          databaseClient: databaseClient,
          secret: process.env.NEXTAUTH_SECRET,
        }),
      }),
  databaseClient,
});

export default handler;

ビジュアル編集の実装

// components/VisualEditor.tsx
import { useTina } from 'tinacms/dist/react'
import { useVisualEditing } from '@tinacms/vercel-previews'

export const BlogPost = (props) => {
  const { data: tinaData } = useTina(props)
  
  // ビジュアル編集を有効化
  const data = useVisualEditing({
    data: tinaData,
    query: props.query,
    variables: props.variables,
    enabled: true,
    stringEncoding: true, // 文字列に自動的にメタデータを追加
  })

  return (
    <article>
      <h1>{data.post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.post.body }} />
    </article>
  )
}