How to Add Page Building Functionality to a Headless WordPress Site Built with a Sveltekit Frontend

October 16, 2023

Featured

Background

Recently, we've had more clients express an interest in using their favorite CMS, WordPress, in a headless manner. If you don't know, this means that a developer would create the frontend, or all the design you see on a webpage, using their own code. They would then fill in the content by fetching it using an API from a WordPress instance.

There are a lot of advantages to this approach. They include:

  1. Speed of development
  2. Frontend Flexibility
  3. Speed of the site itself
  4. Advanced styling features

Truth be told, developers usually prefer this approach too. We all have our favorite frontend framework. Here at Tattoo, we are partial to Svelte and Sveltekit as our library and framework of choice. We've built some beautiful UI, and are getting more efficient and faster with this stack all the time. However, the headless approach has presented an issue.

The Problem

A few clients have recently asked what they do if they want to add their own custom pages. Well, that's an interesting question in the context of headless. Pages AREN'T created in WordPress with the headless approach. Content is. If you want a page builder, you usually have to stay in WordPress's sandbox, which means no custom frontend interface in your framework of choice. Right?

The Solution

Enter the WPGraphQL Gutenberg plugin. Once installed, this plugin allows you to fetch various details about the blocks within a page, included the saved HTML of that block. That, paired with some Sveltekit magic, allows us to give the end user the ability to create whatever custom content they want on any page! Let's see how:

First, ensure you have the following plugins install in WordPress:

  • WPGraphQL
  • WPGraphQL Gutenberg

You can get the first from the plugin library in WP, but the second will need to uploaded via zip file. Check the link above to get that.

Next, let's create a very basic page using some simple blocks. I came up with this:

image1.png

Technically the sky is the limit here, but not right away. For this page, try to stick to basic text and an image.

Next, let's create a new Sveltekit skeleton project and create the following in our routes directory:

  • +page.svelte (probably already exists, just delete the contents if you're starting fresh)
  • +page.js (or page.server.js, it's up to you)
  • +layout.svelte (if you want to add the same styling I do)
  • A [slug] directory

First, let's take a look at the code for +page.svelte:

<script>
    export let data;
</script>

<div>
    <h1>Testing out Gutenberg Blocks with Headless!</h1>
    
    <p>Dynamically Creating Svelte Pages from Gutenberg Blocks!</p>

    <nav>
        {#each data.links as link}
            <a href={link.uri}>{link.title}</a>
        {/each}
    </nav>
</div>

<style>
    div {
        width: 100vw;
        height: 100vh;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
    }
    a {
        color: rgb(231, 80, 231);
        text-decoration: none;
        font-size: 20px;
        margin: 0px 30px;
    }
    a:hover {
        color: black;
    }
    nav {
        margin-top: 30px;
    }
</style>

This code includes some default styles in it. It also won't work. We have an each loop in the markup that is looking for a list of page links from WP to make a little nav. Let's update +page.js to load that data and get this working:

import { GraphQLClient, gql } from 'graphql-request'
    
const client = new GraphQLClient("http://lab.local/index.php?graphql")
    
export async function load() {
    
    const dataQuery = gql`
    {
        pages {
          nodes {
            title
            uri
          }
        }
      }
    `

    let queryResponse = await client.request(dataQuery).then((data) => {
        return data
    })

    return {
        "links": queryResponse.pages.nodes
    }

}

Here, we use graphql-request to create a graphql call to our WP instance (mine is lab.local, yours will be different) that fetches all the page titles and URIs. Then, back in +page.svelte, we loop through this data and create a list of links to our pages. This isn't necessary for the end goal, but it helps to show how you can dynamically generate navigation to custom pages. Before we get into the block magic, let's just add a little more styling to make our page bearable. I added a link to the roboto google font, but styling is obviously your choice. I put this in +layout.svelte:

<slot></slot>

<style>
    :global(body){
        margin: 0;
        font-family: 'Roboto', sans-serif;
        background-color: rgb(54, 54, 54);
        color: #fff;
        font-size: 20px;
    }
    :global(h1){
        font-weight: 700;
        font-size: 60px;
    }
</style>

Alright! Enough of this quality of life stuff, let's get into the meat of it. Rendering blocks on our frontend.

First, know that after you activate the WPGraphQL Gutenberg plugin, you already have block content in your GraphQL schema. Let's enter the [slug] directory and create a couple files that will let us access it. Create the following in the [slug] directory:

  • +page.js (or +page.server.js)
  • +page.svelte

Let's start with +page.js and grab our block content. Enter the following code:

import { GraphQLClient, gql } from 'graphql-request'
import { error } from '@sveltejs/kit';
    
const client = new GraphQLClient("http://lab.local/index.php?graphql")
    
export async function load({ params }) {
    
    const dataQuery = gql`
    query getBlocksBySlug($slug: ID!){
        page(id: $slug, idType: URI) {
            slug
            title
            blocks {
                saveContent
            }
            }
        }
    `
    let variables = {
        "slug": params.slug
        }

    let queryResponse = await client.request(dataQuery, variables).then((data) => {
        return data
    })

    if (queryResponse.page == null){
        throw error(404, {
            message: 'Not found'
        });
    }

    return {
        "blockData": queryResponse.page.blocks
    }

}

What is this code doing? Let's go over it quickly. First, we create another graphql client with our endpoint, and then create a query called getBlocksBySlug. The variable for this query is the page slug from the params object that we pass into the load function.

Next, we use the query to get data on our blocks. Before we return the data, we have to check whether or not it's null. If it's null, there is no page that matches the slug, and you should throw a 404. If it isn't null, we return it to our frontend in the data object. Let's swap over to our +page.svelte file to see how we render it:

<script>
    export let data;
</script>

<div class="content">
{#each data.blockData as block}
{@html block.saveContent}
{/each}
</div>

<style>
    .content {
        width: 100vw;
        height: 100vh;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
    }
</style>

Again, I included some styles to center content, but everything else is right from WordPress. This code loops through each block in the data object and uses the @html svelte directive to render it out as HTML. Let's take a look at how this displays on our site:

Image2.png

Voila! It works. There is our block content. We can style it the same way we'd style anything, or you can leave default styles.

Caveats

There is one caveat to this approach. The way I do it here, it works best with text and image content. However, WordPress has tens of blocks for content and media with more advanced styling. This content doesn't always render correctly on our end, because we're missing block styles from WordPress. I'm working on sourcing a master stylesheet with all those styles to load in so that all blocks work like they should. I will update this article once I manage to do that. For now, happy hacking!