TinaCMS

React製のGit-basedヘッドレスCMS。ビジュアル編集とコード管理を両立する次世代CMS。

CMSヘッドレスGit-basedTypeScriptReactGraphQLビジュアル編集
ライセンス
Apache 2.0
言語
TypeScript/React
料金
開発者プラン無料

CMS

TinaCMS

概要

TinaCMSは、ビジュアル編集とコード管理を両立するReact製のGit-basedヘッドレスCMSで、次世代の開発者ファーストCMSです。

詳細

TinaCMSは、Forestry.ioチームが開発した次世代のGit-based CMSで、ビジュアル編集機能と開発者フレンドリーなワークフローを組み合わせた革新的なCMSです。TypeScriptとReactをフル活用し、型安全性とモダンな開発体験を提供します。

特徴的なのは、「click-to-edit」機能によるビジュアル編集です。ウェブサイトを見ながら直接コンテンツをクリックして編集できるため、非技術者でも直感的に操作できます。同時に、Gitベースのワークフローを採用しているため、開発者は慣れ親しんだバージョン管理プロセスを維持できます。

GraphQL APIを提供し、MDXサポート、リアルタイムプレビュー、ブランチベースの編集など、現代的な開発に必要な機能を網羅しています。Next.jsやGatsbyとの統合が特に優れており、JAMstackプロジェクトに最適です。

メリット・デメリット

メリット

  • ビジュアル編集: click-to-editで直感的なコンテンツ編集
  • 型安全性: TypeScriptフルサポートでエラーを事前に防止
  • モダンなDX: React/TypeScriptベースの最新開発体験
  • Git統合: バージョン管理とCI/CDのシームレスな統合
  • GraphQL API: 効率的なデータフェッチング
  • MDXサポート: リッチなコンテンツ作成が可能
  • オープンソース: カスタマイズと拡張が自由

デメリット

  • React専用: 他のフレームワークでの利用が困難
  • 学習コストが高い: TypeScript/React/GraphQLの知識が必要
  • 新しいプロダクト: エコシステムが発展途上
  • 設定の複雑さ: 初期設定に時間がかかる
  • リソース使用量: ビジュアル編集機能が重い場合がある

主要リンク

使い方の例

プロジェクト初期化

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

# 既存プロジェクトに追加
npx @tinacms/cli init

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

# TinaCMS管理画面: http://localhost:3000/admin

スキーマ定義(型安全)

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

export const schema = defineSchema({
  collections: [
    {
      label: "Articles",
      name: "article",
      path: "content/articles",
      format: "mdx",
      fields: [
        {
          type: "string",
          label: "Title",
          name: "title",
          required: true,
        },
        {
          type: "datetime",
          label: "Published Date",
          name: "publishedAt",
        },
        {
          type: "reference",
          label: "Author",
          name: "author",
          collections: ["author"],
        },
        {
          type: "rich-text",
          label: "Content",
          name: "body",
          isBody: true,
          templates: [
            {
              name: "CodeBlock",
              label: "Code Block",
              fields: [
                {
                  name: "language",
                  label: "Language",
                  type: "string",
                },
                {
                  name: "code",
                  label: "Code",
                  type: "string",
                  ui: {
                    component: "textarea",
                  },
                },
              ],
            },
          ],
        },
      ],
    },
  ],
});

export default defineConfig({
  schema,
  branch: "main",
  clientId: process.env.TINA_CLIENT_ID!,
  token: process.env.TINA_TOKEN!,
  build: {
    publicFolder: "public",
    outputFolder: "admin",
  },
});

Next.js統合とビジュアル編集

// pages/posts/[slug].tsx
import { useTina } from 'tinacms/dist/react'
import { client } from '../../.tina/__generated__/client'

export default function Post(props: {
  data: any
  variables: any
  query: string
}) {
  // ビジュアル編集を有効化
  const { data } = useTina({
    query: props.query,
    variables: props.variables,
    data: props.data,
  })

  return (
    <article>
      <h1>{data.article.title}</h1>
      <TinaMarkdown content={data.article.body} />
    </article>
  )
}

export const getStaticProps = async ({ params }: any) => {
  const { data, query, variables } = await client.queries.article({
    relativePath: params.slug + '.mdx',
  })

  return {
    props: {
      data,
      query,
      variables,
    },
  }
}

GraphQLクエリの実行

// カスタムGraphQLクエリ
import { client } from './.tina/__generated__/client'

export async function getArticles() {
  const articlesResponse = await client.queries.articleConnection()
  
  return articlesResponse.data.articleConnection.edges?.map(
    (article) => ({
      title: article.node?.title,
      slug: article.node?._sys.filename,
      publishedAt: article.node?.publishedAt,
    })
  )
}

カスタムフィールドコンポーネント

// カスタムフィールドを登録
import { wrapFieldsWithMeta } from 'tinacms'

const ColorPickerField = wrapFieldsWithMeta((props) => {
  const { field, input } = props
  
  return (
    <div>
      <label htmlFor={input.name}>{field.label}</label>
      <input
        type="color"
        id={input.name}
        value={input.value}
        onChange={(e) => input.onChange(e.target.value)}
      />
    </div>
  )
})

// CMSに登録
export const tinaConfig = defineConfig({
  // ...
  cmsCallback: (cms) => {
    cms.fields.add({
      name: 'color',
      Component: ColorPickerField,
    })
  },
})

ローカル開発とクラウドの切り替え

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

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!,
      }),
    })