adriantirusli

WordPress (Headless) + Gatsby = ❤️

Kata “head” dari “headless” mengacu kepada front-end yang berarti headless adalah content management system (CMS) yang hanya digunakan sebagai back-end saja. Tujuannya adalah untuk menyajikan konten dan membuatnya dapat diakses melalui API (seperti REST atau GraphQL).

Platform headless tidak memiliki front-end default untuk menentukan bagaimana konten akan disajikan kepada pengguna. Artinya konten yang dihasilkan sifatnya mentah dan dapat dipublikasikan di mana saja, melalui kerangka kerja apa saja.

Akhirnya, pengembang front-end memiliki kebebasan untuk membangun “head” sebanyak yang diinginkan, tanpa memperdulikan perangkat mana yang ingin ditargetkan karena pengambilan konten untuk setiap perangkat, Headless CMS akan merespon panggilan API.

WordPress sebagai Headless CMS

Data IONOS mengenai CMS terpopuler saat ini

Gambar diatas menunjukkan bahwa saat ini CMS yang paling populer di jagat internet adalah siapa lagi kalau bukan yakni WordPress. Disamping fakta bahwa WordPress sangat populer, penggunaannya sebagai headless CMS juga menyiratkan bahwa CMS dapat bekerja dengan baik pada beragam kombinasi perangkat keras dan perangkat lunak dan juga dalam pemeliharaan rutin dan pembaruan keamanan.

Tanpa basa-basi lagi, langsung saja masuk ke implementasi. Kali ini aku membuat sebuah situs blog sederhana yang akan menggunakan Gatsby sebagai front-end karena kenapa tidak? hahaha becanda deng. Gatsby melakukan pre-render seluruh situs pada waktu kompilasi dan karenanya, pengguna mendapatkan situs statis yang sepenuhnya siap berdasarkan permintaan pertama. Hal ini yang menjadikan Gatsby merupakan salah satu pendekatan terbaik dari sisi performa.

Install WordPress dan Plugin-pluginnya

Dimulai dari install WordPress terlebih dahulu, terserah baik lokal maupun online. Kali ini aku akan mencoba menggunakan Local dari FlyWheel.

Plugin

  • WPGraphQL – Digunakan untuk membuat endpoint dari GraphQL
  • WPGraphiQL – Membantuk untuk query

Install kedua plugin ini kedalam wp-admin, baik dengan mengunduh langsung dari repositori atau dengan menggunakan perintah git clone:

git clone https://github.com/wp-graphql/wp-graphql
git clone https://github.com/wp-graphql/wp-graphiql

Pastikan kedua plugin ini sudah di aktivasi.

Install Gatsby dan Plugin-pluginnya

Dimulai dari membuat projek baru, disini aku menggunakan Gatsby CLI untuk membuat projek baru. Gatsby juga menyediakan berbagai macam starter yang bisa dipilih sesuai kebutuhan. Dalam projek ini aku akan menggunakan gatsby-starter-blog.

gatsby new gatsby-starter-blog https://github.com/gatsbyjs/gatsby-starter-blog

Plugin

Setelah membuat projek baru dilanjutkan dengan intall beberapa plugin, diantaranya:

  • dotenv – Menyimpan variabel berdasarkan environment.
  • gatsby-source-graphql – Menghubungkan API GraphQL dengan GraphQL yang dimiliki Gatsby
yarn add dotenv gatsby-source-graphql

Konfigurasi

Kita akan menggunakan dotenv dengan dua file yang berbeda untuk variabel environment. Buatlah file .env.development dan .env.production di direktori root dari projek Gatsby yang sudah dibuat tadi.

Tambahkan .env.development pada file .gitignore

# .gitignore
.env.development

Selanjutnya file .env.development dan .env.production

# .env.development
WORDPRESS_ENDPOINT = http://headlesstest.local
# .env.production
WORDPRESS_ENDPOINT = http://d782704b.ngrok.io

Sesuaikan masing-masing URL dengan URL yang digunakan baik itu lokal maupun online. Local dari FlyWheel memungkinkan untuk menghasilkan live link yang menggunakan link ngrok.io, URL ini dapat digunakan di lingkungan proction.

Kemudian edit file gatsby-config.js dan letakkan baris kode berikut di paling atas sebelum module.exports, yang berguna untuk memilih file .env yang harus digunakan:

let activeEnv =
  process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV || "development"

console.log(`Using environment config: '${activeEnv}'`)

require("dotenv").config({
  path: `.env.${activeEnv}`,
})

console.log(`WordPress Endpoint: '${process.env.WORDPRESS_ENDPOINT}'`)

Selanjutnya tambahkan gatsby-source-graphql ini pada module.exports di dalam plugins:[…]:

{
   resolve: "gatsby-source-graphql",
      options: {
        typeName: "WPGraphQL",
        fieldName: "wpgraphql",
        url: `${process.env.WORDPRESS_URL}/graphql`,
   },
},

Coba run dengan gatsby develop dan navigasikan ke http://localhost:8000/___graphql untuk mengecek apakah plugin gatsby-source-graphql sudah berjalan.

Page dan Post

Pertama kita akan membuat templat sederhana yang bisa membaca data pages dan post dari WordPress. Disini aku tidak memerlukan beberapa file, jadi bisa dihapus terlebih dahulu beberapa file berikut:

  • components/bio.js
  • pages/index.js
  • templates/blog.js

Kemudian buat file baru yakni page.js

// src/templates/page/page.js

import React  from "react"

import Layout from "../../components/layout"
import SEO from "../../components/seo"


const Page = ({ pageContext }) => {

  const page = pageContext.page

  return (
    <Layout>
      <SEO title={page.title} />

      <h1>{page.title}</h1>
      <div dangerouslySetInnerHTML={{__html: page.content}} />

    </Layout>
  )
}

export default Page

Dilanjutkan dengan post.js

// src/templates/post/post.js

import React  from "react"

import Layout from "../../components/layout"
import SEO from "../../components/seo"


const Post = ({ pageContext }) => {

  const post = pageContext.post

  return (
    <Layout>
      <SEO title={post.title} />

      <h1> {post.title} </h1>
      <div dangerouslySetInnerHTML={{__html: post.content}} />

    </Layout>
  )
}

export default Post

Kemudian edit file gatsby-node.js menjadi seperti kode dibawah ini untuk membuat pages dan posts dinamis dari file createPages.js dan createPosts.js.

// gatsby-node.js

const createPages = require("./create/createPages")
const createPosts = require("./create/createPosts")

exports.createPagesStatefully = async ({ graphql, actions, reporter }, options) => {
  await createPages({ actions, graphql, reporter }, options)
  await createPosts({ actions, graphql, reporter }, options)
}

Create Page dan Create Post

Pertama di buatlah direktori create pada root

// create/createPages.js

const pageTemplate = require.resolve("../src/templates/page/page.js")

const GET_PAGES = `
  query GET_PAGES($first:Int $after:String) {
    wpgraphql {
      pages(
        first: $first
        after: $after
        # This will make sure to only get the parent nodes and no children
        where: {
            parent: null
        }
      ) {
        pageInfo {
            hasNextPage
            endCursor
        }
        nodes {                
            id
            title
            pageId
            content
            uri
            isFrontPage
        }
      }
    }
  }
`

const allPages = []
let pageNumber = 0
const itemsPerPage = 10

module.exports = async ({ actions, graphql, reporter }, options) => {
  const { createPage } = actions
  const fetchPages = async (variables) =>
    await graphql(GET_PAGES, variables).then(({ data }) => {
      const {
        wpgraphql: {
          pages: {
            nodes,
            pageInfo: { hasNextPage, endCursor },
          },
        },
      } = data

      nodes
      && nodes.map((pages) => {
        allPages.push(pages)
      })

      if (hasNextPage) {
        pageNumber++
        reporter.info(`fetch page ${pageNumber} of pages...`)
        return fetchPages({ first: itemsPerPage, after: endCursor })
      }

      return allPages
    })

  await fetchPages({ first: itemsPerPage, after: null }).then((wpPages) => {

    wpPages && wpPages.map((page) => {
      let pagePath = `/${page.uri}`

      if(page.isFrontPage) {
        pagePath = '/'
      }

      createPage({
        path: pagePath,
        component: pageTemplate,
        context: {
          page: page,
        },
      })

      reporter.info(`page created: ${page.uri}`)
    })

    reporter.info(`# -----> PAGES TOTAL: ${wpPages.length}`)
  })
}

Kode seabrek ini buat apa?

Pertama-tama kita mendefinisikan fungsi fetchPages(), yang secara rekursif mengambil halaman (10 sekaligus) sampai tidak ada lagi yang perlu diambil. Kemudian ditambahkan ke dalam array allPages

Lalu, dipetakan melalui wpPages dan memanggil createPage().

Terdapat pengecekan yang dilakukan untuk halaman depan dengan menggunakan page.isFrontPage, hal ini dilakukan karena untuk homepage yang diinginkan untuk menjadi root path adalah / bukanlah /home.

Pada fungsi createPage() kita set path sesuai dengan uri yang kemudian menjadi slug untuk masing-masing halaman. Kemudian component akan membaca file pada variabel pageTemplate untuk isi dari halaman kemudian data halaman akan diteruskan ke context.

Create Post

// create/createPosts.js
  
const postTemplate = require.resolve("../src/templates/post/post.js")

const GET_PAGES = `
    query GET_POSTS($first:Int $after:String) {
        wpgraphql {
            posts(
                first: $first
                after: $after
                # This will make sure to only get the parent nodes and no children
                where: {
                    parent: null
                }
            ) {
                pageInfo {
                    hasNextPage
                    endCursor
                }
                nodes {                
                    id
                    title
                    postId
                    content
                    uri
                }
            }
        }
    }
`

const allPosts = []
let postNumber = 0
const itemsPerPost = 10

module.exports = async ({ actions, graphql, reporter }, options) => {
  const { createPage } = actions
  const fetchPosts = async (variables) =>
    await graphql(GET_PAGES, variables).then(({ data }) => {
      const {
        wpgraphql: {
          posts: {
            nodes,
            pageInfo: { hasNextPage, endCursor },
          },
        },
      } = data

      nodes
      && nodes.map((posts) => {
        allPosts.push(posts)
      })

      if (hasNextPage) {
        postNumber++
        reporter.info(`fetch post ${postNumber} of posts...`)
        return fetchPosts({ first: itemsPerPost, after: endCursor })
      }

      return allPosts
    })

  await fetchPosts({ first: itemsPerPost, after: null }).then((wpPosts) => {

    wpPosts && wpPosts.map((post) => {
      const path = `blog/${post.uri}/`

      createPage({
        path: path,
        component: postTemplate,
        context: {
          post: post,
        },
      })

      reporter.info(`post created:  ${post.uri}`)
    })

    reporter.info(`# -----> POSTS TOTAL: ${wpPosts.length}`)
  })
}

Pada dasarnya createPost.js ini sama dengan createPages.js cuma di prefixnya saja yang menggunakan /blog.

Ketika run menggunakan gatsby develop halaman sudah bisa terlihat, untuk mengecek halaman apa saja yang sudah ada dapat dilakukan dengan mengetikkan slug sembarang seperti http://localhost:8000/sdsakdnk.

Blog Post

Sekarang saatnya fokus ke blog post.

Tambahkan Blog Post di WordPress

Mulailah dengan membuat dummy post di WordPress dan buatlah sekitar 10 post agar bisa sekalian mengecek apakah pagination yang nanti akan kita implementasikan berjalan atau tidak.

Blog

Waktunya untuk membuat halaman Blog untuk menampilkan semua blog post yang dimiliki.

Buat file globals.js

// globals.js
const Globals = {
  blogURI: 'blog'
}

module.exports = Globals

File globals.js ini diletakkan pada root dari projek.

Buat file Blog.js

// src/templates/post/blog.js

import React from "react"
import Layout from "../../components/Layout"
import PostEntry from "../../components/PostEntry"
import Pagination from "../../components/Pagination"
import SEO from "../../components/SEO"

const Blog = ({ pageContext }) => {
  const { nodes, pageNumber, hasNextPage, itemsPerPage, allPosts } = pageContext

  return (
    <Layout>
      <SEO
        title="Blog"
        description="Blog posts"
        keywords={[`blog`]}
      />

      {nodes && nodes.map(post => <PostEntry key={post.postId} post={post}/>)}

      <Pagination
        pageNumber={pageNumber}
        hasNextPage={hasNextPage}
        allPosts={allPosts}
        itemsPerPage={itemsPerPage}
      />
    </Layout>
  )
}

export default Blog

Seperti yang bisa dilihat, terdapat beberapa komponen yang belum tersedia. Sekarang tambahkan komponen-komponen tersebut.

Buat file Image.js

// src/components/Image.js

import React from "react"
import { useStaticQuery, graphql } from "gatsby"

const Image = ({ image, withFallback = false, ...props }) => {
  const data = useStaticQuery(graphql`
      query {
          fallBackImage: file(relativePath: { eq: "fallback.svg" }) {
              publicURL
          }
      }
  `)

  /**
   * Return fallback Image, if no Image is given.
   */
  if (!image) {
    return withFallback ? <img src={data.fallBackImage.publicURL} alt={"Fallback"} {...props}/> : null
  }

  return <img src={image.sourceUrl} alt={image.altText} {...props}/>
}

export default Image

withFallback akan memberikan opsi untuk menampilkan fallback image jika tidak ada gambar yang ditambahkan. Jika withFallback salah, seperti di default, maka itu tidak elemen DOM tidak akan dibuat.

Buat file PostEntry.js

// src/components/PostEntry.js

import React from "react"
import { Link } from "gatsby"
import Image from "./Image"
import { blogURI } from "../../globals"

const PostEntry = ({ post }) => {

  const { uri, title, featuredImage, excerpt } = post

  return (
    <div style={{ marginBottom: "30px" }}>
      <header>
        <Link to={`${blogURI}/${uri}/`}>
          <h2 style={{ marginBottom: "5px" }}>{title}</h2>
          <Image image={featuredImage} style={{ margin: 0 }}/>
        </Link>

      </header>

      <div dangerouslySetInnerHTML={{ __html: excerpt }}/>
    </div>
  )
}

export default PostEntry

Buat file Pagination.js

// src/components/Pagination.js

import React from "react"
import { Link } from "gatsby"
import { blogURI } from "../../globals"

const Pagination = ({ pageNumber, hasNextPage }) => {

  if (pageNumber === 1 && !hasNextPage) return null

  return (
    <div style={{ margin: "60px auto", textAlign: "center" }}>
      <h2>Posts navigation</h2>
      <div>
        {
          pageNumber > 1 && (
            <Link
              className="prev page-numbers"
              style={{
                padding: "4px 8px 5px 8px",
                backgroundColor: "rgba(0,0,0,.05)",
                borderRadius: "3px",
              }}
              to={pageNumber > 2 ? `${blogURI}/page/${pageNumber - 1}` : `${blogURI}/`}
            >
              <span>Previous page</span>
            </Link>
          )
        }
        <span aria-current="page" className="page-numbers current" style={{ padding: "5px 10px" }}>
          <span className="meta-nav screen-reader-text">Page </span>
          {pageNumber}
        </span>

        {
          hasNextPage && (
            <Link
              style={{
                padding: "4px 8px 5px 8px",
                backgroundColor: "rgba(0,0,0,.05)",
                borderRadius: "3px",
              }}
              className="next page-numbers"
              to={`${blogURI}/page/${pageNumber + 1}`
              }
            >
              <span>Next page </span>
            </Link>
          )
        }
      </div>
    </div>
  )
}

export default Pagination

Kembali ke Create Post

Sekarang saatnya untuk menyesuaikan kembali file createPosts.js yang sudah dibuat sebelumnya agar bisa membaca blog post yang sudah dibuat di WordPress.

Buat file data.js

Disini digunakan konsep GraphQL Fragment yang akan membantu memisahkan beberapa poin dan membuat kode menjadi lebih terstruktur.

// src/templates/post/data.js

const PostTemplateFragment = `
  fragment PostTemplateFragment on WPGraphQL_Post {
    id
    postId
    title
    content
    link
    featuredImage {
      sourceUrl
    }
    categories {
      nodes {
        name
        slug
        id
      }
    }
    tags {
      nodes {
        slug
        name
        id
      }
    }
    author {
      name
      slug
    }
  }
`

const BlogPreviewFragment = `
  fragment BlogPreviewFragment on WPGraphQL_Post {
    id
    postId
    title
    uri
    date
    slug
    excerpt
    content
    featuredImage {
      sourceUrl
    }
    author {
      name
      slug
    }
  }
`

module.exports.PostTemplateFragment = PostTemplateFragment
module.exports.BlogPreviewFragment = BlogPreviewFragment

Sesuaikan kembali createPosts.js

// create/createPosts.js

const {
  PostTemplateFragment,
  BlogPreviewFragment,
} = require("../src/templates/post/data.js")

const { blogURI } = require("../globals")

const postTemplate = require.resolve("../src/templates/post/index.js")
const blogTemplate = require.resolve("../src/templates/post/blog.js")

const GET_POSTS = `
    ${PostTemplateFragment}
    ${BlogPreviewFragment}

    query GET_POSTS($first:Int $after:String) {
      wpgraphql {
        posts(
            first: $first
            after: $after
            where: {
                parent: null
            }
        ) {
          pageInfo {
              hasNextPage
              endCursor
          }
          nodes {           
              uri     
          }
        }
      }
    }
`
const allPosts = []
const blogPages = [];
let pageNumber = 0;
const itemsPerPage = 10;

module.exports = async ({ actions, graphql, reporter }, options) => {
  const { createPage } = actions
  const fetchPosts = async (variables) =>
    await graphql(GET_POSTS, variables).then(({ data }) => {
      const {
        wpgraphql: {
          posts: {
            nodes,
            pageInfo: { hasNextPage, endCursor },
          },
        },
      } = data

      const blogPagePath = !variables.after
        ? `${blogURI}`
        : `${blogURI}/page/${pageNumber + 1}`

      blogPages[pageNumber] = {
        path: blogPagePath,
        component: blogTemplate,
        context: {
          nodes,
          pageNumber: pageNumber + 1,
          hasNextPage,
          itemsPerPage,
          allPosts,
        },
      }

      nodes
      && nodes.map((posts) => {
        allPosts.push(posts)
      })

      if (hasNextPage) {
        pageNumber++
        reporter.info(`fetch post ${pageNumber} of posts...`)
        return fetchPosts({ first: itemsPerPage, after: endCursor })
      }

      return allPosts
    })

  await fetchPosts({ first: itemsPerPage, after: null }).then((wpPosts) => {

    wpPosts && wpPosts.map((post) => {
      const path = `${blogURI}/${post.uri}`

      createPage({
        path: path,
        component: postTemplate,
        context: {
          post: post,
        },
      })

      reporter.info(`post created:  ${post.uri}`)
    })

    reporter.info(`# -----> POSTS TOTAL: ${wpPosts.length}`)

    blogPages
    && blogPages.map((blogPage) => {
      if (blogPage.context.pageNumber === 1) {
        blogPage.context.publisher = true;
        blogPage.context.label = blogPage.path.replace('/', '');
      }
      createPage(blogPage);
      reporter.info(`created blog archive page ${blogPage.context.pageNumber}`);
    });
  })
}

Import string fragmen dan tempatkan didalam kueri GET_POST pertama (bukan yang query GET_POSTS($first:Int $after:String) {…}) yang berfungsi untuk register fragmen agar bisa digunakan di kueri yang mendatang.

Didalam kueri, bisa menggunakan standar … sintaks fragmen.

Navigasi

Membuat menu pada WordPress

Sebelum membuat menu, pastikan halaman yang ingin ditambahkan di dalam menu tersedia.

Daftar halaman

Dilanjutkan dengan membuat menu pada WordPress dengan masuk ke Appearance > Menus. Kemudian buat menu baru dan tambahkan halaman-halaman yang ingin dimasukkan ke dalam menu.

Membuat menu baru dengan WordPress

Selanjutnya saatnya bermain dengan GraphQL

GraphQL untuk mengambil menu

Ketika menggunakan tema Twenty Twenty, set where: location ke PRIMARY.

Membuat components di Gatsby

Dimulai dengan membuat fungsi utils untuk membuat URL relatif. Hal ini dilakukan karena endpoint GraphQL akan memberikan URL absolut dari instance WordPress.

// src/utils/index.js

export const CreateLocalLink = (menuItem, wordPressUrl, blogURI='blog/') => {
  const { url, connectedObject } = menuItem;

  if (url === '#') {
    return null;
  }

  if (connectedObject && connectedObject.__typename === 'WPGraphQL_Post') {
    newUri = blogURI + newUri;
  }

  return newUri;
};

Kemudian buat MenuItem Component

import React from "react"
import { CreateLocalLink } from "../utils"
import { Link } from "gatsby"

const MenuItem = ({ menuItem, wordPressUrl }) => {
  return (
    <Link style={{marginRight: '20px' }} to={CreateLocalLink(menuItem, wordPressUrl)}>{menuItem.label}</Link>
  )
}

export default MenuItem

Dilanjutkan dengan membuat Menu Component, yang akan memanggil MenuItem Component

import React from "react"
import { StaticQuery, graphql } from "gatsby"

import MenuItem from "./MenuItem"

const MENU_QUERY = graphql`

    fragment MenuItem on WPGraphQL_MenuItem {
        id
        label
        url
        title
        target
    }

    query GETMAINMENU {
        wpgraphql {
            menuItems(where: {location: PRIMARY}) {
                nodes {
                    ...MenuItem
                }
            }
            generalSettings {
                url
            }
        }
    }
`

const Menu = () => {
  return (
    <StaticQuery
      query={MENU_QUERY}
      render={(data) => {
        if (data.wpgraphql.menuItems) {
          const menuItems = data.wpgraphql.menuItems.nodes
          const wordPressUrl = data.wpgraphql.generalSettings.url

          return (
            <div style={{ marginBottom: "20px" }}>
              {
                menuItems &&
                menuItems.map((menuItem) => (
                  <MenuItem key={menuItem.id} menuItem={menuItem} wordPressUrl={wordPressUrl}/>
                ))
              }
            </div>
          )
        }
        return null
      }}
    />
  )
}

export default Menu

Kemudian tambahkan komponen Menu pada Layout

import React from "react"
import { Link } from "gatsby"
import Menu from "./Menu"

import { rhythm } from "../utils/typography"

const Layout = ({ title, children }) => {
  let header

  header = (
    <h3
      style={{
        fontFamily: `Montserrat, sans-serif`,
        marginTop: 0,
      }}
    >
      <Menu />
      <Link
        style={{
          boxShadow: `none`,
          textDecoration: `none`,
          color: `inherit`,
        }}
        to={`/`}
      >
        {title}
      </Link>
    </h3>
  )

  return (
    <div
      style={{
        marginLeft: `auto`,
        marginRight: `auto`,
        maxWidth: rhythm(24),
        padding: `${rhythm(1.5)} ${rhythm(3 / 4)}`,
      }}
    >
      <header>{header}</header>
      <main>{children}</main>
      <footer>
        © {new Date().getFullYear()}, Built with
        {` `}
        <a href="https://www.gatsbyjs.org">Gatsby</a>
      </footer>
    </div>
  )
}

export default Layout

Dan akhirnya selesai juga… 🥳

Penutup

Sampai disini, bisa dilihat bahwa Headless CMS dapat digunakan untuk membangun sebuah blog. Dengan Headless CMS, projek frontend bukan lagi bagian dari CMS. Headless CMS tidak memiliki kendali atas cara konten disajikan.

Terima kasih, semoga bermanfaat…