Migrating a Hugo site from Forestry to Tina

illustrations illustrations illustrations illustrations illustrations illustrations illustrations

Published on 5 January 2023 by Andrew Owen (7 minutes)

I launched the current version of my website a year ago. Having become a developer advocate in 2021, I didn’t think a WordPress site that hadn’t been updated in a decade would cut it any more. I wanted to do something a bit more modern. At my previous company, I’d built a developer portal on Hugo. The company ended up hosting the site itself, but I’d had discussions with Netlify and Forestry at the time. And I’d been using GitHub for my big open source projects for a long time. I picked a free Hugo starter theme from Themefisher that had built-in support for Netlify and Forestry. I spent a weekend on it: setting up the site structure, customizing the theme and adding content. I didn’t have all the features at first (search, tags and RSS came later), but it was a huge step up from my old site.

According to Jamstack, Vercel’s Next.js with its React templates has overtaken Hugo in popularity. And I’m not surprised. By most measures, JavaScript has never been out of the top 10 programming languages over the last two decades. And of 347 static site generators listed, 130 are written in JavaScript, 51 are written in Python and 26 are written in PHP. Hugo, and 16 others, are written in Go. But Hugo claims to be the fastest (you can check out those claims with PageSpeed) so I’m sticking with it. However, since 2019 the team behind Forestry have been developing the next iteration called TinaCMS. And on November 8, 2022 it came out of beta. Forestry is scheduled to be discontinued in late March 2023. Existing users will be offered a migration path to TinaCMS, a next generation headless CMS from the creators of Forestry. There are plans to share the migration tool in mid-January, but I decided to go ahead and do a manual migration.

I decided to take the opportunity to do some clean up and add a Hugo shortcode for audio (thanks to John Arrroyo for information on how to do that). I also finally got around to adding a custom 404 page. I created a new empty GitHub repository and connected it to a new staging site on Netlify. I checked out a local copy and brought in the content from the old version of the site. You can run TinaCMS locally, so after installation I was able to make changes before pushing to staging. I removed the Forestry config (although it wouldn’t have done any harm to leave it in place). But I’ll assume you want to simply add TinaCMS support to your existing repo.

Create the project

  1. Register for a Tina account and log in.
  2. From the Dashboard, navigate to Projects and click New Project.
  3. Click Import Your Site, then click Authenticate with GitHub. If you’re not already logged in to GitHub, do it now.
  4. Select the repository for your Hugo site and enter the site URLs (your live site’s URL and localhost with the port you want to use when you’re doing local editing).
  5. Click Create Project.

Set up your site schema

You’ll need npm. If it’s not already installed:

  • On macOS with Homebrew: brew install npm.
  • On Ubuntu: sudo apt install npm.
  • On Windows with Scoop: scoop install npm.

You also need hugo. You can install it the same way you installed npm.

  1. Check out a local copy of your repo from GitHub.
  2. In the root of the repo folder: npx @tinacms/cli@latest init.
  3. When prompted to choose your package manager, select yarn.
  4. Choose if you want to use Typescript.
  5. When prompted for the public assets’ storage folder name, enter static.
  6. Start TinaCMS: npx tinacms dev -c "hugo server -D -p 3003".
  7. Navigate to the admin page: http://localhost:3003/admin.

Because I used VScode, I created a tasks.json file in the .vscode folder to automate deploying TinaCMS locally:

  "version": "2.0.0",
  "tasks": [
      "label": "start TinaCMS",
      "type": "shell",
      "command": "npx tinacms dev -c \"hugo server -D\"",
      "group": {
        "kind": "build",
        "isDefault": true

By default, TinaCMS expects to find images in a media folder. Edit your TinaCMS config file to point to /static/images/ or wherever you keep your images. For example:

    media: {
      tina: {
        mediaRoot: "images",
        publicFolder: "static",

Model your content

Before you can edit your content, you need to model it, based on the metadata you’re using in the headers of your markdown files. In my case I’m using date, description, draft status, image, a tags list and a title. Besides the metadata, you also need to define the body text. My schema looks like this:

schema: {
      collections: [
          name: "blog",
          format: "md",
          label: "Blog",
          path: "content/blog/",
          defaultItem: () => {
            return {
              draft: true,
          fields: [
              name: "draft",
              type: "boolean",
              label: "Draft",
              required: true,

I have a single collection called Blog that corresponds to the folder where my articles go (content/blog). The draft field is a Boolean that determines if the article is displayed. You can set a default value for new articles in the defaultItem list so that new articles are all created as drafts.

              name: "title",
              type: "string",
              label: "Title",
              isTitle: true,
              required: true,
              name: "date",
              type: "datetime",
              label: "Date",
              name: "description",
              type: "string",
              label: "Description",
              name: "image",
              type: "image",
              label: "Image",

The title, date, description, and image are all fairly self-explanatory. You can set the required value to true to prevent saving an article that’s missing a required field.

              name: 'tags',
              type: 'string',
              label: 'Tags',
              list: true,
              name: 'body',
              type: 'rich-text',
              isBody: true,
              label: "Body",
              templates: [
                  name: 'shortcode',
                  label: 'shortcode',
                  match: {
                    start: '{{',
                    end: '}}',
                  fields: [
                      // Be sure to call this field `text`
                      name: 'text',
                      label: 'Text',
                      type: 'string',
                      required: true,
                      isTitle: true,
                      ui: {
                        component: 'textarea',

The tags are a set of text strings. For items like this, set the list value to true. Setting the type to rich-text enables the GUI editor for the body text. To be able to include Hugo shortcodes, you need to include the above template for them. In practice, I found the need to include the opening and closing angle brackets in the start and end items so that no space would be inserted between the curly brackets and the angle bracket (because a space kills the audio shortcode).

Fix your Markdown

This part is a headache, but until there’s a Forestry to Tina migration tool, there’s no getting around it. The big problem I encountered was that all my Markdown front matter was in TOML and, at the time of writing, TinaCMS only supports YAML. I used search and replace in VScode. But there is a better way.

Enable Tina Cloud in TinaCMS

  1. Log into Tina Cloud.
  2. Navigate to Overview and get a copy of your clientID.
  3. Navigate to Tokens and click New Token.
  4. Give the token a name. For example: Production Content Token.
  5. Enter the Git branches the token has access to. For example: main. Then click Create Token.
  6. Navigate to Tokens and get a copy of the token you just created.
  7. Add your clientID and token to your config:
  export default defineConfig({
    clientId: "",   // Get this from tina.io
    token: "",      // Get this from tina.io

In your netlify.toml file, add TinaCMS to your build command:

publish = "public"
command = "yarn tinacms build && hugo"

You can set this directly in your build settings on Netlify, but whatever is in the file will override whatever is on Netlify.

Now you can push your changes to GitHub. After Netlify deploys your build, you’ll be able to work on your site in Tina Cloud.

Sync your media

Before you start editing. Go to Media Manager, click Sync and then click Sync Media. This copies media assets from the images folder in your designated branch (typically main) in your git repository to Tina Cloud’s asset service. Now you can use those assets in your site with Tina Cloud.


I wrote this before TinaCMS published its Forestry migration tool and guide. I’m indebted to Forestry CEO and co-founder Scott Gallant for sharing a draft of that guide with me when I got stuck (and to JP O’Halloran for writing it). He was also super helpful on the TinaCMS Discord. Fun fact: Tina is named after the llama in “Napoleon Dynamite”.

Also, you may want to decommission your Forestry site. You can do that in Forestry by navigating to My sites. Click the branch drop down and select Remove Site. Then click Remove Site to confirm.