😍Study Cms Wordpress Nextjs (ok)

Step 1: Config env

next.config.ts

if (!URL.canParse(process.env.WORDPRESS_API_URL as string)) {
  throw new Error(`Please provide a valid WordPress instance URL.Add to your environment variables WORDPRESS_API_URL.`);
}
const { protocol, hostname, port, pathname } = new URL(process.env.WORDPRESS_API_URL as string);
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
  reactStrictMode: true,
  images: {
    domains: [
      'wpclidemo.dev',
      "2.gravatar.com",
      "0.gravatar.com",
      "secure.gravatar.com",
    ],
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'wpclidemo.dev',
        port: '',
        pathname: '**',
      },
    ],
  },
};
export default nextConfig;

Step 2: Create Home Page

pages\_document.tsx

import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body className="min-h-screen">
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

pages\_app.tsx

import "@/styles/globals.css";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

constants\index.ts

export const EXAMPLE_PATH = "cms-wordpress";
export const CMS_NAME = "WordPress";
export const CMS_URL = "https://wordpress.org";
export const HOME_OG_IMAGE_URL ="https://og-image.vercel.app/Next.js%20Blog%20Example%20with%20**WordPress**.png?theme=light&md=1&fontSize=75px&images=https%3A%2F%2Fassets.vercel.com%2Fimage%2Fupload%2Ffront%2Fassets%2Fdesign%2Fnextjs-black-logo.svg&images=data%3Aimage%2Fsvg%2Bxml%2C%253C%253Fxml+version%3D%271.0%27+encoding%3D%27UTF-8%27%253F%253E%253Csvg+preserveAspectRatio%3D%27xMidYMid%27+version%3D%271.1%27+viewBox%3D%270+0+256+255%27+xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg+fill%3D%27%2523464342%27%253E%253Cpath+d%3D%27m18.124+127.5c0+43.295+25.161+80.711+61.646+98.441l-52.176-142.96c-6.0691+13.603-9.4699+28.657-9.4699+44.515zm183.22-5.5196c0-13.518-4.8557-22.88-9.0204-30.166-5.5446-9.01-10.742-16.64-10.742-25.65+0-10.055+7.6259-19.414+18.367-19.414+0.48494+0+0.94491+0.060358+1.4174+0.087415-19.46-17.828-45.387-28.714-73.863-28.714-38.213+0-71.832+19.606-91.39+49.302+2.5662+0.077008+4.9847+0.13112+7.039+0.13112+11.441+0+29.151-1.3882+29.151-1.3882+5.8963-0.34758+6.5915+8.3127+0.7014+9.01+0+0-5.9255+0.69724-12.519+1.0427l39.832+118.48+23.937-71.79-17.042-46.692c-5.8901-0.3455-11.47-1.0427-11.47-1.0427-5.8942-0.3455-5.2033-9.3575+0.69099-9.01+0+0+18.064+1.3882+28.811+1.3882+11.439+0+29.151-1.3882+29.151-1.3882+5.9005-0.34758+6.5936+8.3127+0.7014+9.01+0+0-5.938+0.69724-12.519+1.0427l39.528+117.58+10.91-36.458c4.7287-15.129+8.3273-25.995+8.3273-35.359zm-71.921+15.087l-32.818+95.363c9.7988+2.8805+20.162+4.4561+30.899+4.4561+12.738+0+24.953-2.202+36.323-6.2002-0.29346-0.46829-0.55987-0.96572-0.77841-1.5069l-33.625-92.112zm94.058-62.046c0.47037+3.4841+0.73678+7.2242+0.73678+11.247+0+11.1-2.073+23.577-8.3169+39.178l-33.411+96.599c32.518-18.963+54.391-54.193+54.391-94.545+0.002081-19.017-4.8557-36.899-13.399-52.48zm-95.977-75.023c-70.304+0-127.5+57.196-127.5+127.5+0+70.313+57.2+127.51+127.5+127.51+70.302+0+127.51-57.194+127.51-127.51-0.002082-70.304-57.209-127.5-127.51-127.5zm0+249.16c-67.08+0-121.66-54.578-121.66-121.66+0-67.08+54.576-121.65+121.66-121.65+67.078+0+121.65+54.574+121.65+121.65+0+67.084-54.574+121.66-121.65+121.66z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E";

components\layout.tsx

import Header from "./header";
import Main from "./main";
import Footer from "./footer";
export default function Layout({ children }: any) {
  return (
    <>
      <Header />
      <Main>{children}</Main>
      <Footer />
    </>
  );
}

components\header.tsx

import Link from "next/link";
export default function Header() {
  return (
    <header className="text-gray-600 body-font">
      <div className="container flex flex-col flex-wrap items-center p-5 mx-auto md:flex-row">
        <nav className="flex flex-wrap items-center text-base lg:w-2/5 md:ml-auto">
          <a className="mr-5 hover:text-gray-900">First Link</a>
          <a className="mr-5 hover:text-gray-900">Second Link</a>
          <a className="mr-5 hover:text-gray-900">Third Link</a>
          <a className="hover:text-gray-900">Fourth Link</a>
        </nav>
        <a className="flex items-center order-first mb-4 font-medium text-gray-900 lg:order-none lg:w-1/5 title-font lg:items-center lg:justify-center md:mb-0">
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" className="w-10 h-10 p-2 text-white bg-indigo-500 rounded-full" viewBox="0 0 24 24">
            <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
          </svg>
          <span className="ml-3 text-xl">Tailblocks</span>
        </a>
        <div className="inline-flex ml-5 lg:w-2/5 lg:justify-end lg:ml-0">
          <button className="inline-flex items-center px-3 py-1 mt-4 text-base bg-gray-100 border-0 rounded focus:outline-none hover:bg-gray-200 md:mt-0">Button
            <svg fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" className="w-4 h-4 ml-1" viewBox="0 0 24 24">
              <path d="M5 12h14M12 5l7 7-7 7"></path>
            </svg>
          </button>
        </div>
      </div>
    </header>
  );
}

components\main.tsx

export default function Main({ children }: any) {
  return (
    <main>
      {children}
    </main>
  )
}

components\footer.tsx

import Container from "./container";
export default function Footer() {
  return (
    <footer className="bg-accent-1">
      <div className="container p-5 m-auto">
        <h3>Statically Generated with Next.js.</h3>
      </div>
    </footer>
  )
}

components\layout.tsx

import Header from "./header";
import Main from "./main";
import Footer from "./footer";
export default function Layout({ children }: any) {
  return (
    <>
      <Header />
      <Main>{children}</Main>
      <Footer />
    </>
  );
}

components\container.tsx

export default function Container({ children }:any) {
  return <div className="container px-5 mx-auto">{children}</div>;
}

components\intro.tsx

import { CMS_NAME } from "../constants";
export default function Intro() {
  return (
    <section className="flex flex-col">
      <p className="w-full mb-5">A statically generated blog example using <span className="text-orange-950">Next.js</span> and <span className="text-orange-950">{CMS_NAME}</span></p>
      <img src="./graphiql.png" alt="graphiql" />
    </section>
  );
}

pages\index.tsx

import Head from "next/head";
import Layout from "@/components/layout";
import { CMS_NAME } from "@/lib/constants";
import Intro from "@/components/intro";
import Container from "@/components/container";
export default function Home() {
  return (
    <Layout>
      <Head>
        <title>{`Next.js Blog Example with ${CMS_NAME}`}</title>
      </Head>
      <Container>
        <Intro />
      </Container>
    </Layout>
  );
}

.env.local

WORDPRESS_API_URL=https://wpclidemo.dev/graphql
NODE_TLS_REJECT_UNAUTHORIZED=0
# Only required if you want to enable preview mode
# WORDPRESS_AUTH_REFRESH_TOKEN=
# WORDPRESS_PREVIEW_SECRET=

Step 3: getAllPostsForHome

apis\index.ts

const API_URL = process.env.WORDPRESS_API_URL as string;
async function fetchAPI(query = "", { variables }: Record<string, any> = {}) {
  const headers = { "Content-Type": "application/json" };
  const res = await fetch(API_URL, {
    headers,
    method: "POST",
    body: JSON.stringify({
      query,
      variables,
    }),
  });
  const json = await res.json();
  if (json.errors) {
    console.error(json.errors);
    throw new Error("Failed to fetch API");
  }
  return json.data;
}
export async function getAllPostsForHome() {
  const data = await fetchAPI(`
  query AllPosts {
      posts(first: 20, where: { orderby: { field: DATE, order: DESC } }) {
        edges {
          node {
            title
            excerpt
            slug
            date
            featuredImage {
              node {
                sourceUrl
              }
            }
            author {
              node {
                name
                firstName
                lastName
                avatar {
                  url
                }
              }
            }
          }
        }
      }
    }
 `);
  return data?.posts;
}

pages\index.tsx

import Head from "next/head";
import Layout from "@/components/layout";
import { CMS_NAME } from "@/constants";
import Intro from "@/components/intro";
import Container from "@/components/container";
import { getAllPostsForHome } from "@/apis";
import { GetStaticProps } from "next";
export default function Home({ allPosts: { edges } }:any) {
  console.log(edges);
  return (
    <Layout>
      <Head>
        <title>{`Next.js Blog Example with ${CMS_NAME}`}</title>
      </Head>
      <Container>
        <Intro />
      </Container>
    </Layout>
  );
}
export const getStaticProps: GetStaticProps = async () => {
  const allPosts = await getAllPostsForHome();
  return {
    props: { allPosts },
    revalidate: 10,
  };
}

Step 3.1: HeroPost

pages\index.tsx

import Head from "next/head";
import Layout from "@/components/layout";
import { CMS_NAME } from "@/constants";
import Container from "@/components/container";
import { getAllPostsForHome } from "@/apis";
import { GetStaticProps } from "next";
import HeroPost from "@/components/hero-post";
export default function Home({ allPosts: { edges } }:any) {
  const heroPost = edges[0]?.node;
  return (
    <Layout>
      <Head>
        <title>{`Next.js Blog Example with ${CMS_NAME}`}</title>
      </Head>
      <Container>
        {heroPost && (
          <HeroPost
            title={heroPost.title}
            coverImage={heroPost.featuredImage}
            date={heroPost.date}
            author={heroPost.author}
            slug={heroPost.slug}
            excerpt={heroPost.excerpt}
          />
        )}
      </Container>
    </Layout>
  );
}
export const getStaticProps: GetStaticProps = async () => {
  const allPosts = await getAllPostsForHome();
  return {
    props: { allPosts },
    revalidate: 10,
  };
}

components\hero-post.tsx

import Link from "next/link";
import CoverImage from "./cover-image";
import Date from "./date";
import Avatar from "./avatar";
export default function HeroPost({ title, coverImage, date, excerpt, author, slug, }: any) {
  return (
    <section className="relative">
      <div className="mb-8 md:mb-16">
        {coverImage && (
          <CoverImage title={title} coverImage={coverImage} slug={slug} />
        )}
      </div>
      <div className="md:grid md:grid-cols-2 md:gap-x-16 lg:gap-x-8 p-2 absolute left-1 bottom-1 bg-slate-300 bg-opacity-50">
        <div>
          <h3 className="mb-4 text-4xl lg:text-6xl leading-tight">
            <Link
              href={`/posts/${slug}`}
              className="hover:underline"
              dangerouslySetInnerHTML={{ __html: title }}
            ></Link>
          </h3>
          <div className="mb-4 md:mb-0 text-lg">
            <Date dateString={date} />
          </div>
        </div>
        <div>
          <div
            className="text-lg leading-relaxed mb-4"
            dangerouslySetInnerHTML={{ __html: excerpt }}
          />
          <Avatar author={author} />
        </div>
      </div>
    </section>
  )
}

components\cover-image.tsx

import Image from "next/image";
import Link from "next/link";
interface Props {
  title: string;
  coverImage: {
    node: {
      sourceUrl: string;
    };
  };
  slug?: string;
}
export default function CoverImage({ title, coverImage, slug }: Props) {
  const image = (
    <Image
      alt={`Cover Image for ${title}`}
      fill={true}
      src={coverImage?.node.sourceUrl}
      objectFit="cover"
    />
  );
  return (
    <div className="sm:mx-0 relative h-[450px] w-full">
      {slug ? (
        <Link href={`/posts/${slug}`} aria-label={title}>
          {image}
        </Link>
      ) : (
        image
      )}
    </div>
  );
}

components\date.tsx

import { parseISO, format } from "date-fns";
export default function Date({ dateString }:any) {
  const date = parseISO(dateString);
  return <time dateTime={dateString}>{format(date, "LLLL	d, yyyy")}</time>;
}

components\avatar.tsx

import Image from "next/image";
export default function Avatar({ author }:any) {
  const isAuthorHaveFullName = author?.node?.firstName && author?.node?.lastName;
  const name = isAuthorHaveFullName ? `${author.node.firstName} ${author.node.lastName}` : author.node.name || null;
  return (
    <div className="flex items-center">
      <div className="w-12 h-12 relative mr-4">
        <Image
          src={author.node.avatar.url}
          layout="fill"
          className="rounded-full"
          alt={name}
        />
      </div>
      <div className="text-xl font-bold">{name}</div>
    </div>
  );
}

Step 3.2 MorePost

pages\index.tsx

import Head from "next/head";
import Layout from "@/components/layout";
import { CMS_NAME } from "@/constants";
import Container from "@/components/container";
import { getAllPostsForHome } from "@/apis";
import { GetStaticProps } from "next";
import HeroPost from "@/components/hero-post";
import MoreStories from "../components/more-stories";
export default function Home({ allPosts: { edges } }:any) {
  const heroPost = edges[0]?.node;
  const morePosts = edges.slice(1);
  return (
    <Layout>
      <Head>
        <title>{`Next.js Blog Example with ${CMS_NAME}`}</title>
      </Head>
      <Container>
        {heroPost && (
          <HeroPost
            title={heroPost.title}
            coverImage={heroPost.featuredImage}
            date={heroPost.date}
            author={heroPost.author}
            slug={heroPost.slug}
            excerpt={heroPost.excerpt}
          />
        )}
        {morePosts.length > 0 && <MoreStories posts={morePosts} />}
      </Container>
    </Layout>
  );
}
export const getStaticProps: GetStaticProps = async () => {
  const allPosts = await getAllPostsForHome();
  return {
    props: { allPosts },
    revalidate: 10,
  };
}

components\more-stories.tsx

import PostPreview from "./post-preview";
export default function MoreStories({ posts }:any) {
  return (
    <section>
      <h2 className="mb-8 text-6xl md:text-7xl font-bold tracking-tighter leading-tight">
        More Stories
      </h2>
      <div className="grid grid-cols-1 md:grid-cols-2 md:gap-x-16 lg:gap-x-32 gap-y-20 md:gap-y-32 mb-32">
        {posts.map(({ node }:any) => (
          <PostPreview
            key={node.slug}
            title={node.title}
            coverImage={node.featuredImage}
            date={node.date}
            author={node.author}
            slug={node.slug}
            excerpt={node.excerpt}
          />
        ))}
      </div>
    </section>
  )
}

components\post-preview.tsx

import Avatar from "./avatar";
import Date from "./date";
import CoverImage from "./cover-image";
import Link from "next/link";
export default function PostPreview({ title, coverImage, date, excerpt, author, slug, }: any) {
  return (
    <div>
      <div className="mb-5">
        {coverImage && (<CoverImage title={title} coverImage={coverImage} slug={slug} />)}
      </div>
      <h3 className="text-3xl mb-3 leading-snug">
        <Link
          href={`/posts/${slug}`}
          className="hover:underline"
          dangerouslySetInnerHTML={{ __html: title }}
        ></Link>
      </h3>
      <div className="text-lg mb-4">
        <Date dateString={date} />
      </div>
      <div
        className="text-lg leading-relaxed mb-4"
        dangerouslySetInnerHTML={{ __html: excerpt }}
      />
      <Avatar author={author} />
    </div>
  )
}

Step 4: Detail Post

pages\posts[slug].tsx

import { getAllPostsWithSlug, getPostAndMorePosts } from "@/apis";
import Container from "@/components/container";
import Layout from "@/components/layout";
import PostBody from "@/components/post-body";
import PostHeader from "@/components/post-header";
import { CMS_NAME } from "@/constants";
import { GetStaticPaths, GetStaticProps } from "next";
import Head from "next/head";
export default function Post({ post, posts }: any) {
  console.log(post);
  return (
    <Layout>
      <Container>
        <>
          <article>
            <Head>
              <title>
                {`${post.title} | Next.js Blog Example with ${CMS_NAME}`}
              </title>
              <meta
                property="og:image"
                content={post.featuredImage?.node.sourceUrl}
              />
            </Head>
            <PostHeader
              title={post.title}
              coverImage={post.featuredImage}
              date={post.date}
              author={post.author}
              categories={post.categories}
            />
            <PostBody content={post.content} />
          </article>
        </>
      </Container>
    </Layout>
  )
};
export const getStaticProps: GetStaticProps = async ({ params, previewData }: any) => {
  const data: any = await getPostAndMorePosts(params?.slug, previewData);
  return {
    props: {
      post: data.post,
      posts: data.posts,
    },
    revalidate: 10,
  };
}
export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await getAllPostsWithSlug();
  return {
    paths: allPosts.edges.map(({ node }: any) => `/posts/${node.slug}`) || [],
    fallback: true,
  };
};

apis\index.ts

const API_URL = process.env.WORDPRESS_API_URL as string;
async function fetchAPI(query = "", { variables }: Record<string, any> = {}) {
  const headers: any = { "Content-Type": "application/json" };
  if (process.env.WORDPRESS_AUTH_REFRESH_TOKEN) {
    headers["Authorization"] = `Bearer ${process.env.WORDPRESS_AUTH_REFRESH_TOKEN}`;
  }
  // WPGraphQL Plugin must be enabled
  const res = await fetch(API_URL, {
    headers,
    method: "POST",
    body: JSON.stringify({
      query,
      variables,
    }),
  });
  const json = await res.json();
  if (json.errors) {
    console.error(json.errors);
    throw new Error("Failed to fetch API");
  }
  return json.data;
}
export async function getPreviewPost(id: any, idType = "DATABASE_ID") {
  const data = await fetchAPI(
    `
    query PreviewPost($id: ID!, $idType: PostIdType!) {
      post(id: $id, idType: $idType) {
        databaseId
        slug
        status
      }
    }`,
    {
      variables: { id, idType },
    },
  );
  return data.post;
}
export async function getAllPostsWithSlug() {
  const data = await fetchAPI(`
    {
      posts(first: 10000) {
        edges {
          node {
            slug
          }
        }
      }
    }
  `);
  return data?.posts;
}
export async function getAllPostsForHome() {
  const data = await fetchAPI(
    `
    query AllPosts {
      posts(first: 20, where: { orderby: { field: DATE, order: DESC } }) {
        edges {
          node {
            title
            excerpt
            slug
            date
            featuredImage {
              node {
                sourceUrl
              }
            }
            author {
              node {
                name
                firstName
                lastName
                avatar {
                  url
                }
              }
            }
          }
        }
      }
    }
  `
  );
  return data?.posts;
}
export async function getPostAndMorePosts(slug: any, previewData: any) {
  const postPreview = previewData?.post;
  // The slug may be the id of an unpublished post
  const isId = Number.isInteger(Number(slug));
  const isSamePost = isId ? Number(slug) === postPreview.id : slug === postPreview?.slug;
  const isDraft = isSamePost && postPreview?.status === "draft";
  const isRevision = isSamePost && postPreview?.status === "publish";
  const data = await fetchAPI(
    `
    fragment AuthorFields on User {
      name
      firstName
      lastName
      avatar {
        url
      }
    }
    fragment PostFields on Post {
      title
      excerpt
      slug
      date
      featuredImage {
        node {
          sourceUrl
        }
      }
      author {
        node {
          ...AuthorFields
        }
      }
      categories {
        edges {
          node {
            name
          }
        }
      }
      tags {
        edges {
          node {
            name
          }
        }
      }
    }
    query PostBySlug($id: ID!, $idType: PostIdType!) {
      post(id: $id, idType: $idType) {
        ...PostFields
        content
      }
      posts(first: 3, where: { orderby: { field: DATE, order: DESC } }) {
        edges {
          node {
            ...PostFields
          }
        }
      }
    }
  `,
    {
      variables: {
        id: isDraft ? postPreview.id : slug,
        idType: isDraft ? "DATABASE_ID" : "SLUG",
      },
    },
  );
  // Draft posts may not have an slug
  if (isDraft) data.post.slug = postPreview.id;
  // Filter out the main post
  data.posts.edges = data.posts.edges.filter(({ node }: any) => node.slug !== slug);
  // If there are still 3 posts, remove the last one
  if (data.posts.edges.length > 2) data.posts.edges.pop();
  return data;
}

components\post-title.tsx

export default function PostTitle({ children }:any) {
  return (
    <h1
      className="text-6xl md:text-7xl lg:text-8xl font-bold tracking-tighter leading-tight md:leading-none mb-12 text-center md:text-left"
      dangerouslySetInnerHTML={{ __html: children }}
    />
  );
}

components\post-header.tsx

import Avatar from "./avatar";
import Date from "./date";
import CoverImage from "./cover-image";
import PostTitle from "./post-title";
import Categories from "./categories";
export default function PostHeader({ title, coverImage, date, author, categories }:any) {
  return (
    <>
      <PostTitle>{title}</PostTitle>
      <div className="mb-8 md:mb-16 sm:mx-0">
        <CoverImage title={title} coverImage={coverImage} />
      </div>
      <div className="w-full">
        <div className="block  mb-6">
          <Avatar author={author} />
        </div>
        <div className="mb-6 text-lg">
          Posted <Date dateString={date} />
          <Categories categories={categories} />
        </div>
      </div>
    </>
  );
}

components\post-body.tsx

import styles from "./post-body.module.css";
export default function PostBody({ content }:any) {
  return (
    <div className="w-full">
      <div
        className={styles.content}
        dangerouslySetInnerHTML={{ __html: content }}
      />
    </div>
  );
}

Step 4.1 Tags

pages\posts\[slug].tsx

import { getAllPostsWithSlug, getPostAndMorePosts } from "@/apis";
import Container from "@/components/container";
import Layout from "@/components/layout";
import PostBody from "@/components/post-body";
import PostHeader from "@/components/post-header";
import { CMS_NAME } from "@/constants";
import { GetStaticPaths, GetStaticProps } from "next";
import Tags from "@/components/tags";
import Head from "next/head";
export default function Post({ post, posts }: any) {
  return (
    <Layout>
      <Container>
        <>
          <article>
            <Head>
              <title>
                {`${post.title} | Next.js Blog Example with ${CMS_NAME}`}
              </title>
              <meta
                property="og:image"
                content={post.featuredImage?.node.sourceUrl}
              />
            </Head>
            <PostHeader
              title={post.title}
              coverImage={post.featuredImage}
              date={post.date}
              author={post.author}
              categories={post.categories}
            />
            <PostBody content={post.content} />
            <footer>
              {post.tags.edges.length > 0 && <Tags tags={post.tags} />}
            </footer>
          </article>
        </>
      </Container>
    </Layout>
  )
};
export const getStaticProps: GetStaticProps = async ({ params, previewData }: any) => {
  const data: any = await getPostAndMorePosts(params?.slug, previewData);
  return {
    props: {
      post: data.post,
      posts: data.posts,
    },
    revalidate: 10,
  };
}
export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await getAllPostsWithSlug();
  return {
    paths: allPosts.edges.map(({ node }: any) => `/posts/${node.slug}`) || [],
    fallback: true,
  };
};

components\tags.tsx

export default function Tags({ tags }: any) {
  return (
    <div className="max-w-2xl mx-auto">
      <p className="mt-8 text-lg font-bold">
        Tagged
        {tags.edges.map((tag: any, index: any) => (
          <span key={index} className="ml-4 font-normal">
            {tag.node.name}
          </span>
        ))}
      </p>
    </div>
  );
}

Step 4.1 Xử lý trường hợp không có post thật trên đường dẫn

pages\posts\[slug].tsx

import { getAllPostsWithSlug, getPostAndMorePosts } from "@/apis";
import Container from "@/components/container";
import Layout from "@/components/layout";
import PostBody from "@/components/post-body";
import PostHeader from "@/components/post-header";
import { CMS_NAME } from "@/constants";
import { GetStaticPaths, GetStaticProps } from "next";
import Tags from "@/components/tags";
import Head from "next/head";
import { useRouter } from "next/router";
import MoreStories from "@/components/more-stories";
import ErrorPage from "next/error";
import PostTitle from "@/components/post-title";
export default function Post({ post, posts }: any) {
  const router = useRouter();
  const morePosts = posts?.edges;
  if (!router.isFallback && !post?.slug) {
    return <ErrorPage statusCode={404} />;
  }
  return (
    <Layout>
      <Container>
        {router.isFallback ? (<PostTitle>Loading…</PostTitle>) : (
          <>
            <article>
              <Head>
                <title>
                  {`${post.title} | Next.js Blog Example with ${CMS_NAME}`}
                </title>
                <meta
                  property="og:image"
                  content={post.featuredImage?.node.sourceUrl}
                />
              </Head>
              <PostHeader
                title={post.title}
                coverImage={post.featuredImage}
                date={post.date}
                author={post.author}
                categories={post.categories}
              />
              <PostBody content={post.content} />
              <footer>
                {post.tags.edges.length > 0 && <Tags tags={post.tags} />}
              </footer>
            </article>
          </>
        )}
      </Container>
    </Layout>
  )
};
export const getStaticProps: GetStaticProps = async ({ params, previewData }: any) => {
  const data: any = await getPostAndMorePosts(params?.slug, previewData);
  return {
    props: {
      post: data.post,
      posts: data.posts,
    },
    revalidate: 10,
  };
}
export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await getAllPostsWithSlug();
  return {
    paths: allPosts.edges.map(({ node }: any) => `/posts/${node.slug}`) || [],
    fallback: true,
  };
};

Step 4.2 MorePost

pages\posts\[slug].tsx

import { getAllPostsWithSlug, getPostAndMorePosts } from "@/apis";
import Container from "@/components/container";
import Layout from "@/components/layout";
import PostBody from "@/components/post-body";
import PostHeader from "@/components/post-header";
import { CMS_NAME } from "@/constants";
import { GetStaticPaths, GetStaticProps } from "next";
import Tags from "@/components/tags";
import Head from "next/head";
import { useRouter } from "next/router";
import MoreStories from "@/components/more-stories";
import ErrorPage from "next/error";
import PostTitle from "@/components/post-title";
export default function Post({ post, posts }: any) {
  const router = useRouter();
  const morePosts = posts?.edges;
  if (!router.isFallback && !post?.slug) {
    return <ErrorPage statusCode={404} />;
  }
  return (
    <Layout>
      <Container>
        {router.isFallback ? (<PostTitle>Loading…</PostTitle>) : (
          <>
            <article>
              <Head>
                <title>
                  {`${post.title} | Next.js Blog Example with ${CMS_NAME}`}
                </title>
                <meta
                  property="og:image"
                  content={post.featuredImage?.node.sourceUrl}
                />
              </Head>
              <PostHeader
                title={post.title}
                coverImage={post.featuredImage}
                date={post.date}
                author={post.author}
                categories={post.categories}
              />
              <PostBody content={post.content} />
              <footer>
                {post.tags.edges.length > 0 && <Tags tags={post.tags} />}
              </footer>
            </article>
            {morePosts.length > 0 && <MoreStories posts={morePosts} />}
          </>
        )}
      </Container>
    </Layout>
  )
};
export const getStaticProps: GetStaticProps = async ({ params, previewData }: any) => {
  const data: any = await getPostAndMorePosts(params?.slug, previewData);
  return {
    props: {
      post: data.post,
      posts: data.posts,
    },
    revalidate: 10,
  };
}
export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await getAllPostsWithSlug();
  return {
    paths: allPosts.edges.map(({ node }: any) => `/posts/${node.slug}`) || [],
    fallback: true,
  };
};

Step 5 Viết lại template để phù hợp cho việc làm menu

Lý do vì do bản trước không sử dụng template chung cho toàn trang giờ chuyển sang file pages_app.tsx có dạng sau:

components\layout.tsx

import Header from "./header";
import Main from "./main";
import Footer from "./footer";
import { useEffect, useState } from "react";
export default function Layout({ children }: any) {
  const [menu, setMenu] = useState([]);
  useEffect(() => {
    fetch('https://wpclidemo.dev/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: `
        {
          menu(id: 17, idType: DATABASE_ID) {
            menuItems {
              edges {
                node {
                  label
                  uri
                  url
                }
              }
            }
          }
        }
      `,
      }),
    })
    .then(res => res.json())
      .then(res => setMenu(res.data.menu.menuItems.edges))
  });
  return (
    <>
      <Header menus={menu} />
      <Main>{children}</Main>
      <Footer />
    </>
  );
}

pages\_app.tsx

import "@/styles/globals.css";
import type { AppProps } from "next/app";
import Layout from "@/components/layout";
export default function App({ Component, pageProps }: AppProps) {
  return <Layout><Component {...pageProps} /></Layout>;
}

Last updated

Was this helpful?