feat: single page payload integration fetching from db

This commit is contained in:
RizqiSyahrendra 2025-04-28 21:21:22 +07:00
parent 5240b07145
commit 06057b51db
10 changed files with 391 additions and 155 deletions

View File

@ -0,0 +1,5 @@
import LoaderFixed from "@/components/loaders/LoaderFixed";
export default function Loading() {
return <LoaderFixed />;
}

View File

@ -0,0 +1,111 @@
import DetailPageBlog from "@/components/blogs/DetailPageBlog";
import DetailPage from "@/components/pages/DetailPage";
import { fetchBlogDetail } from "@/services/payload/blog";
import { fetchPageBySlug } from "@/services/payload/page";
import { getDefaultMetadata } from "@/utils/metadata";
import { Metadata } from "next";
import { headers } from "next/headers";
import { notFound } from "next/navigation";
export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const metadata = await getDefaultMetadata();
const params = await props.params;
let title = `Page Not Found - ${metadata.openGraph?.siteName}`;
let description = title;
let publishedAt = "";
let updatedAt = "";
let imgUrl = "";
let createdByName = "";
let canonicalUrl = "";
const blog = await fetchBlogDetail(params.slug);
if (!!blog) {
// check for blog data
title = `${!!blog.data?.meta?.title ? blog.data?.meta?.title : blog.data.title} - ${metadata.openGraph?.siteName}`;
description = `${!!blog.data?.meta?.description ? blog.data?.meta?.description : blog.data.title}`;
imgUrl = blog.img.url;
publishedAt = blog.data.createdAt;
updatedAt = blog.data.updatedAt;
if (!!blog.data?.meta?.canonical_url) {
canonicalUrl = blog.data.meta.canonical_url;
}
if (!!blog?.data?.createdBy && typeof blog.data.createdBy !== "number") {
createdByName = blog.data.createdBy?.name ?? "";
}
} else {
// check for page data when blog is not found
const page = await fetchPageBySlug({ slug: params.slug });
if (!!page) {
title = `${!!page?.data?.meta?.title ? page?.data?.meta?.title : page.data.title} - ${metadata.openGraph?.siteName}`;
description = `${!!page?.data?.meta?.description ? page?.data?.meta?.description : page.data.title}`;
imgUrl = page.heroImg?.url;
publishedAt = page.createdAt;
updatedAt = page.updatedAt;
if (!!page.data?.meta?.canonical_url) {
canonicalUrl = page.data.meta.canonical_url;
}
if (!!page?.data?.createdBy && typeof page?.data?.createdBy !== "number") {
createdByName = page?.data?.createdBy?.name ?? "";
}
}
}
metadata.title = title;
metadata.description = description;
if (!!metadata.openGraph) {
// @ts-ignore
metadata.openGraph.type = "article";
metadata.openGraph.title = title;
metadata.openGraph.description = description;
metadata.openGraph.images = !!imgUrl ? [imgUrl] : undefined;
}
if (!!metadata.alternates && !!canonicalUrl) {
metadata.alternates.canonical = canonicalUrl;
}
metadata.twitter = {
card: "summary_large_image",
title: title,
description: description,
images: !!imgUrl ? [imgUrl] : undefined,
};
metadata.other = {
"article:published_time": publishedAt,
"article:modified_time": updatedAt,
"twitter:label1": "Written by",
"twitter:data1": !!createdByName ? createdByName : "Admin",
"twitter:label2": "Est. reading time",
"twitter:data2": "3 minutes",
};
return metadata;
}
export default async function SinglePage(props: { params: Promise<{ slug: string }> }) {
const params = await props.params;
const headersList = await headers();
const fullUrl = headersList.get("x-full-url");
const shareUrl = {
facebook: `https://www.facebook.com/sharer/sharer.php?u=${fullUrl}`,
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${fullUrl}`,
twitter: `https://twitter.com/intent/tweet?url=${fullUrl}`,
};
const blog = await fetchBlogDetail(params.slug);
if (!!blog) {
return (
<>
<DetailPageBlog data={blog} shareUrl={shareUrl} />
</>
);
}
const page = await fetchPageBySlug({ slug: params.slug });
if (!page) return notFound();
return (
<>
<DetailPage data={page} shareUrl={shareUrl} />
</>
);
}

View File

@ -1,19 +1,16 @@
import ListOfRecentBlog from "@/components/blogs/ListOfRecentBlog"; import DetailPageBlog from "@/components/blogs/DetailPageBlog";
import HeroImage from "@/components/HeroImage";
import { fetchBlogDetail } from "@/services/payload/blog"; import { fetchBlogDetail } from "@/services/payload/blog";
import { getDefaultMetadata } from "@/utils/metadata"; import { getDefaultMetadata } from "@/utils/metadata";
import { RichText } from "@payloadcms/richtext-lexical/react";
import { Metadata } from "next"; import { Metadata } from "next";
import { headers } from "next/headers"; import { headers } from "next/headers";
import Image from "next/image";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> { export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const metadata = await getDefaultMetadata(); const metadata = await getDefaultMetadata();
const params = await props.params; const params = await props.params;
let title = "Page"; let title = `Page Not Found - ${metadata.openGraph?.siteName}`;
let description = "Page"; let description = title;
let publishedAt = ""; let publishedAt = "";
let updatedAt = ""; let updatedAt = "";
let imgUrl = ""; let imgUrl = "";
@ -81,135 +78,7 @@ export default async function BlogDetail(props: { params: Promise<{ slug: string
return ( return (
<> <>
<HeroImage title={blog.data.title} /> <DetailPageBlog data={blog} shareUrl={shareUrl} />
<section className="section section-md bg-colorSection1">
<div className="container">
<div className="row justify-content-lg-center">
<div className="col-lg-8">
<article className="blog-post-solo">
<div className="blog-post-solo-part">
<div className="w-full h-56 md:h-80 bg-colorImgPlaceholder/50 relative">
<Image className="object-cover" src={blog.img.url} alt={blog.img.alt} fill />
</div>
<RichText className="mt-4" data={blog.data.content} />
</div>
<div className="blog-post-solo-footer">
<div className="blog-post-solo-footer-left">
<ul className="blog-post-solo-footer-list">
<li>
<span className="icon mdi mdi-clock"></span>
<a href="#">{blog.createdAt}</a>
</li>
</ul>
</div>
<div className="blog-post-solo-footer-right">
<ul className="blog-post-solo-footer-list-1">
<li>
<span>Share this post</span>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-5 fa-facebook"
href={shareUrl.facebook}
></a>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-6 fa-twitter"
href={shareUrl.twitter}
></a>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-4 fa-linkedin"
href={shareUrl.linkedin}
></a>
</li>
</ul>
</div>
</div>
<ListOfRecentBlog currentBlogId={blog?.data?.id} />
</article>
</div>
{/* Sidebar */}
<div className="col-lg-4">
<div className="pdl-xl-40">
<div className="row row-60">
<div className="col-md-6 col-lg-12">
<form action="/blog" className="form-lg rd-search rd-search-classic">
<div className="form-wrap">
<input
className="form-input"
id="rd-search-form-input"
type="text"
name="s"
autoComplete="off"
placeholder="Search blog..."
/>
</div>
<button className="rd-search-submit" type="submit"></button>
</form>
</div>
<div className="col-md-6 col-lg-12">
<div className="block-info-2">
<div className="block-info-2-title">
<h3>Latest Listings</h3>
</div>
<a className="post-minimal-1" href="#">
<div className="post-minimal-1-left">
<Image src="/images/post-agent-01-212x208.jpg" alt="" width="212" height="208" />
</div>
<div className="post-minimal-1-body">
<div className="post-minimal-1-title">
<span>401 Biscayne Blvd</span>
</div>
<div className="post-minimal-1-text">
<span>$5000\mo</span>
</div>
</div>
</a>
<a className="post-minimal-1" href="#">
<div className="post-minimal-1-left">
<Image src="/images/post-agent-02-212x208.jpg" alt="" width="212" height="208" />
</div>
<div className="post-minimal-1-body">
<div className="post-minimal-1-title">
<span>35 Pond St, New York</span>
</div>
<div className="post-minimal-1-text">
<span>$5550\mo</span>
</div>
</div>
</a>
<a className="post-minimal-1" href="#">
<div className="post-minimal-1-left">
<Image src="/images/post-agent-03-212x208.jpg" alt="" width="212" height="208" />
</div>
<div className="post-minimal-1-body">
<div className="post-minimal-1-title">
<span>182 3rd St, Seattle</span>
</div>
<div className="post-minimal-1-text">
<span>$2520\mo</span>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</> </>
); );
} }

View File

@ -7,7 +7,7 @@ type HeroImageProps = {
export default function HeroImage({ title = "", imgSrc = "/images/breadcrumbs-bg-05-1922x441.jpg" }: HeroImageProps) { export default function HeroImage({ title = "", imgSrc = "/images/breadcrumbs-bg-05-1922x441.jpg" }: HeroImageProps) {
return ( return (
<section className="breadcrumbs-custom bg-image context-dark"> <section className="breadcrumbs-custom context-dark min-h-[300px]!">
<Image <Image
alt="Blog" alt="Blog"
src={imgSrc} src={imgSrc}

View File

@ -4,23 +4,9 @@ import { RichText } from "@payloadcms/richtext-lexical/react";
export function ContentBlock(props: any) { export function ContentBlock(props: any) {
return ( return (
<div className="container relative"> <div>
<div className="row"> {/* @ts-ignore */}
{/* Content */} <RichText data={props.content} />
<div className="col-md-10 offset-md-1 col-lg-10 offset-lg-1">
{/* Post */}
<div className="blog-item mb-10">
<div className="blog-item-body">
<div>
{/* @ts-ignore */}
<RichText data={props.content} />
</div>
</div>
</div>
{/* End Post */}
</div>
{/* End Content */}
</div>
</div> </div>
); );
} }

View File

@ -9,7 +9,7 @@ type CardBlogProps = {
}; };
export default function CardBlog({ data, colorPreset = 1, isDescriptionVisible = true }: CardBlogProps) { export default function CardBlog({ data, colorPreset = 1, isDescriptionVisible = true }: CardBlogProps) {
const linkDetail = `/blog/${data.slug}`; const linkDetail = `/${data.slug}`;
if (colorPreset === 2) { if (colorPreset === 2) {
return ( return (

View File

@ -0,0 +1,151 @@
import { fetchBlogDetail } from "@/services/payload/blog";
import Image from "next/image";
import ListOfRecentBlog from "./ListOfRecentBlog";
import HeroImage from "../HeroImage";
import { RichText } from "@payloadcms/richtext-lexical/react";
type shareUrlDestination = "facebook" | "linkedin" | "twitter";
type DetailPageBlogProps = {
data: Awaited<ReturnType<typeof fetchBlogDetail>>;
shareUrl: Record<shareUrlDestination, string>;
};
export default function DetailPageBlog({ data, shareUrl }: DetailPageBlogProps) {
const blog = data;
if (!blog) return <></>;
return (
<>
<HeroImage title={blog.data.title} />
<section className="section section-md bg-colorSection1">
<div className="container">
<div className="row justify-content-lg-center">
<div className="col-lg-8">
<article className="blog-post-solo">
<div className="blog-post-solo-part">
<div className="w-full h-56 md:h-80 bg-colorImgPlaceholder/50 relative">
<Image className="object-cover" src={blog.img.url} alt={blog.img.alt} fill />
</div>
<RichText className="mt-4" data={blog.data.content} />
</div>
<div className="blog-post-solo-footer">
<div className="blog-post-solo-footer-left">
<ul className="blog-post-solo-footer-list">
<li>
<span className="icon mdi mdi-clock"></span>
<a href="#">{blog.createdAt}</a>
</li>
</ul>
</div>
<div className="blog-post-solo-footer-right">
<ul className="blog-post-solo-footer-list-1">
<li>
<span>Share this post</span>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-5 fa-facebook"
href={shareUrl.facebook}
></a>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-6 fa-twitter"
href={shareUrl.twitter}
></a>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-4 fa-linkedin"
href={shareUrl.linkedin}
></a>
</li>
</ul>
</div>
</div>
<ListOfRecentBlog currentBlogId={blog?.data?.id} />
</article>
</div>
{/* Sidebar */}
<div className="col-lg-4">
<div className="pdl-xl-40">
<div className="row row-60">
<div className="col-md-6 col-lg-12">
<form action="/blog" className="form-lg rd-search rd-search-classic">
<div className="form-wrap">
<input
className="form-input"
id="rd-search-form-input"
type="text"
name="s"
autoComplete="off"
placeholder="Search blog..."
/>
</div>
<button className="rd-search-submit" type="submit"></button>
</form>
</div>
<div className="col-md-6 col-lg-12">
<div className="block-info-2">
<div className="block-info-2-title">
<h3>Latest Listings</h3>
</div>
<a className="post-minimal-1" href="#">
<div className="post-minimal-1-left">
<Image src="/images/post-agent-01-212x208.jpg" alt="" width="212" height="208" />
</div>
<div className="post-minimal-1-body">
<div className="post-minimal-1-title">
<span>401 Biscayne Blvd</span>
</div>
<div className="post-minimal-1-text">
<span>$5000\mo</span>
</div>
</div>
</a>
<a className="post-minimal-1" href="#">
<div className="post-minimal-1-left">
<Image src="/images/post-agent-02-212x208.jpg" alt="" width="212" height="208" />
</div>
<div className="post-minimal-1-body">
<div className="post-minimal-1-title">
<span>35 Pond St, New York</span>
</div>
<div className="post-minimal-1-text">
<span>$5550\mo</span>
</div>
</div>
</a>
<a className="post-minimal-1" href="#">
<div className="post-minimal-1-left">
<Image src="/images/post-agent-03-212x208.jpg" alt="" width="212" height="208" />
</div>
<div className="post-minimal-1-body">
<div className="post-minimal-1-title">
<span>182 3rd St, Seattle</span>
</div>
<div className="post-minimal-1-text">
<span>$2520\mo</span>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</>
);
}

View File

@ -105,7 +105,7 @@ export default async function Footer() {
<span>Dynamic Realty</span> <span>&copy;&nbsp;</span> <span>Dynamic Realty</span> <span>&copy;&nbsp;</span>
<span className="copyright-year"></span> <span className="copyright-year"></span>
<span>&nbsp;</span> <span>&nbsp;</span>
<a href="privacy-policy.html">Privacy Policy</a> <a href="/privacy-policy">Privacy Policy</a>
</p> </p>
</div> </div>
<div className="col-sm-6 text-sm-right"> <div className="col-sm-6 text-sm-right">

View File

@ -0,0 +1,74 @@
import { fetchPageBySlug } from "@/services/payload/page";
import HeroImage from "../HeroImage";
import { RenderBlocks } from "../blocks/RenderBlocks";
type shareUrlDestination = "facebook" | "linkedin" | "twitter";
type DetailPageProps = {
data: Awaited<ReturnType<typeof fetchPageBySlug>>;
shareUrl: Record<shareUrlDestination, string>;
};
export default function DetailPage({ data, shareUrl }: DetailPageProps) {
const page = data;
if (!page) return <></>;
return (
<>
<HeroImage imgSrc={page.heroImg?.url} title={page.data.title} />
<section className="section section-md bg-colorSection1">
<div className="container">
<div className="row justify-content-lg-center">
<div className="col-12">
<article className="blog-post-solo">
<div className="blog-post-solo-part">
<RenderBlocks blocks={page.data.layout} />
</div>
<div className="blog-post-solo-footer">
<div className="blog-post-solo-footer-left">
<ul className="blog-post-solo-footer-list">
<li>
<span className="icon mdi mdi-clock"></span>
<a href="#">{page.createdAt}</a>
</li>
</ul>
</div>
<div className="blog-post-solo-footer-right">
<ul className="blog-post-solo-footer-list-1">
<li>
<span>Share this post</span>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-5 fa-facebook"
href={shareUrl.facebook}
></a>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-6 fa-twitter"
href={shareUrl.twitter}
></a>
</li>
<li>
<a
target="_blank"
className="icon icon-circle icon-rounded icon-4 fa-linkedin"
href={shareUrl.linkedin}
></a>
</li>
</ul>
</div>
</div>
</article>
</div>
</div>
</div>
</section>
</>
);
}

View File

@ -0,0 +1,40 @@
import payloadConfig from "@/payload.config";
import { formatDate } from "@/utils/datetime";
import { getPayload } from "payload";
export const fetchPageBySlug = async ({ slug }: { slug: string | undefined }) => {
const payload = await getPayload({ config: payloadConfig });
const result = await payload.find({
collection: "pages",
// draft,
limit: 1,
pagination: false,
// overrideAccess: draft,
where: {
_status: { equals: "published" },
slug: {
equals: slug,
},
},
});
if (!result.docs?.[0]) {
return null;
}
const data = result.docs[0];
const heroImgUrl = typeof data.hero_img !== "number" ? (data?.hero_img?.url ?? "") : "";
const heroImgAlt = typeof data.hero_img !== "number" ? (data?.hero_img?.alt ?? "") : "";
return {
data: data,
createdAt: formatDate(data.createdAt),
updatedAt: formatDate(data.updatedAt),
heroImg: {
url: heroImgUrl,
alt: heroImgAlt,
},
};
};