Go Headless with Netlify CMS
Tags: tutorial, beginners, javascript, webdev
Published: 2021-03-31 15:19:10

Got a site? How easy is it to make content changes? Are you using a CMS? What's a CMS? How do I set up a CMS?

After this article, you'll know how to get up and running with [Netlify CMS](https://www.netlifycms.org/). And you'll know some tricks to make your life easier.

- Set up Netlify CMS with Next.js
- Use `netlify-cli`
- Make use of Netlify CMS Beta features!

## What's a CMS?

A content management system(CMS) is a convenient way for you to manage content. Think of it as a special place you visit that allows you to update your website. Update images, copy, content! It can live on the same domain or a completely different one.

What's a "headless" CMS? This means that our CMS is not coupled to a front end. It has no notion of how the content will get displayed. This is ideal. It gives portability to our content. Want to switch front-end frameworks? Cool! Drop all your content into something new. The underlying structure of what you're presenting doesn't have to change.

## Why CMS?

We mentioned it there. But, "Portability". Decoupling your content from the presentation layer has the benefit of "Create once, display anywhere!". Also, "Convenience". Realized you made a typo on your site? But, you're not at your desk? Log into your CMS, make the change, hit "Publish", done! It also opens your content up for a more collaborative experience. Want to support different authors or guest posting? Create separate logins for those users.

## Netlify CMS

Netlify CMS is a great option if you're new to CMS and even if you're not. It's an interesting offering that is "Git Based". That means it creates and updates the content in your Git repo. This is great if you're new to CMSs as you have the ability to see the content changes on your machine as you develop. It's very [customizable](https://www.netlifycms.org/docs/customization/) too. Think custom widgets and previews.

## Setting Up Netlify CMS

For today's demo, we're going to set up Netlify CMS with a Next.js site. The set up is the same though for whatever you use. If you don't have an app to use, follow along and create a Next.js site.

The prerequisites?

- Github/Gitlab/Bitbucket account
- Netlify account
- Optional – An app to integrate. Or create a demo app from the steps below.

For those in camp **TL;DR** , you can grab the demo app and starter kit from [this repo](https://github.com/jh3y/netlify-cms-next-starter).

### Create App

Let's get started by creating an app.

```shell
yarn create next-app netlify-cms-next-starter
```

And for this app, I've gone ahead and pulled Tailwind in with the new shiny "JIT" compiler.

### Install Netlify CMS

Then we need `netlify-cms-app`.

```shell
yarn add netlify-cms-app
```

### Set Up Admin Page

Next, we need an "admin" page. This needs to be accessible via `/admin`. Different frameworks have different ways of doing this. We're using Next.js. We could drop an HTML file into `public` at `public/admin/index.html` or create `pages/admin/index.js`.

The straight HTML would use the CDN links for Netlify CMS and looks like this.

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Admin: Netlify CMS && Next.js Starter</title>
    <link rel="shortcut icon" href="/assets/icons/favicon-32x32.png" />
    <link rel="apple-touch-icon" href="/assets/icons/logo-192x192.png" />
    <link rel="manifest" href="/manifest.webmanifest" />
    <script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
  </head>
  <body>
    <!-- Include the script that builds the page and powers Netlify CMS -->
    <script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
  </body>
</html>
```

But, we've installed `netlify-cms-app`. That means we can start up the CMS from a module.

```javascript
import CMS from 'netlify-cms-app'
// Initialize the CMS object
CMS.init()
```

We're using Next.js though. And that means we can do something a little different with our setup.

```javascript
import dynamic from 'next/dynamic'

const CMS_CONFIG = {}
const Loading = () => (
  <div className="min-h-screen flex items-center justify-center">
    <p className="text-gray-500 font-semibold text-xl">Loading...</p>
  </div>
)

const CMS = dynamic(
  () =>
    import('netlify-cms-app').then((CMS) => {
      CMS.init({ CMS_CONFIG })
    }),
  { ssr: false, loading: Loading }
)

const Admin = () => <CMS />

export default Admin
```

Here, we load the `netlify-cms-app` dynamically into the browser with `next/dynamic` and then initialize the CMS.

### Set Up CMS Config

The CMS is config-driven. We create a configuration file and this tells Netlify CMS what fields to show, what to update, etc. We need a `config.yml` file that our "Admin" page has access to. There are a variety of options for configuring the CMS. Here's a basic starting point. In this config, we're creating two collections. One handles global data such as site copy for example. The other is for standalone pages. This could be the same structure for creating blog posts, etc.

```yaml
backend:
  name: git-gateway
  branch: main
publish_mode: editorial_workflow
media_folder: "public/images"
public_folder: "/images"
collections:
  - label: "Globals"
    name: "globals"
    files:
      - label: "Site Copy"
        name: "site_copy"
        delete: false
        create: true
        file: "_data/copy.json"
        fields:
          - { label: "Tagline", name: "tagline", widget: "string" }
          - { label: "Headline", name: "headline", widget: "string" }
  - name: "standalone pages"
    label: "Standalone Pages"
    folder: "standalone-pages"
    slug: ""
    file: "standalone-pages/.mdx"
    create: true
    extension: mdx
    format: frontmatter
    fields:
      - { label: "Title", name: "title", widget: "string" }
      - {
          label: "SEO Meta Title",
          name: "metaTitle",
          widget: "string",
          required: false,
        }
      - {
          label: "SEO Meta Description",
          name: "metaDesc",
          widget: "string",
          required: false,
        }
      - {
          label: "Social Image",
          name: "socialImage",
          widget: "image",
          required: false,
        }
      - { label: "Body", name: "body", widget: "markdown" }
```

We've got so many options for setting up Netlify CMS. The main things here are the "backend", "media\_folder", and how our collections work.

When it comes to collections, we can define the types of files, where they live, how we create the content. For example, our standalone page collection. We are stating that the files will have the MDX `extension` and live under `standalone-pages/`. Each field under `fields` makes up the managed content for a page. In our example, we're using the `string` widget for text fields. But, the `body` of the file will be `markdown`. Note the `format` field on our collection? This tells Netlify CMS that we want to store the data in `frontmatter` and follow it with the content for `body`. It's worth checking out the "[Configuration](https://www.netlifycms.org/docs/configuration-options/)" docs and "[Fields](https://www.netlifycms.org/docs/widgets/)" docs.

### Creating An Identity

If we start up our app and visit `/admin` we're hit with a login screen. The CMS is working! But, we don't have any credentials to log in with.

![Blank Netlify CMS login screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j9ras3tzbxxyjwxr0knn.png)

We need an identity for authentication. You can use different auth options. But, we're going to use Netlify Identity for auth. It's the quickest to set up and will give you exposure to using Netlify Identity if you've not used it before.

Open up your Netlify dashboard and you'll want to create a "New site from Git". That's assuming you've pushed your app to a Git repository.

!["New site from Git" button on Netlify dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eau6wdrmxqztqvkxvrnh.png)

This will take you through your site setup. Follow the prompts and Netlify will detect your build settings for you. Note how in our case, we get the neat "Essential Next.js" build plugin installed for us.

![New site set up on Netlify](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6q2riuhpvyzov3rub9os.png)

Now we need to enable "Identity" for our site. Navigate to the "Identity" tab and hit "Enable Identity".

![Enabling Identity on Netlify dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpxp1o5rlerrjmprx9oa.png)

Go into "Settings and usage" and hit "Enable Git Gateway". It's a bit of scrolling down.

![Enabling Git Gateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7rqtbaqvomvxdvhjrs3l.png)

We are almost there! Now we need to invite a user to use our CMS. Hit "Invite users" and send an invite to an email address you want to use.

![Inviting users](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e7r9nnrrwgjbwldf5c4y.png)

Once you've done that, you'll see the user in the list.

![Updated users list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yu7pqiaip68cegv71vsc.png)

Check your email and you should see one inviting you to join the app. But, if you hit the link, you'll get directed to your hosted site and nothing will happen. That's because we need to drop the identity widget into the page and run a piece of code for it.

This only needs to doing on our root page. We need the following code to run.

```javascript
if (window.netlifyIdentity) {
  window.netlifyIdentity.on('init', (user) => {
    if (!user) {
      window.netlifyIdentity.on('login', () => {
        document.location.href = '/admin/'
      })
    }
  })
}
```

And this script included.

```html
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
```

For our Next.js app, we could update our "Home" page to run the code in an effect and use `next/head` to include the script.

```javascript
import { useEffect, Fragment } from 'react'
import Head from 'next/head'

const Home = () => {
  useEffect(() => {
    if (window.netlifyIdentity) {
      window.netlifyIdentity.on('init', (user) => {
        if (!user) {
          window.netlifyIdentity.on('login', () => {
            document.location.href = '/admin/'
          })
        }
      })
    }
  }, [])
  return (
    <Fragment>
      <Head>
        <title>Netlify CMS && Next.js Starter</title>
        <script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
      </Head>
      <main className="min-h-screen flex items-center justify-center mx-auto flex-col prose">
        <h1 className="text-center">Netlify CMS && Next.js Starter</h1>
      </main>
    </Fragment>
  )
}
```

### Re-Deploy

For the changes to take effect, we need to re-deploy our site. We can do this by pushing our changes up to our git repo. Netlify will detect the changes and re-deploy for us.

Or, we could use `netlify-cli` to re-deploy our site. I recommend making use of `netlify-cli`. It can improve your workflow a lot. Especially if you've already got your terminal open. To use `netlify-cli`, install it first.

```shell
npm i -g netlify-cli
```

Once installed, login with

```shell
netlify login
```

And then from within your repo directory, link your repo to your site with

```shell
netlify link
```

Now you can build and deploy from the command line. Your build settings including plugins get pulled down for you too. It's a great way to get a deploy preview without opening a pull request too.

```shell
netlify build
netlify deploy
```

When you're happy with what you're deploying.

```shell
netlify build
netlify deploy --prod
```

## Access the CMS

Once that's deployed, use your invite link from the email and you'll see a form to "Complete your signup". Create a password and you'll get logged in.

![Complete sign up form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2wr8dtdez49ujdojzjbk.png)

Now visit `/admin` and you'll get prompted for your CMS login details. Log in and we're in! We've set up a CMS for your site with Netlify CMS.

## Update Content

Now we're in the CMS, have a poke around and explore what you can do. For example, upload an image or change the content in one of your collections. Here I've changed the content under a "Tagline" field in our "Globals" collection. When we're ready to push the changes, we hit "Save", update the status to "Ready", and then hit "Publish".

![Publishing copy changes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pwumfx60lbaukqsj4ars.png)

Next, it's a waiting game. Wait for the changes to get pushed. Pull them down to your repo in your terminal and you'll see that in our case `_data/copy.json` has updated. This was the path we defined in our `config.yml` file.

Now you can integrate your content any way you like with your frontend. For example, in our demo app, I've added an import path for `_data`. And I've updated the "Home" page to display the headline and tagline.

```javascript
import { useEffect, Fragment } from 'react'
import Head from 'next/head'
import copy from '@data/copy.json'

const Home = () => {
  useEffect(() => {...}, [])
  return (
    <Fragment>
      <Head>
        <title>Netlify CMS && Next.js</title>
        <script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script>
      </Head>
      <main className="min-h-screen flex items-center justify-center mx-auto flex-col prose">
        <h1 className="text-center">{copy.headline}</h1>
        <h2>{copy.tagline}</h2>
      </main>
    </Fragment>
  )
}
```

At this stage, you've got a working CMS for your app. Explore [the docs](https://www.netlifycms.org/docs/) and manage your content any way you like. But, we're not quite done yet. Let's improve this setup.

## Local Backend

We've got a working CMS. But, the workflow could be slow. Especially if we're feeling out what we're going to put in our CMS. We haven't got time to keep deploying, making changes in the CMS, and waiting for things to get pushed to Git. Builds take time and they'll use up your build minutes on Netlify.

To get around this we're going to use a [Beta feature](https://www.netlifycms.org/docs/beta-features/), `local_backend`. In fact, if you've been using Netlify CMS already, this trick might help you out a bunch if you didn't know about it.

To use `local_backend`, add it to our `config.yml` file.

```yaml
local_backend: true
backend:
  name: git-gateway
  branch: main
publish_mode: editorial_workflow
media_folder: "public/images"
public_folder: "/images"
```

And then we have one more step. We need to use `netlify-cms-proxy-server` when we are developing. We can update our `dev` script to accommodate this.

```json
"scripts": {
  "dev": "npx netlify-cms-proxy-server & next dev",
}
```

Run our dev server and visit `/admin`. We're in! No log-in screen required. Make some content changes, hit publish, and the content gets updated in your directory. Awesome. This improves our workflow tenfold. No more waiting.

![Netlify CMS locally](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fjzuvwv2u2e5awkps93n.png)

This is all possible because of `netlify-cms-proxy-server` which creates a local unauthenticated server. This is what the CMS runs on when we use `local_backend`. You can configure `local_backend` in different ways. And you don't have to worry if you deploy a `config.yml` file with `local_backend` on.

But, if that doesn't sit right with you, you can write a node script that you run when running your dev server.

Here's a script you can use that copies your `config.yml` to the `public` directory on change. It uses `chokidar` to watch the file changes and logs any activity with `pino`.

```javascript
const chokidar = require('chokidar')
const logger = require('pino')({ prettyPrint: true })
const fs = require('fs')

// Write file to public/config.yml
const writeConfig = () => {
  logger.info('Updating Netlify CMS Config')
  const CONFIG = fs.readFileSync('./admin/config.yml', 'utf-8')
  fs.writeFileSync(
    `${process.cwd()}/public/config.yml`,
    `local_backend: true\n${CONFIG}`,
    'utf-8'
  )
}
// Set up Netlify CMS Config Watching
logger.info('Setting up Netlify CMS config watch')
chokidar.watch(`${process.cwd()}/admin/config.yml`).on('change', writeConfig)
// Write on script run so it's there
writeConfig()
```

In this script, we are copying the config and appending the `local_backend` option. This means we no longer need a committed version of `config.yml` inside `public`. In the demo app, I've moved it into an `admin` directory along with the watch script.

To have that run at dev time, we can update our `package.json` script to run this alongside `netlify-cms-proxy-server`.

```json
{
  "scripts": {
    "dev": "npx netlify-cms-proxy-server & next dev & node admin/config-watcher.js"
  }
}
```

## That's It!

That's how to get up and running with a "Headless" CMS. In this case, Netlify CMS. It's a neat solution and a great entry point if you've not set up a CMS for yourself before.

I'd recommend setting up a CMS for your sites. Especially your personal portfolio sites. How about trying Netlify CMS? Get set up, have a look around [the docs](https://www.netlifycms.org/docs/), [customize it](https://www.netlifycms.org/docs/customization/) to your needs!

Want to try it out with Next.js? Check out the [demo repository](https://github.com/jh3y/netlify-cms-next-starter) that's set up with Tailwind and use that as a starting point.

As always, thanks for reading. Wanna know more? Wanna see something else? Come find me on [Twitter](https://twitter.com/jh3yy)!

**Stay awesome! ʕ •ᴥ•ʔ**