tip minersTip Miners

WordPress Headless CMS with Remix JS for Free

Headless Worpress with Remix JS on CloudflareWritten by Miguel

Let’s start by answering one simple question, what is a headless CMS?

A headless CMS (Content Management System) is a content management system that allows content creators to manage and organize content. Still, unlike traditional CMS it does not include the presentation layer, in replace of this it provides a mechanism ( in many cases a REST API ) to access the content it manages.

Then, why do you need a headless CMS? The root answer to this question is flexibility, if the website project that you have in your hands requires high control of the front end(View, look and feel) yet with the capability of easy management of the content(without constant deploys ), a headless CMS is a candidate solution. In a headless CMS context, you implement the view layer of the website/app with a separate technology, it can be a SPA(single page application) with any modern framework like React JS or Vue JS or some full-stack frameworks like Remix JS, Next JS, Laravel ( for PHP )

WordPress as Headless CMS with Remix App as Frontend

The stack that we are going to use for implementing a website using a headless CMS solution is WordPress for the headless CMS, Remix JS as a full-stack framework, and Cloudflare as web hosting

WordPress is one of the most famous ( if not the most famous) CMS that exists and it comes with an out-of-the-box REST API to access the content.

Remix JS is one of the top competitors in the JS full-stack ecosystem, is easy to learn and it is specialized in serving dynamic content.

Cloudflare is a global cloud network that provides as a key feature a high performance across its services, it comes with a free tier for its famous Workers, which are, in a few words a medium to deploy web apps in its global network.

Lets start implementing our headless CMS with WordPress at no cost.

1. WordPress setup in a free hosting as Headless CMS

I won’t give you a list of the best free hosting providers for WordPress, instead of that, I’ll tell you the one I consider the best option for having WordPress as a Headless CMS, which is 000WebHost, this hosting provider is actually free, downsides?

  • Slow, it’s not blazing fast of course, but with we will cover this issue in this article
  • Availability of service, you might find times when the service is down or has high latency, as the point above we will cover a workaround for this issue.
  • Advertising, you will have ads for the pro version.
  • You can create only one website per account

To get started you need to create an account on 000WebHost if you don’t have one, and then :

  1. Use the option to create a new site, and select WordPress from the list, you don’t need to worry about templates or something else, since the front end will be created later, only you care about the content.
  2. Once you finish the wizard from 000WebHost you will land on WordPress Admin, on that stage you have the 70% of the job done, test what I say with the following URL:
    https://YOUR-WORPRESS.HOST.COM/wp-json/wp/v2/posts
    you should get a JSON with all the active articles available on your CMS
  3. This step involves a little bit of SEO, by itself the WordPress API will give us all we need to display the content of our pages, but to round it up you will need metadata about your page, like an SEO-friendly description, title, keywords, etc; for this, we will set up your WordPress to provide this extra pieces. Let’s enable custom fields on your WordPress articles, to do that you need to click on the three dots on the corner of the edit screen of any article, then click on the preferences option, with the preferences dialog open you have to choose Panels left subsection and then at the very bottom of the screen you have to enable Custom Fields Option.
    How to enable custom fields on wordpress
  4. Following the previous step, lets enable WordPress custom fields metadata to be exposed on the API, to do that you will have to select the appearance option on your WordPress Admin, then click on the suboption Theme File Editor, and from the theme files select functions.php file , with the functions file open add the following code at the very end of the file content :
register_rest_field( 'post', 'metadata', array(
    'get_callback' => function ( $data ) {
        return get_post_meta( $data['id'], '', '' );
    }, ));


Enable custom fields to be exposed on WordPress Rest API
With this code you are basically telling the WordPress API to add include the field metadata to all posts, to test this out go to the post JSON object of one your articles(note to get the id of a post just look at the URL your editing screen of any post and look for the number after post= ) :
https://YOUR-WORPRESS.HOST.COM/wp-json/wp/v2/posts/POSTID
In the content of that JSON you must find a "metadata" attribute.

2.- Remix JS app consuming your WordPress REST API

The section is a little bit longer and code heavier, then, please grab your cup of coffee ☕️ ( or tea, I don’t mind ) to get ready. Let me tell you what I won’t cover in this article: how to style your Remix JS, how to include images from a CDN in your posts and no deep technical explanation of Remix concepts(there are not a lot ).

With the previous statements in mind, lets begin:

Create a new Remix JS project selecting Cloudflare Workers as the deploy target, to do that you need to run the following command on your shell/terminal :
npx create-remix@latest
From the questions you will get ( to meet the purposes of this article) you have to answer :
TypeScript or JavaScript? TypeScript
Where do you want to deploy? Cloudflare Workers
The rest is your free choice 👌🏼

Next is about writing the functions that will consume/fetch WordPress REST API, as you can imagine there are already packages out there that can do this task, sincerely, after spending 30 mins going through the docs of some of them, I decided that for this simple task, is easier to consume directly the API with fetch calls, let’s put them under a folder /app/services/wp ( like most of the things in the JS world, this is totally up to you ) , create a file post-calls.ts, and let’s put the first API call :

export async function wpGetCategories():Promise<Category[]> {

    const categoriesResponse:any = await promisedGet(`${BASE_WP_URL}/wp-json/wp/v2/categories?hide_empty=true`);
    
    if(categoriesResponse.error){
        return [];
    }
    
    return categoriesResponse.map((category:wpCategory)=>({
        id:category.id,
        name:category.name,
        description:category.name,
        slug:category.slug,
        postCount:category.count,
    }));
} 

The code above will perform a GET to the endpoint that retrieves all the categories asking only for categories that have at least one active post.

The next piece of the puzzle is the code that gets the detail of the images that are linked to a post, inside of the WordPress API this under Media Objects :

export async function wpGetMedia(mediaId:number):Promise<Media> {
    const response:any = await promisedGet(`${BASE_WP_URL}/wp-json/wp/v2/media/${mediaId}`);
    if(response.error){
        return {
            url:"",
            alt:"",
            description:"",
        };

    }
    
    return {
        url:response.source_url,
        alt:response.alt_text,
        description: response.description
    }
}

Until now, everything is about making calls to WordPress API, and like this, your Remix App will work partially fine, why? , you have to keep in mind the downsides of WordPress free hosting, slowness, and medium availability, this will reflect in your Remix App in a negative way if no action is taken, Remix guys cover this performances issue in their very entertaining documentation, one of the solution concepts they provide is the plain ol’ caching, and that’s what we are going to do :

Since we are going to host our Remix App in the all-mighty Cloudflare Workers we can easily leverage from one of their fantastic features, Workers KV, that in a very simplistic way is a global key-value data store with low latency ( super fast ☺️), to make use of KV to cache(store) we can implement the following code :

const TTLDay = 60 * 60 * 24;

async function getCategories():Promise<Category[]>{

    const key = "categories";
    //first check if it exist on cache.
    const cachedEntry = await SITE_POSTS?.get(key);
    if(cachedEntry){
        
        return JSON.parse(cachedEntry);

    }else { // no cache , then make a call to get it 
        const categories = await wpGetCategories();
        await SITE_POSTS?.put(key,JSON.stringify(categories),{
            expirationTtl:TTLDay
        });
        return categories;
    }
} 

The code above is basically caching all categories’ responses from the function that we already implemented consuming WordPress API in the entry “categories” of the KV SITE_POSTS , but from where this SITE_POSTS variable came?

Your wrangler.toml config file is the answer to that question, this config file rules the way your Remix App behaves and deploys as a worker, to make available a KV in your app, you need to make a binding through a variable inside your wrangler config, the binding config that will make work the code above is :

kv_namespaces = [
  { binding = "SITE_POSTS", id = "a453453454564575765757a99395e" }
]

We will learn how to get the value of that id attribute in the last section of this article, for now, keep it on the back of your head.

Mixing the concept of caching and the calls to the WordPress REST API we are able to implement a function that retrieves all posts that belong to a cached category :

async function wpGetAllPosts(categories?:number[],pageSize?:number,pageNumber?:number):Promise<Post[]>{

    let qs:string = "?"
    if(categories){
        qs += `categories=${categories.join(',')}&`;
    }

    if(pageSize){
        qs+=`per_page=${pageSize}&`;
    }

    if(pageNumber){
        qs+=`page=${pageNumber}`;
    }

    const postsResponse:any = await promisedGet(`${BASE_WP_URL}/wp-json/wp/v2/posts${qs}`);
 
    if(postsResponse.error){
        return [];
    }

    const listCategories = await getCategories(); // this gets all the cached categories

    return Promise.all(postsResponse.map(async (rawPost) => ({
        id:rawPost.id,
        creationDate:rawPost.dtae,
        modifiedDate:rawPost.modified,
        slug:rawPost.slug,
        title:rawPost.title.rendered,
        content:rawPost.content.rendered,
        author:rawPost.author,
        categories:listCategories.filter((category)=>rawPost.categories.indexOf(category.id)>=0),
        thumb: await wpGetMedia(rawPost.featured_media) 
    })));
}

How would it look like the function that gets the latest posts of a category with the backup of KV caching?

async function getLatestPostsByCategory(categoryId:number):Promise<Post[]> {
    const postsKey = `posts-category-${categoryId}`;
    //first check if it exist on cache.
    const cachedPosts = await SITE_POSTS?.get(postsKey);
    if(cachedPosts){
        
        return JSON.parse(cachedPosts);

    }else { // no post , then make a call to get it 
        const posts = await wpGetAllPosts([categoryId],6,1);
        await SITE_POSTS?.put(postsKey,JSON.stringify(posts),{
            expirationTtl:TTLDay
        });
        return posts;
    }
}

So far the code we have written does not have specific code from Remix JS its time to have fun with this beautiful framework, lets say that we want to have a link on our site that shows all the categories and per each category, we should show the lastest 6 posts, to do that on Remix we will add articles.tsx file under our /app/routes/ folder with the following functions:

meta function will set up the title of our wonderful page, besides the title you can set up meta tags for the articles route


export const meta:MetaFunction = ()=>{

    return {
        title: "My Remix App with WordPress Headless CMS"
    }
}

loader function, in charge of providing the data for our view, for folks that are used to MVC frameworks, that would be our controller, note how simple the loader function look, like most of the controllers should do

export const loader = async () => {
    const categories = await getCategoriesListing();
    return json({categories});
}

default export function is basically our view, as you see the code returns JSX component. The pipe where you get the loader data is through the useLoaderData hook

export default function (){
    const {categories}:{categories:Category[]} = useLoaderData();

    return(
        <main className='flex flex-col'>
            {
                categories.map((category:Category)=>(
                    <section className='flex flex-wrap p-4' key={category.id}>
                        <h1 className="text-5xl text-lavender mb-4 w-full">{category.name}</h1>
                        {
                            category.posts?.map((post:Post)=>(

                                <div key={post.id} className='p-4 w-full sm:w-1/2  lg:w-1/3 '>
                                        <Link to={`/articles/${category.id}/${category.slug}/${post.id}/${post.slug || 'nicearticle'}`} className='border-0   w-full  flex flex-col'>
                                        <img className='w-full h-28 object-cover rounded rounded-b-none' 
                                            alt={post.thumb?.alt || ''}
                                            src={post.thumb?.url || 'https://www.stockvault.net/data/2016/03/08/186269/preview16.jpg'} />
                                        <h2 className='text-lg p-2  text-lavender whitespace-normal'>{post.title}</h2>
                                    </Link>
                                </div>
                                
                            ))
                        
                        }
                    </section>
                    
                ))
            }
            
        </main>
        
       
    )
}

In Remix, everything that is required to serve a page/URL on a site/app lives in the route file of that page, like this, you have a cohesive architecture that regardless of the fact that can produce big route files makes development so much easier than other frameworks.

Anatomy of a route file on Remix JS

3 Cloudflare Setup of your Remix App

For this step, you have to have a Cloudflare account, if you don’t have one, you can create one for free.

Login into your account and select Workers and Pages over the side menu, then pick the KV option from the submenu that comes out, with this you will get to the management console of KV Namespaces, click on the Create Namespace blue button on the top right corner:

How to create a KV namespace on cloudflare

  • The name does not matter for your code purposes, nonetheless is important for you to know where to go to fix any possible problem. Imagine this Namespace is a bucket where you are going to cache information from WordPress Content.
  • Remember the ID that I told you to keep on the back of your head? after the creation of the namespace, you can copy its ID and place it in your wrangler config

where to get the id of a cloudflare KV namespace

With the previous step done, we have everything ready to deploy our Remix App using WordPress as Headless CMS, someone extra you can do is set up your Remix App to use a custom domain.

  • Open your terminal/shell at the location of your remix app project
  • Execute the following command npm run deploy
  • Wrangler will open a browser window if you are not authenticated, if you are it will simply deploy your app to Cloudflare, and the terminal will tell you where you can go and enjoy your site

Thats it !! , you made it to the end, enjoy your lighting-fast blog at no cost!!

More articles of Web Development

Idiomatic Application

And .. What is an idiomatic application?

adding google analytics script  or GTM on your Remix JS app is a matter of adding it with JSX in a  dangerouslySetInnerHTML attribute

How to add google analytics to a Remix Run Web App

change remix app to be deployed to Cloudflare workers

Change deploy target to Cloudflare workers on Remix