Forestry (TinaCMS)

Git-based headless CMS. Developer-first CMS optimized for JAMstack workflows.

CMSHeadlessGit-basedJAMstackTypeScriptReactStatic Sites
License
Apache 2.0
Language
TypeScript/React
Pricing
オープンソース版無料
Official Site
Visit Official Site

CMS

Forestry (TinaCMS)

Overview

Forestry was a beloved Git-based headless CMS among developers, but it ended its service on April 22, 2023, with TinaCMS being developed as its successor.

Details

Forestry.io was supported by many developers as a Git-based headless CMS optimized for JAMstack workflows. It provided an intuitive interface for managing Markdown and YAML files through direct integration with Git repositories. However, the Forestry team decided to focus on developing TinaCMS as their next-generation vision and ended Forestry's service in April 2023.

TinaCMS inherits the best aspects of Forestry while offering more advanced features. Particularly noteworthy are its visual editing capabilities and type safety through TypeScript. Adopting a modern React-based architecture, it achieves seamless integration with static site generators like Next.js and Gatsby. Through integration with GitHub and GitLab, it maintains developer-friendly workflows while providing a visual editor that's accessible to non-technical users.

Pros and Cons

Pros

  • Git-based workflow: Integrated version control and development flow
  • Visual editing: Real-time preview and click-to-edit functionality
  • Type safety: Complete type support through TypeScript
  • Open source: Customizable without vendor lock-in
  • Modern developer experience: Latest tech stack based on React/TypeScript
  • Flexible authentication: Supports both local development and cloud authentication
  • GraphQL API: Efficient data fetching

Cons

  • Migration required from Forestry: Existing Forestry users need to perform migration work
  • Learning curve: Need to learn new concepts and APIs
  • React-specific: Limited integration with other frameworks
  • Complex initial setup: Technical knowledge required for environment setup
  • Ecosystem size: Still developing compared to Forestry

Main Links

Usage Examples

Initializing TinaCMS Project (Forestry Migration)

# Create new project
npx create-tina-app@latest my-site

# Migrate existing Forestry project
npx @tinacms/cli init
# Answer "y" to "Migrate your .forestry folder?"

# Start development server
npm run dev

Schema Definition (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"],
        },
      ],
    },
  ],
});

Database Configuration (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 Integration

// 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

Backend Authentication Setup

// 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;

Visual Editing Implementation

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

export const BlogPost = (props) => {
  const { data: tinaData } = useTina(props)
  
  // Enable visual editing
  const data = useVisualEditing({
    data: tinaData,
    query: props.query,
    variables: props.variables,
    enabled: true,
    stringEncoding: true, // Automatically add metadata to strings
  })

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