😍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?