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.
Modeling content
Our content will consist of two collections: blog
and pages
, and one taxonomy: tags
.
Here's what the blueprints look like.
Pages
Image loading...Pages blueprint
Blog
Image loading...Blog blueprint
Tags
Image loading...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:
Image loading...Blog collection
Connecting Statamic and Astro
In this example, we're going to use an Astro Blog theme.
.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:
Image loading...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:
Image loading...Blog post preview
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:
Image loading...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
$$
Image loading...Node.js action details
Make sure to set the version of Node to at least 14.15.0
:
Image loading...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.
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:
Image loading...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
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.