Headless WordPress with Remix and Tailwind CSS

12 minute read
Jeff Everhart
Jeff Everhart
Sr. Developer Advocate

One of the benefits of headless WordPress architecture is that it allows us total flexibility in how we want to develop our frontend website. Given this flexibility, it allows headless developers to experiment with new and emerging technologies easily. In this post, we’ll take a look at a newer framework in the JavaScript world and build a simple headless WordPress site using Remix. By the end of this post, you should be able to do the following things:

  • Create a new Remix project from scratch, or customize this starter, and hook it up to WordPress using Apollo Client
  • Implement loader functions in Remix to query data
  • Create dynamic routes in Remix to display individual posts by post uri
  • Implement different Link prefetching strategies based on your use-case
  • Understand the tradeoffs of using Tailwind CSS to style block editor content

Getting Started with the Remix WordPress Starter

To get us started, I’ve created a pretty basic headless WordPress site using the latest version of Remix, all of the needed dependencies, and some styling with Tailwind CSS. If you run the following commands in your terminal, you should end up with a fully running site in your browser that is pulling data from a remote WordPress site.

git clone https://github.com/JEverhart383/remixing-wordpress.git
cd remixing-wordpress
npm install
npm run dev

The rest of this article will walk through some of the different parts of this codebase to explain how this Remix starter is created and discuss specific aspects of Remix that developers coming from other frameworks may want to be aware of.

As prerequisites, you should be familiar with React, WPGraphQL, and ideally one meta-framework like Next.js, Nuxt, Svelete, or Gatsby. For refreshers on those foundational topics, you can reference our Headless WordPress Developer Roadmap.

Loading Data in the Index Page

Like other full stack frameworks, Remix uses a page-based routing system, which means that we can access the code that renders our index route in the /app/routes/index.jsx file. This file exports a default function named Index that returns a React component that represents this route:

import Post from "../components/Post"
import Header from "../components/Header"
import { useLoaderData } from "@remix-run/react"
import { gql } from "@apollo/client"
import { client }  from "../lib/apollo"

export async function loader (){
  const PostsQuery = gql`
        query GetPosts {
            posts {
                nodes {
                    title
                    content
                    date
                    slug
                }
            }
        }
    `
    const response = await client.query({
        query: PostsQuery
    })

    const posts = response?.data?.posts?.nodes
    return posts
}


export default function Index() {
  const posts = useLoaderData();
  return (
    <div>
      <Header title="Home Page" ></Header>
        <div className="grid gap-8 grid-cols-1 lg:grid-cols-3 p-6">
          {posts.map(post => {
              return (
                <Post post={post} key={post.title}></Post>
              )
            })}
        </div>
    </div>
  );
}

In this case our Index function creates a grid of our site’s posts by mapping over them and creating individual Post components. In addition to exporting the route component as a default, we also export an async function named loader, which is Remix’s convention for loading data into components.

When this route is requested by the browser, the Remix framework runs the code inside of the loader function on the server, and then the component can access that data inside of the component using the useLoaderData hook. In our example above, we import a pre-configured instance of ApolloClient and execute a GraphQL query, returning that data inside the loader function.

To change the example GraphQL origin to your own WordPress site. You can change the url property in the /app/lib/apollo.js file:

import { ApolloClient, InMemoryCache} from "@apollo/client";

export const client = new ApolloClient({
    uri: 'Swap your URL here',
    cache: new InMemoryCache()
})

How Does Remix Standout on Data Loading

When looking at something like Next.js in comparison, there are a number of different ways to load data into page components for static or server-rendered pages, like getStaticProps, but also methods for having components request their own data from your GraphQL origin on the client using Apollo’s useQuery hook for example.

While there are a lot of different rendering methods available to modern developers, splitting the responsibility of data fetching across multiple different domains (the client and the server) adds to the API surface area that developers contend with as they develop applications and can lead to additional challenges.

In particular, the Remix team talks about credential management being improved by loading all data on the server. Since the client never makes requests to sensitive origins on its own, we don’t need to worry about exposing API keys or proxying request manually since Remix already acts as our proxy. Personally, I’m a huge fan of this selling point as someone who’d developed a lot of SPAs.

In addition, client-side fetching can sometimes fall victim to a waterfall of network requests, where a parent might fetch its own data, but then pass data into a child component that triggers its own data requests based on the data passed in by the parent component.

Because there is a centralized mechanism for loading data into components in Remix using loader functions, it is easier to coordinate and parallelize some of those network requests to avoid this waterfall loading pattern.

As a developer, I tend to appreciate opinionated software more than software that provides a lot of different approaches. Having the opportunity to evaluate a lot of different JavaScript frameworks as a part of my job, I can personally understand the issues Remix is trying to solve here. But it’s also a divergence from a common practice in the JavaScript world and thus not for everyone or every situation.

Prefetching with Link Components in Remix

Now that we have a good understanding of how data loading works in Remix, let’s take a look at how the framework implements prefetching with Link components. Link prefetching isn’t unique to Remix, as its a part of the browser platform and also used by Next.js, but in some ways it works in tandem to help eliminate any negative impacts of loading data on the server.

Let’s take a look at the Post component in /app/components/Post.jsx to examine this in more detail:

import { Link } from "@remix-run/react"

export default function Post({post}){
    return (
        <Link prefetch="intent" to={`/posts/${post.slug}`}>
            <div className="flex 
            items-center 
            bg-gradient-to-r
            from-cyan-500 
            to-blue-500 
            p-8 
            rounded-lg 
            text-white 
            transition-all 
            hover:-translate-y-1 
            hover:scale-105"
            >
                <div>
                    <h2 className="font-semibold text-2xl">{post.title}</h2>
                    <p>{ new Date(post.date).toLocaleDateString() }</p>
                </div>
            </div>
        </Link>

    )
}

In this code snippet, we are returning the JSX element that represents our post card inside of the post grid on our index page. This card is wrapped in a Link component provided by the Remix framework, and here we provide it both a dynamic navigation path via the to prop as well as a prefetch="intent" attribute. It is this feature of the Link component in Remix that allows us to enable prefetching on user intent, which occurs when a user interacts with a Link component via hover or focus, or on render, which prefetches all available routes on the page.

As you can see in the example GIF below, as we hover over the Link components, Remix loads all of the resources (JS, CSS, and data) associated with each route so that the routes are available immediately when the user clicks on them.

In Remix link prefetching on intent will load route resources when a user hovers or focuses the link

While Remix isn’t the first or only framework to implement prefetching, it pairs very nicely with the server-loaded data in my opinion. Given that Remix can load my application’s data in the background, I’m less inclined to stress about the amount of network requests it takes to get that data since it’s still immediately available for my users.

When examining Link components using the prefetch="render"attribute, we see that there is the potential of much more network activity using this method. As soon as those components have rendered, they will request additional data from the server, which can be great if you want to prime certain high priority routes.

In Remix link prefetching on render will load route resources a link element is rendered

When compared to the prefetching available with Next.js, Remix doesn’t necessarily bring anything controversial to the table but does adopt a a pattern of defaults for prefetching that is different from Next.js. Frankly, I’m glad to see more frameworks give this technique consideration, as it can drastically improve the user experience during site navigation.

Dynamic Routing in Remix

Now that we’ve looked at how to render our Index page and use Link components to prefetch the data for our posts, let’s look at how Remix’s router makes that possible using dynamic routes.

To construct dynamic page-based routes in Remix, we can add additional folders and files to our routes folder. In this case, we create a file called $slug.jsx inside of the /routes/posts directory, which creates a dynamic route segment that responds to browser requests for URLs like /posts/[post-slug-here]. When we do this, Remix makes $slug available as a variable inside of our loader function so we can use it to request data. We can see that played out in our page component for that route:

import Header from "../../components/Header"
import { useLoaderData } from "@remix-run/react";
import { gql } from "@apollo/client"
import { client }  from "../../lib/apollo"

export async function loader ({params}) {
  const slug = params.slug
  const PostQuery = gql`
    query GetPostBySlug($id: ID!) {
        post(id: $id, idType: SLUG) {
            date
            content
            title
            slug
        }
    }
  `

  const response = await client.query({
    query: PostQuery,
    variables: {
        id: slug
    }
  })

  const post = response?.data?.post
  return post
}

export default function Index() {
  const post = useLoaderData()
  return (
    <div>
      <Header title="Home Page" ></Header>
        <main className="bg-gray-100 container mx-auto mt-6 p-6 rounded-lg">
            <h1 className="text-4xl">{post.title}</h1>
            <div className="text-2xl mt-4">{new Date(post.date).toLocaleDateString()}</div>
            <article className="mt-4 space-y-2" dangerouslySetInnerHTML={{__html: post.content}}></article>
        </main>
    </div>
  );
}

The loader function of our route components gives us access to any route params passed in by the user, but it can also access additional details actual HTTP request as well:

export async function loader ({params, request}) {
  const slug = params.slug
  const url = new URL(request.url);
  const search = url.searchParams.get("search");
// Do stuff with slug and search here
}

In our example above, we then use the value of params.slug to populate a GraphQL query for our specific WordPress post:

export async function loader ({params}) {
  const slug = params.slug
  const PostQuery = gql`
    query GetPostBySlug($id: ID!) {
        post(id: $id, idType: SLUG) {
            date
            content
            title
            slug
        }
    }
  `

  const response = await client.query({
    query: PostQuery,
    variables: {
        id: slug
    }
  })

  const post = response?.data?.post
  return post
}

It’s also worth noting at this point, that the data fetching code that runs inside of the loader function can be very flexible for the developer. It’s up to you in how to create and organize your data layer. In previous versions of this tutorial, I extracted all of the logic you see above into a single function in a separate file, so my loader looked like this with one additional import statement:

import { getPostBySlug } from ../../lib/WordPressService

export async function loader ({params}) {
  const slug = params.slug
  return await getPostBySlug(slug)
}

This gives developers a ton of flexibility in determining the organizational practices for their application. If you’re a fan of colocating data fetching code in your route modules, you can do that, but if you prefer a different pattern Remix gives you the flexibility to do that as well.

Supporting Tailwind CSS

A common question that comes up on our Headless WordPress Discord server is how to use Tailwind CSS and X framework to develop WordPress sites. For this article, I wanted to look at that specifically to evaluate how Remix works with Tailwind CSS, but also so that I could explore a CSS framework I have little experience with.

Luckily, both the Remix docs and the Tailwind CSS docs have guides on configuring the tools to work together, and I was able to get Tailwind support running very quickly.

Since this article isn’t about Tailwind CSS per se, I’m going to focus on the important aspects of this framework for headless WordPress developers specifically. Tailwind CSS is a utility-first framework, which means that you essentially add utility classes to your HTML elements to style them. Our Post component is an example of this:


<Link prefetch="render" to={`/posts/${post.slug}`}>
            <div className="flex 
            items-center 
            bg-gradient-to-r
            from-cyan-500 
            to-blue-500 
            p-8 
            rounded-lg 
            text-white 
            transition-all 
            hover:-translate-y-1 
            hover:scale-105"
            >
                <div>
                    <h2 className="font-semibold text-2xl">{post.title}</h2>
                    <p>{ new Date(post.date).toLocaleDateString() }</p>
                </div>
            </div>
        </Link>

Using a series of CSS classes, we describe the display and styling of this element, but this involves adding classes to the actual HTML. While this pattern is really nice for creating components, it was a bit more challenging to think through how to style some of the block editor content that WordPress returns as a CMS.

If we look at the route component for our $slug page, the article component is using React’s dangerouslySetInnerHTML attribute to output all of the rendered markup that makes up the post content:

export default function Slug() {
  const post = useLoaderData()
  return (
    <div>
      <Header title="Home Page" ></Header>
        <main className="bg-gray-100 container mx-auto mt-6 p-6 rounded-lg">
            <h1 className="text-4xl">{post.title}</h1>
            <div className="text-2xl mt-4">{new Date(post.date).toLocaleDateString()}</div>
            <article className="mt-4 space-y-2" dangerouslySetInnerHTML={{__html: post.content}}></article>
        </main>
    </div>
  );
}

Because our WordPress editor content is essentially echoed out inside of the article element, we don’t have the opportunity to add Tailwind utility classes to that markup before or as it is rendered without doing some extra work.

In other looks at styling block editor markup, we talk about importing WordPress npm packages to use native CSS, or even writing our own classes to match the markup. But both of those seemed at odds with the Tailwind philosophy of utility-first CSS.

Tailwind would theoretically pair nicely if you are willing to either parse blocks from the HTML response, or use the WPGraphQL Gutenberg extension to create more formal components. This pattern would be very powerful, but does require another level of commitment.

Tailwind CSS also has its own method of extracting classes using the @apply directive, which may give developers interested in exploring Tailwind with headless WordPress another avenue of targeting block editor markup. If I were to do a deeper project with Tailwind again, this is likely the avenue I would take. In theory, it would let us do something like this to style an image’s figure container:

@layer components {
  .wp-block-image {
     @apply //our tailwind utility classes here;
  }
}

For the purposes of this demo, I was able to make some basic styling changes with Tailwind just by relying on the targeting the parent article element, but this exercise may include some trial and error:

<article className="mt-4 space-y-2" dangerouslySetInnerHTML={{__html: post.content}}></article>

Wrapping Up! ๐ŸŽ‰

Thanks for exploring Remix, Tailwind CSS, and headless WordPress with me in this article. If you want an example of you can access the finished state of our example app on this GitHub branch.

Looking for a place to host your headless WordPress project? Check out WP Engineโ€™s Atlas platform.