• Guides
  • Delivery
  • How to build a static site using Statamic as headless CMS

How to build a static site using Statamic as headless CMS

September 1, 2022

How to build a static site using Statamic as headless CMS

This is the first article in our series on using headless CMS with Jamstack. In this guide, we are going to build an Astro application using the Statamic REST API.

Enabling REST API in Statamic

There are two important things to remember when it comes to using the REST API in Statamic:

  • It is only available in the Pro version. The good news is that you can enable the Trial Mode to try it out.
  • It is disabled by default.

To enable the REST API, open the .env file and set STATAMIC_API_ENABLED to true.

Next, you have to pick which endpoints should be available. To do so, go to config/statamic/api.php and find the resources array.

In our case, we will only need the "collections" endpoint.

To learn more about the REST API, visit the Statamic website.

Modeling content

Our content will consist of two collections: blog and pages, and one taxonomy: tags.

Here's what the blueprints look like.

Pages

Pages blueprintPages blueprint

Blog

Blog blueprintBlog blueprint

Tags

Tag blueprintTag blueprint

The only thing left is to add some content and we're ready to start working with Astro.

This is how the Blog collection looks like:

Blog collectionBlog collection

Connecting Statamic and Astro

Before you start, make sure to check out our introduction to Astro guide, as it contains useful information about Astro's structure and routing.

In this example, we're going to use an Astro Blog theme.

Click here to fork the repository with a theme modified for the purpose of this guide.

.env file

First, we need to create an .env file in our Astro application. There you can store the Statamic API URL:

PUBLIC_API_URL='https://statamic-api.domain/api'

API helper

Next, it's time to add a small API helper. Create a src/api.js with:

const API_URL = import.meta.env.PUBLIC_API_URL;

export async function fetchAPI( query='' ) {
    const res = await fetch( `${API_URL}/${query}` );

    if ( res.ok ) {
        return res.json();
    } else {
        const error = await res.json();

        throw new Error(
            '❗ Failed to fetch API for ' + query + "\n" +
            'Code: ' + error.code + "\n" +
            'Message: ' + error.message + "\n"
        );
    }
}

This will make fetching the API more straightforward.

Blog archive

Now it's time to list all the blog posts. Go to src/pages/blog.astro – you should see this:

---
import BaseHead from '../components/BaseHead.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import {fetchAPI} from '../api';

import { SITE_TITLE, SITE_DESCRIPTION } from '../config';

const posts = await fetchAPI( 'collections/blog/entries?sort=-date' );
---

<!DOCTYPE html>
<html lang="en-us">
    <head>
        <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
        <style>
            ul {
                list-style-type: none;
                padding: unset;
            }
            ul li {
                display: flex;
            }
            ul li time {
                flex: 0 0 130px;
                font-style: italic;
                color: #888;
            }
            ul li a:visited {
                color: #8e32dc;
            }
        </style>
    </head>
    <body>
        <Header />
        <main>
            <content>
                <ul>
                    {posts.data.map((post) => (
                        <li>
                            <time datetime={post.date}>
                                {new Date(post.date).toLocaleDateString('en-us', {
                                    year: 'numeric',
                                    month: 'short',
                                    day: 'numeric',
                                })}
                            </time>
                            <a href={'blog/' + post.slug}>{post.title}</a>
                        </li>
                    ))}
                </ul>
            </content>
            <Footer />
        </main>
    </body>
</html>

The most important part is

const posts = await fetchAPI( 'collections/blog/entries?sort=-date' );

It grabs all posts from the API and assigns to the posts constant.

The rest is pretty straightforward: we're just printing variables on the screen using items like {post.slug}, {post.title}, etc.

It's also worth mentioning how to sort entries in the Statamic REST API. We can do this by using endpoint/?sort=date (ascending), or endpoint/?sort=-data (descending).

As the result, Astro will generate the following:

Generated sampleGenerated sample

Generating blog posts

Go to the src/pages/blog/[slug].astro file. In the file, you'll find something like this:

---
import BaseHead from "../../components/BaseHead.astro";
import Header from "../../components/Header.astro";
import Footer from "../../components/Footer.astro";
import {fetchAPI} from '../../api';

export async function getStaticPaths() {
  const posts = await fetchAPI( 'collections/blog/entries' );
  return posts.data.map((post) => {
    return {
      params: { slug: post.slug },
      props: { post } };
  });
}
const {post} = Astro.props;
---

<html>
    <head>
        <BaseHead title={post.title} description={post.description} />
        <style>
            .title {
                font-size: 2em;
                margin: 0.25em 0 0;
            }
            hr {
                  border-top: 1px solid #DDD;
                margin: 1rem 0;
            }
        </style>
    </head>

    <body>
        <Header />
        <main>
            <article>
                {post.featured_image && (
                    <img
                        width={720}
                        height={360}
                        src={post.featured_image.permalink}
                        alt=""
                    />
                )}
                <h1 class="title">{post.title}</h1>
                {post.date && <time>{new Date(post.date).toLocaleDateString('en-us', {
                    year: 'numeric',
                    month: 'short',
                    day: 'numeric',
                })}</time>}
                <hr/>
                <div set:html={post.content}></div>

                {post.tags && <div>Tags: { post.tags.map( (tag) => tag.title ).join(', ') }</div>}
            </article>
        </main>
        <Footer />
    </body>
</html>

Thanks to the getStaticPaths function, we'll be able to generate all the blog posts as separate pages. The variable post.slug will always be posted and based on this Astro will create all the required pages.

This is how the single blog post looks like:

Blog post previewBlog post preview

You can read more about it in the Routing section of the Astro guide.

Generating pages

The only thing left is to generate the static pages. The process is very similar to generating blog posts – you only need to go to src/pages/[slug].astro. The only difference between this and posts is the endpoint we are using: collections/pages/entries.

---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import {fetchAPI} from '../api';

export async function getStaticPaths() {
  const posts = await fetchAPI( 'collections/pages/entries' );
  return posts.data.map((post) => {
    return {
      params: { slug: post.slug },
      props: { post } };
  });
}
const {post} = Astro.props;
---

<html>
    <head>
        <BaseHead title={post.title} description={post.description} />
        <style>
            .title {
                font-size: 2em;
                margin: 0.25em 0 0;
            }
            hr {
                  border-top: 1px solid #DDD;
                margin: 1rem 0;
            }
        </style>
    </head>

    <body>
        <Header />
        <main>
            <article>
                <h1 class="title">{post.title}</h1>
                <div set:html={post.content}></div>
            </article>
        </main>
        <Footer />
    </body>
</html>

This result is this:

Generated pageGenerated page

Build and deploy

Deploying Astro using Buddy is very straightforward. Create a new pipeline and add a Node.js action with these commands:

npm install
npm run build
$$

Node.js action detailsNode.js action details

Make sure to set the version of Node to at least 14.15.0:

Selecting Node versionSelecting Node version

Also, because we are storing a PUBLIC_API_URL variable in our .env, we have to add this variable in the Variables tab.

The only thing left is to push our code to a server. With Buddy, you can deploy to a wide range of managed hosting providers or to private servers.

In the case of Astro sites, remember to push only the dist folder which contains the built and ready-to-publish website.

Deploy from Statamic

You can run deployments with Buddy directly from the Statamic control panel by using Itiden's Buddy Addon. Just install the addon, add a couple of variables, and you're ready to go:

Statamic Control Panel with Buddy AddonStatamic Control Panel with Buddy Addon

Summing up

Thanks to using the headless approach we can connect Statamic to one of many Jamstack frameworks. In our example we used Astro, but nothing is standing in your way to use Next.js, Gatsby, or Eleventy. And this is great, because we are not tied to any technology.

Maciek Palmowski

Maciek Palmowski

WordPress & PHP Developer

Maciek is a WordPress Ambassador working at Buddy. An active participant in the Open Source world, he spends most of his free time trying to find interesting news for WP Owls newsletter or cycling.

With Buddy even the most complicated CI/CD workflows take minutes to create

Sign up for Buddy CI/CD

Start a free trial

Trusted by customers from 170+ countries