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 blueprint
Blog
Blog blueprint
Tags
Tag 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 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 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 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 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 details
Make sure to set the version of Node to at least 14.15.0
:
Selecting 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 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
WordPress & PHP Developer
With Buddy even the most complicated CI/CD workflows take minutes to create
Sign up for Buddy CI/CD
Start a free trial