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.
npm i
npm run dev
The site is running at http://localhost:3000.
Basic understanding: How it works?
How pages are created?
pages/abc.tsx
leads tolocahost:3000/abc
pages/xyz/abc.tsx
leads tolocalhost:3000/xyz/abc
pages/[slug].tsx
leads tolocalhost:3000/<some-thing>
. In this file,- We need
getStaticPaths
to generates all the post urls. - We need
getStaticProps
to generate theprops
for the page template. In this props, we get the data of the post which is going to be created! How? It gets theparams.slug
from the URL (which comes from the patternpages/[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)
- We need
Vercel CLI
Build and run vercel environment locally before pushing to the remote.
npm i -g vercel
Link to the current project
vercel dev
# and then choose the corresponding options
Run build locally,
vercel build
Styling
SCSS / SASS
npm install --save-dev sass
👉 Basic Features: Built-in CSS Support | Next.js
Define /styles/main.scss
and import it in _app.tsx
,
import '../styles/main.scss'
Work with Tailwind
👉 Oficial and wonderful documentation.
✳️ Define a new class,
// in a css file
@layer components {
.thi-bg {
@apply bg-white dark:bg-main-dark-bg;
}
}
Use as: className="thi-bg"
✳️ Define a new color,
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
main: '#1e293b'
}
}
}
}
Use as: className="text-main bg-main"
✳️ Custom and dynamic colors,
// './src/styles/safelist.txt'
bg-[#1e293b]
// tailwind.config.js
module.exports = {
content: [
'./src/styles/safelist.txt'
]
}
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
,
define( 'GRAPHQL_JWT_AUTH_SECRET_KEY', 'daylamotkeybimat' );
Get a refresh token with GraphiQL IDE (at WP Admin > GraphQL > GraphiQL IDE),
mutation Login {
login(
input: {
clientMutationId: "uniqueId"
password: "your_password"
username: "your_username"
}
) {
refreshToken
}
}
Modify .env.local
WORDPRESS_AUTH_REFRESH_TOKEN="..."
WORDPRESS_PREVIEW_SECRET='daylamotkeybimat' # the same with the one in wp-config.php
Link the preview (id
is the id of the post, you can find it in the post list).
# "daylamotkeybimat" is the same as WORDPRESS_PREVIEW_SECRET
http://localhost:3000/api/preview?secret=daylamotkeybimat&id=12069
It may not work with math2it.local but math2it.com (the production version).
Dev environment
✳️ VSCode + ESLint + Prettier.
Follow instructions in this blog with additional things,
// Add to .eslintrc
{
rules: {},
extends: ['next'],
ignorePatterns: ['next-env.d.ts']
}
🪲 Error: Failed to load config "next" to extend from?
npm i -D eslint-config-next
✳️ Problem Unknown at rule @apply
when using TailwindCSS.
Add the folloing settings in the VSCode settings,
"css.validate": false, // used for @tailwindcss
"scss.validate": false, // used for @tailwindcss,
Troubleshooting after confuguring
✳️ 'React' must be in scope when using JSX
// Add this line to the top of the file
import React from 'react';
✳️ ...is missing in props validation
// Before
export default function Layout({ preview, children }) {}
// After
type LayoutProps = { preview: boolean; children: React.ReactNode }
export default function Layout(props: LayoutProps) {
const { preview, children } = props
}
Prettier things
npm install --save-dev @trivago/prettier-plugin-sort-imports
npm install -D prettier prettier-plugin-tailwindcss
In .prettierrc
,
{
"plugins": [
"./node_modules/@trivago/prettier-plugin-sort-imports",
"./node_modules/prettier-plugin-tailwindcss"
],
"importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}
Check ESLint server
There are some rules taking longer than the others. Use below command to see who they are.
TIMING=1 npx eslint lib
Rule | Time (ms) | Relative
:--------------------------------------|----------:|--------:
tailwindcss/no-custom-classname | 6573.680 | 91.0%
prettier/prettier | 543.009 | 7.5%
If you wanna turn off some rules (check more options),
// .eslintrc.js
{
rules: {
'tailwindcss/no-custom-classname': 'off'
}
}
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
npm install graphql
npm install -D typescript
npm install -D @graphql-codegen/cli
npx graphql-code-generator init
Below are my answers,
? What type of application are you building? Application built with React
? Where is your schema?: (path or url) http://math2it.local/graphql
? Where are your operations and fragments?: graphql/**/*.graphql
? Where to write the output: graphql/gql
? Do you want to generate an introspection file? No
? How to name the config file? codegen.ts
? What script in package.json should run the codegen? generate-types
codegen.ts
import { loadEnvConfig } from '@next/env'
import type { CodegenConfig } from '@graphql-codegen/cli'
loadEnvConfig(process.cwd())
const config: CodegenConfig = {
overwrite: true,
schema: process.env.WORDPRESS_API_URL,
documents: ['graphql/**/*.graphql', 'lib/api.ts'],
generates: {
'graphql/gql': {
preset: 'client',
plugins: [],
},
},
}
export default config
# install the chosen packages
npm install
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
,
import { loadEnvConfig } from '@next/env'
loadEnvConfig(process.cwd())
// then you can use
const config: CodegenConfig = {
schema: process.env.WORDPRESS_API_URL,
}
Generate types
Generate the types,
npm run generate-types
If you want to run in watch mode aloside with npm run dev
,
npm i -D concurrently
Then modify package.json
,
{
"scripts": [
"dev": "concurrently \"next\" \"npm run generate-types-watch\" -c green,yellow -n next,codegen",
"generate-types-watch": "graphql-codegen --watch --config codegen.ts"
]
}
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,
"""file: graphql/categories.graphql"""
query Categories {
categories {
edges {
node {
name
}
}
}
}
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,
// components/categories.tsx
import { CategoriesQuery } from '../graphql/gql/graphql'
export default function Categories(props: CategoriesQuery) {
const { categories } = props
return (
{ categories.edges.map(...) }
)
}
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!
// From this
const data = await fetchAPI(`
query PreviewPost($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
databaseId
slug
status
}
}`
)
// To this
const data = await fetchAPI(
/* GraphQL */ `
query PreviewPost($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
databaseId
slug
status
}
}`
)
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.
npm install @apollo/client graphql
// Modify lib/api.ts
const client = new ApolloClient({
uri: process.env.WORDPRESS_API_URL,
cache: new InMemoryCache(),
})
async function fetchAPI() {
export async function getAllPostsForHome() {
const { data } = await client.query({
query: gql`
query AllPosts {
posts(first: 20, where: { orderby: { field: DATE, order: DESC } }) {
edges {
node {
title
excerpt
}
}
}
}
`,
})
return data?.posts
}
}
// pages/index.tsx
import { getAllPostsForHome } from '../lib/api'
export default function Index({ allPosts: { edges } }) {
return ()
}
export const getStaticProps: GetStaticProps = async () => {
const allPosts = await getAllPostsForHome()
return {
props: { allPosts },
revalidate: 10,
}
}
GraphQL things
✳️ Query different queries in a single query with alias,
query getPosts {
posts: posts(type: "POST") {
id
title
}
comments: posts(type: "COMMENT") {
id
title
}
}
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,
import profilePic from '../public/me.png'
<Image
src={profilePic}
alt="Picture of the author"
/>
Inside an <a>
tag?
<Link href={`/posts/${slug}`} passHref>
<a aria-label={title}>
<Image />
</a>
</Link>
I use below codes,
(To apply "blur" placeholder effect for external images, we use plaiceholder)
// External images
<div style="position: relative;">
<Image
alt={imageAlt}
src={featuredImage?.sourceUrl}
className={imageClassName}
fill={true} // requires father having position "relative"
sizes={featuredImage?.sizes || '100vw'} // required
placeholder={placeholderTouse}
blurDataURL={blurDataURLToUse}
/>
</div>
// Internal images
<Image
alt={imageAlt}
src={defaultFeaturedImage}
className={imageClassName}
priority={true}
/>
- If you use
fill={true}
forImage
, 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 plaiceholder, 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).
// In the component containing <Image>
import Image from 'next/image'
import { useState } from 'react'
export default function ImageForPost({ title, featuredImage, blurDataURL, categories }) {
const [isImageReady, setIsImageReady] = useState(false)
const onLoadCallBack = () => {
setIsImageReady(true)
}
const image = (
<Image
alt={imageAlt}
src={externalImgSrc}
className={imageClassName}
fill={true}
sizes={externalImgSizes || '100vw'}
onLoadingComplete={onLoadCallBack}
/>
)
return (
<>
<div className="block h-full w-full md:animate-fadeIn">{image}</div>
{!isImageReady && (
<div className="absolute top-0 left-0 h-full w-full">
<div className="relative h-full w-full animate-pulse rounded-lg bg-slate-200">
<div className="absolute left-[14%] top-[30%] z-20 aspect-square h-[40%] rounded-full bg-slate-300"></div>
<div className="absolute bottom-0 left-0 z-10 h-2/5 w-full bg-wave"></div>
</div>
</div>
)}
</>
}
Custom fonts
👉 Basic Features: Font Optimization | Next.js
👉 google-font-display | Next.js
Some remarks:
Using
display=optional
https://fonts.googleapis.com/css2?family=Krona+One&display=optional"
Add Google font to
pages/_document.js
, nextjs will handle the rest.
Next.js currently supports optimizing Google Fonts and Typekit.
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
encodeURIComponent
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${encodeURIComponent(post.slug)}`}>
<a>{post.title}</a>
</Link>
</li>
))}
</ul>;
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?
import cn from 'classnames'
import { useRouter } from 'next/router'
export default async function Navigation() {
const router = useRouter()
const currentRoute = router.pathname
return (
{menus?.map((item: MenuItem) => (
<Link key={item?.id} href={item?.url as string}>
<a
className={isActiveClass(
item?.url === currentRoute
)}
aria-current={
item?.url === currentRoute ? 'page' : undefined
}
>
{item?.label}
</a>
</Link>
))}
)
}
const isActiveClass = (isCurrent: boolean) =>
cn(
'fixed-clasess',
{ 'is-current': isCurrent, 'not-current': !isCurrent }
)
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()
!
import { useRef, useState } from 'react'
import { useRouter } from 'next/router'
export default function Navigation() {
const router = useRouter()
const [valueSearch, setValueSearch] = useState('')
const searchInput = useRef(null)
return (
<form onSubmit={e => {
e.preventDefault()
router.push(`/search/?s=${encodeURI(valueSearch)}`)
}}
>
<button type="submit">Search</button>
<input
type="search"
value={valueSearch}
ref={searchInput}
onChange={e => setValueSearch(e.target.value)}
/>
</form>
)
}
👉 Read more: SWR data fetching.
👉 Read more: Client-side data fetching.
import React from 'react'
import Layout from '../components/layout'
import useSWR from 'swr'
import Link from 'next/link'
export default function SearchPage() {
const isBrowser = () => typeof window !== 'undefined'
let query = ''
if (isBrowser()) {
const { search } = window.location
query = new URLSearchParams(search).get('s') as string
}
const finalSearchTerm = decodeURI(query as string)
const { data, error } = useSWR(
`
{
posts(where: { search: "` +
finalSearchTerm +
`" }) {
nodes {
id
title
uri
}
}
}
`,
fetcher
)
return (
<Layout>
<div className="pt-20 px-8">
<div className="text-3xl">This is the search page!</div>
<div className="mt-8">
{error && <div className="pt-20">Failed to load</div>}
{!data && <div className="pt-20">Loading...</div>}
{data && (
<ul className="mb-6 list-disc pl-5">
{data?.posts?.nodes.map(node => (
<li key={node.id}>
<Link href={node.uri}>
<a>{node.title}</a>
</Link>
</li>
))}
</ul>
)}
</div>
</div>
</Layout>
)
}
const fetcher = async query => {
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
}
const res = await fetch('http://math2it.local/graphql', {
headers,
method: 'POST',
body: JSON.stringify({
query,
}),
})
const json = await res.json()
return json.data
}
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
const image = (<Image />)
// then
<Link href="">{image}</Link>
Wrap it with <a>
tag,
<Link href=""><a>{image}</a></Link>
🪲 If you use <a>
, a linting problem The href attribute is required for an anchor to be keyboard accessible....
// Try <button> instead of <a>
<Link href=""><button>{image}</button></Link>
Or add below rule to .eslintrc
(not that, estlint detects the problem but it's still valid from the accessibility point of view ). Source.
rules: {
'jsx-a11y/anchor-is-valid': [
'error',
{
components: ['Link'],
specialLink: ['hrefLeft', 'hrefRight'],
aspects: ['invalidHref', 'preferButton'],
},
],
}
👉 Read more in the official tut.
✳️ Warning: A title element received an array with more than 1 element as children.
// Instead of
<title>Next.js Blog Example with {CMS_NAME}</title>
// Use
const title = `Next.js Blog Example with ${CMS_NAME}`
<title>{title}</title>
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; }'.
// This will fail typecheck
const headers = { 'Content-Type': 'application/json' }
headers['Authorization'] = `Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`
// This will pass typecheck
const headers: { [key: string]: string } = { 'Content-Type': 'application/json' }
headers['Authorization'] = `Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`
✳️ (Codegen's error) [FAILED] Syntax Error: Expected Name, found ")".
// Instead of
query PostBySlug($id: ID!, $idType: PostIdType!) {
post(id: $id, idType: $idType) {
// You have
query PostBySlug($id: ID!, $idType: PostIdType!) {
post() { // <- here!!!
✳️ 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'
npm i --save-dev @types/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 Link
s getting href
an undefined
. Find and fix them or use,
<Link href={value ?? ''}>...</Link>
✳️ Restore to the previous position of the page when clicking the back button (Scroll Restoration)
// next.config.js
module.exports = {
experimental: {
scrollRestoration: true,
},
}
Remark: You have to restart the dev server.
💬 Comments