Thi's avatar
HomeAboutNotesBlogTopicsToolsReading
About|My sketches |Cooking |Cafe icon Support Thi
💌 [email protected]

Next.js & Wordpress

Anh-Thi Dinh
WordpressNext.jsWeb DevSSG
Left aside
⚠️
This note contains only the "difficult" parts when creating a Next.js website from Wordpress. You should read the official documetation and also things mentioned in this note (it's about Gatsby and WP).
☝
I prefer Next.js other than Gatsby when using with Wordpress because I want to use for free the "incremental building" functionality. As I know, if we want something like that with Gatsby, we have to use Gatsby Cloud (paid tiers).

Getting started

👉 IMPORTANT: Use this official starter (it uses TypeScript, Tailwind CSS).
⚠️
The following steps are heavily based on this starter, which means that some packages/settings have already been set up by this starter.
Using Local for a local version of Wordpress (WP) site. Read more in this blog. From now, I use math2it.local for a local WP website in this note and math2it.com for its production version.
In WP, install and activate plugin WPGraphQL.
Copy .env.local.example to .env.local and change it content.
The site is running at http://localhost:3000.

Basic understanding: How it works?

How pages are created?
  • pages/abc.tsx leads to locahost:3000/abc
  • pages/xyz/abc.tsx leads to localhost:3000/xyz/abc
  • pages/[slug].tsx leads to localhost:3000/<some-thing>. In this file,
    • We need getStaticPaths to generates all the post urls.
    • We need getStaticProps to generate the props for the page template. In this props, we get the data of the post which is going to be created! How? It gets the params.slug from the URL (which comes from the pattern pages/[slug].tsx). We use this params to get the post data. The [slug].tsx helps us catch the url's things. Read more about the routing.
    • Note that, all neccessary pages will be created before deploying (that why we call it static)

Vercel CLI

Build and run vercel environment locally before pushing to the remote.
👉 Vercel CLI – Vercel Docs
Link to the current project
Run build locally,

Styling

SCSS / SASS

👉 Basic Features: Built-in CSS Support | Next.js
Define /styles/main.scss and import it in _app.tsx

Work with Tailwind

👉 Oficial and wonderful documentation.
✳️ Define a new class,
Use as: className="thi-bg"
✳️ Define a new color,
Use as: className="text-main bg-main"
✳️ Custom and dynamic colors,
Use as: className="bg-[#1e293b]"

Preview mode

Check this doc and the following instructions.
Install and activate WP plugin wp-graphql-jwt-authentication.
Modify wp-config.php
Get a refresh token with GraphiQL IDE (at WP Admin > GraphQL > GraphiQL IDE),
Modify .env.local
Link the preview (id is the id of the post, you can find it in the post list).
⚠️
It may not work with math2it.local but math2it.com (the production version).

Dev environment

✳️ VSCode + ESLint + Prettier.
Follow instructions in Build a website with Wordpress and Gatsby (part 1) with additional things
🐝 Error: Failed to load config "next" to extend from?
✳️ Problem Unknown at rule @apply when using TailwindCSS.
Add the folloing settings in the VSCode settings,

Troubleshooting after confuguring

✳️ 'React' must be in scope when using JSX
✳️ ...is missing in props validation

Prettier things

In .prettierrc

Check ESLint server

There are some rules taking longer than the others. Use below command to see who they are.
If you wanna turn off some rules (check more options),
Run TIMING=1 npx eslint lib again to check!

Types for GraphQL queries

⚠️
Update: I decide to use self-defined types for what I use in the project.
We use GraphQL Code Generator (or codegen). Read the official doc.

Install codegen

Below are my answers,

Use codegen with env variable

👉 Official doc.
Want to use next.js's environment variable (.env.local) in the codegen's config file? Add the following codes to codegen.ts

Generate types

Generate the types,
If you want to run in watch mode aloside with npm run dev
Then modify package.json
Now, just use npm run dev for both. What you see is something like this

Usage

For example, you want to query categories from http://math2it.local/graphql with,
After run the generate code, we have type CategoriesQuery in graphql/gql/graphql.ts (==the name of the type is in the formulas <nameOfQuery>Query==). You can import this type in a .tsx component as follow,

Make codegen recognize query in lib/api.ts

Add /* GraphQL */ before the query string! Read more in the official doc for other use cases and options!

Use with Apollo Client

The starter we use from the beginning of this note is using typescript's built-in fetch() to get the data from WP GraphQL (check the content of lib/api.ts). You can use Apollo Client instead.

GraphQL things

✳️ Query different queries in a single query with alias,
Otherwise, we get an error Fields 'posts' conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.

Images

👉 Basic Features: Image Optimization | Next.js
👉
next/image | Next.js
👉 (I use this)
next/future/image | Next.js
Local images,
Inside an <a> tag?
I use below codes,
(To apply "blur" placeholder effect for external images, we use plaiceholder)
⚠️
Remarks:
  • If you use fill={true} for Image, its parent must have position "relative" or "fixed" or "absolute"!
  • If you use plaiceholder, the building time takes longer than usual. For my site, after applying , the building time increases from 1m30s to 2m30s on vercel!
🐝 Invalid next.config.js options detected: The value at .images.remotePatterns[0].port must be 1 character or more but it was 0 characters. → Remove port: ''!

Loading placeholder div for images

☝
We use only the CSS for the placeholder image. We gain the loading time and also the building time for this idea!
If you wanna add a div (with loading effect CSS only).

Custom fonts

👉 Basic Features: Font Optimization | Next.js
👉
google-font-display | Next.js
Some remarks:
  • Using display=optional
  • Add Google font to pages/_document.js, nextjs will handle the rest.
☝
Next.js currently supports optimizing Google Fonts and Typekit.s

Routes

👉 Routing: Introduction | Next.js
Catch all routes: pages/post/[...slug].js matches /post/a, but also /post/a/b, /post/a/b/c and so on.
Optional catch all routes: pages/post/[[...slug]].js will match /post, /post/a, /post/a/b, and so on.
The order: predefined routes > dynamic routes > catch all routes

Components

Navigation bar

Fetch menu data from WP and use it for navigation component? Read this for an idea. Note that, this data is fetched on the client-side using Vercel's SWR.
☝
I create a constant MENUS which defines all the links for the navigation. I don't want to fetch on the client side for this fixed menu.
Different classes for currently active menu?
Remark: router's pathname has a weekness when using with dynamic routes, check this for other solutions.

Taxonomy pages

URL format from Wordpress: /category/math/ or /category/math/page/2/ 👉 Create /pages/category/[[...slug]].tsx (Read more about optional catching all routes)
To query posts with "offset" and "limit", use this plugin.
Remark: When querying with graphql, $tagId gets type String whereas $categoryId gets type Int!

Search

If you need: when submitting the form, the site will navigate to the search page at /search/?s=query-string, use router.push()!
👉 Read more: SWR data fetching.
👉 Read more:
Client-side data fetching.

General troubleshooting

✳️ Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
Have this when using
Wrap it with <a> tag,
🐝 If you use <a>, a linting problem The href attribute is required for an anchor to be keyboard accessible....
Or add below rule to .eslintrc (not that, estlint detects the problem but it's still valid from the accessibility point of view ). Source.
👉 Read more in the official tut.
✳️ Warning: A title element received an array with more than 1 element as children.
Read this answer to understand the problem.
✳️ Element implicitly has an 'any' type because expression of type '"Authorization"' can't be used to index type '{ 'Content-Type': string; }'.
✳️ (Codegen's error) [FAILED] Syntax Error: Expected Name, found ")".
✳️ You cannot define a route with the same specificity as a optional catch-all route ("/choice" and "/choice\[\[...slug\]\]").
It's because you have both /pages/choice/[[slug]].tsx and /pages/choice.tsx. Removing one of two fixes the problem.
✳️ Could not find declaration file for module 'lodash'
✳️ TypeError: Cannot destructure property 'auth' of 'urlObj' as it is undefined.
Read this: prerender-error | Next.js. For my personal case, set fallback: false in getStaticPath() and I encountered also the problem of trailing slash. On the original WP, I use /about/ instead of /about. Add trailingSlash: true to the next.config.js fixes the problem.
✳️ Error: Failed prop type: The prop href expects a string or object in <Link>, but got undefined instead.
There are some Links getting href an undefined. Find and fix them or use,
✳️ Restore to the previous position of the page when clicking the back button (Scroll Restoration)
Remark: You have to restart the dev server.

References

  • Incremental Static Regeneration (ISR) – Vercel Docs
  • How to Update Static Content in Next.js Automatically with Incremental Static Regeneration (ISR) - Space Jelly
  • On-Demand ISR – Vercel
  • Debugging
◆Getting started◆Basic understanding: How it works?◆Vercel CLI◆Styling○SCSS / SASS○Work with Tailwind◆Preview mode◆Dev environment○Troubleshooting after confuguring○Prettier things◆Check ESLint server◆Types for GraphQL queries○Install codegen○Use codegen with env variable○Generate types○Usage○Make codegen recognize query in lib/api.ts◆Use with Apollo Client◆GraphQL things◆Images◆Loading placeholder div for images◆Custom fonts◆Routes◆Components○Navigation bar○Taxonomy pages○Search◆General troubleshooting◆References
About|My sketches |Cooking |Cafe icon Support Thi
💌 [email protected]
1npm i
2npm run dev
1npm i -g vercel
1vercel dev
2# and then choose the corresponding options
1vercel build
1npm install --save-dev sass
1import '../styles/main.scss'
1// in a css file
2@layer components {
3  .thi-bg {
4    @apply bg-white dark:bg-main-dark-bg;
5  }
6}
1// tailwind.config.js
2module.exports = {
3 theme: {
4   extend: {
5     colors: {
6       main: '#1e293b'
7     }
8   }
9 }
10}
1// './src/styles/safelist.txt'
2bg-[#1e293b]
1// tailwind.config.js
2module.exports = {
3  content: [
4    './src/styles/safelist.txt'
5  ]
6}
1define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'daylamotkeybimat' );
1mutation Login {
2  login(
3    input: {
4      clientMutationId: "uniqueId"
5      password: "your_password"
6      username: "your_username"
7    }
8  ) {
9  	refreshToken
10  }
11}
1WORDPRESS_AUTH_REFRESH_TOKEN="..."
2WORDPRESS_PREVIEW_SECRET='daylamotkeybimat' # the same with the one in wp-config.php
1# "daylamotkeybimat" is the same as WORDPRESS_PREVIEW_SECRET
2<http://localhost:3000/api/preview?secret=daylamotkeybimat&id=12069>
1// Add to .eslintrc
2{
3  rules: {},
4	extends: ['next'],
5	ignorePatterns: ['next-env.d.ts']
6}
1npm i -D eslint-config-next
1"css.validate": false, // used for @tailwindcss
2"scss.validate": false, // used for @tailwindcss,
1// Add this line to the top of the file
2import React from 'react';
1// Before
2export default function Layout({ preview, children }) {}
3
4// After
5type LayoutProps = { preview: boolean; children: React.ReactNode }
6export default function Layout(props: LayoutProps) {
7  const { preview, children } = props
8}
1npm install --save-dev @trivago/prettier-plugin-sort-imports
2npm install -D prettier prettier-plugin-tailwindcss
1{
2  "plugins": [
3    "./node_modules/@trivago/prettier-plugin-sort-imports",
4    "./node_modules/prettier-plugin-tailwindcss"
5  ],
6  "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
7  "importOrderSeparation": true,
8  "importOrderSortSpecifiers": true
9}
1TIMING=1 npx eslint lib
2Rule                                   | Time (ms) | Relative
3:--------------------------------------|----------:|--------:
4tailwindcss/no-custom-classname        |  6573.680 |    91.0%
5prettier/prettier                      |   543.009 |     7.5%
1// .eslintrc.js
2{
3  rules: {
4    'tailwindcss/no-custom-classname': 'off'
5  }
6}
1npm install graphql
2npm install -D typescript
3npm install -D @graphql-codegen/cli
1npx graphql-code-generator init
1? What type of application are you building? Application built with React
2? Where is your schema?: (path or url) <http://math2it.local/graphql>
3? Where are your operations and fragments?: graphql/**/*.graphql
4? Where to write the output: graphql/gql
5? Do you want to generate an introspection file? No
6? How to name the config file? codegen.ts
7? What script in package.json should run the codegen? generate-types
1# install the chosen packages
2npm install
1import { loadEnvConfig } from '@next/env'
2loadEnvConfig(process.cwd())
3
4// then you can use
5const config: CodegenConfig = {
6  schema: process.env.WORDPRESS_API_URL,
7}
1npm run generate-types
1npm i -D concurrently
1{
2  "scripts": [
3    "dev": "concurrently \\"next\\" \\"npm run generate-types-watch\\" -c green,yellow -n next,codegen",
4    "generate-types-watch": "graphql-codegen --watch --config codegen.ts"
5  ]
6}
1"""file: graphql/categories.graphql"""
2query Categories {
3  categories {
4    edges {
5      node {
6        name
7      }
8    }
9  }
10}
1// components/categories.tsx
2import { CategoriesQuery } from '../graphql/gql/graphql'
3
4export default function Categories(props: CategoriesQuery) {
5  const { categories } = props
6	return (
7  	{ categories.edges.map(...) }
8  )
9}
1// From this
2const data = await fetchAPI(`
3	query PreviewPost($id: ID!, $idType: PostIdType!) {
4    post(id: $id, idType: $idType) {
5      databaseId
6      slug
7      status
8    }
9	}`
10)
1// To this
2const data = await fetchAPI(
3  /* GraphQL */ `
4	query PreviewPost($id: ID!, $idType: PostIdType!) {
5    post(id: $id, idType: $idType) {
6      databaseId
7      slug
8      status
9    }
10	}`
11)
1npm install @apollo/client graphql
1// Modify lib/api.ts
2const client = new ApolloClient({
3  uri: process.env.WORDPRESS_API_URL,
4  cache: new InMemoryCache(),
5})
6
7async function fetchAPI() {
8  export async function getAllPostsForHome() {
9    const { data } = await client.query({
10      query: gql`
11        query AllPosts {
12          posts(first: 20, where: { orderby: { field: DATE, order: DESC } }) {
13            edges {
14              node {
15                title
16                excerpt
17              }
18            }
19          }
20        }
21      `,
22    })
23  	return data?.posts
24  }
25}
1// pages/index.tsx
2import { getAllPostsForHome } from '../lib/api'
3
4export default function Index({ allPosts: { edges } }) {
5  return ()
6}
7
8export const getStaticProps: GetStaticProps = async () => {
9  const allPosts = await getAllPostsForHome()
10  return {
11    props: { allPosts },
12    revalidate: 10,
13  }
14}
1query getPosts {
2  posts: posts(type: "POST") {
3    id
4		title
5  }
6  comments: posts(type: "COMMENT") {
7    id
8		title
9  }
10}
1import profilePic from '../public/me.png'
2<Image
3  src={profilePic}
4  alt="Picture of the author"
5/>
1<Link href={`/posts/${slug}`} passHref>
2  <a aria-label={title}>
3  	<Image />
4  </a>
5</Link>
1<!-- External images -->
2<div style="position: relative;">
3	<Image
4    alt={imageAlt}
5    src={featuredImage?.sourceUrl}
6    className={imageClassName}
7    fill={true} // requires father having position "relative"
8    sizes={featuredImage?.sizes || '100vw'} // required
9    placeholder={placeholderTouse}
10    blurDataURL={blurDataURLToUse}
11	/>
12</div>
13
14<!-- Internal images -->
15<Image
16  alt={imageAlt}
17  src={defaultFeaturedImage}
18  className={imageClassName}
19  priority={true}
20/>
1// In the component containing <Image>
2import Image from 'next/image'
3import { useState } from 'react'
4
5export default function ImageForPost({ title, featuredImage, blurDataURL, categories }) {
6  const [isImageReady, setIsImageReady] = useState(false)
7  const onLoadCallBack = () => {
8    setIsImageReady(true)
9  }
10  const image = (
11  	<Image
12      alt={imageAlt}
13      src={externalImgSrc}
14      className={imageClassName}
15      fill={true}
16      sizes={externalImgSizes || '100vw'}
17      onLoadingComplete={onLoadCallBack}
18    />
19  )
20  return (
21  	<>
22      <div className="block h-full w-full md:animate-fadeIn">{image}</div>
23    	{!isImageReady && (
24        <div className="absolute top-0 left-0 h-full w-full">
25          <div className="relative h-full w-full animate-pulse rounded-lg bg-slate-200">
26            <div className="absolute left-[14%] top-[30%] z-20 aspect-square h-[40%] rounded-full bg-slate-300"></div>
27            <div className="absolute bottom-0 left-0 z-10 h-2/5 w-full bg-wave"></div>
28          </div>
29        </div>
30  		)}
31    </>
32}
1<https://fonts.googleapis.com/css2?family=Krona+One&display=optional>"
1import cn from 'classnames'
2import { useRouter } from 'next/router'
3
4export default async function Navigation() {
5  const router = useRouter()
6  const currentRoute = router.pathname
7	return (
8  	{menus?.map((item: MenuItem) => (
9      <Link key={item?.id} href={item?.url as string}>
10        <a
11          className={isActiveClass(
12            item?.url === currentRoute
13          )}
14          aria-current={
15            item?.url === currentRoute ? 'page' : undefined
16          }
17          >
18          {item?.label}
19        </a>
20      </Link>
21    ))}
22  )
23}
24
25const isActiveClass = (isCurrent: boolean) =>
26	cn(
27  	'fixed-clasess',
28    { 'is-current': isCurrent, 'not-current': !isCurrent }
29  )
1import { useRef, useState } from 'react'
2import { useRouter } from 'next/router'
3
4export default function Navigation() {
5	const router = useRouter()
6  const [valueSearch, setValueSearch] = useState('')
7  const searchInput = useRef(null)
8  return (
9  	<form onSubmit={e => {
10      e.preventDefault()
11      router.push(`/search/?s=${encodeURI(valueSearch)}`)
12    }}
13    >
14    	<button type="submit">Search</button>
15      <input
16        type="search"
17        value={valueSearch}
18        ref={searchInput}
19        onChange={e => setValueSearch(e.target.value)}
20      />
21    </form>
22  )
23}
1import React from 'react'
2import Layout from '../components/layout'
3import useSWR from 'swr'
4import Link from 'next/link'
5
6export default function SearchPage() {
7  const isBrowser = () => typeof window !== 'undefined'
8  let query = ''
9  if (isBrowser()) {
10    const { search } = window.location
11    query = new URLSearchParams(search).get('s') as string
12  }
13  const finalSearchTerm = decodeURI(query as string)
14  const { data, error } = useSWR(
15    `
16      {
17        posts(where: { search: "` +
18      finalSearchTerm +
19      `" }) {
20          nodes {
21            id
22            title
23            uri
24          }
25        }
26      }
27    `,
28    fetcher
29  )
30
31  return (
32    <Layout>
33      <div className="pt-20 px-8">
34        <div className="text-3xl">This is the search page!</div>
35        <div className="mt-8">
36          {error && <div className="pt-20">Failed to load</div>}
37          {!data && <div className="pt-20">Loading...</div>}
38          {data && (
39            <ul className="mb-6 list-disc pl-5">
40              {data?.posts?.nodes.map(node => (
41                <li key={node.id}>
42                  <Link href={node.uri}>
43                    <a>{node.title}</a>
44                  </Link>
45                </li>
46              ))}
47            </ul>
48          )}
49        </div>
50      </div>
51    </Layout>
52  )
53}
54
55const fetcher = async query => {
56  const headers: { [key: string]: string } = {
57    'Content-Type': 'application/json',
58  }
59  const res = await fetch('<http://math2it.local/graphql>', {
60    headers,
61    method: 'POST',
62    body: JSON.stringify({
63      query,
64    }),
65  })
66  const json = await res.json()
67  return json.data
68}
1const image = (<Image />)
2// then
3<Link href="">{image}</Link>
1<Link href=""><a>{image}</a></Link>
1<!-- Try <button> instead of <a> -->
2<Link href=""><button>{image}</button></Link>
1rules: {
2  'jsx-a11y/anchor-is-valid': [
3    'error',
4    {
5      components: ['Link'],
6      specialLink: ['hrefLeft', 'hrefRight'],
7      aspects: ['invalidHref', 'preferButton'],
8    },
9  ],
10}
1<!-- Instead of -->
2<title>Next.js Blog Example with {CMS_NAME}</title>
3
4<!-- Use -->
5const title = `Next.js Blog Example with ${CMS_NAME}`
6<title>{title}</title>
1// This will fail typecheck
2const headers = { 'Content-Type': 'application/json' }
3headers['Authorization'] = `Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`
4
5// This will pass typecheck
6const headers: { [key: string]: string } = { 'Content-Type': 'application/json' }
7headers['Authorization'] = `Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`
8
1// Instead of
2query PostBySlug($id: ID!, $idType: PostIdType!) {
3  post(id: $id, idType: $idType) {
4
5// You have
6query PostBySlug($id: ID!, $idType: PostIdType!) {
7  post() { // <- here!!!
1npm i --save-dev @types/lodash
1<Link href={value ?? ''}>...</Link>
1// next.config.js
2module.exports = {
3	experimental: {
4    scrollRestoration: true,
5  },
6}