diff --git a/src/app/(main)/blog/[slug]/loading.tsx b/src/app/(main)/blog/[slug]/loading.tsx new file mode 100644 index 0000000..779bae8 --- /dev/null +++ b/src/app/(main)/blog/[slug]/loading.tsx @@ -0,0 +1,5 @@ +import LoaderFixed from "@/components/loaders/LoaderFixed"; + +export default function Loading() { + return ; +} diff --git a/src/app/(main)/blog/[slug]/page.tsx b/src/app/(main)/blog/[slug]/page.tsx index d4f4ad6..f4239d9 100644 --- a/src/app/(main)/blog/[slug]/page.tsx +++ b/src/app/(main)/blog/[slug]/page.tsx @@ -1,23 +1,89 @@ import HeroImage from "@/components/HeroImage"; +import { fetchBlogDetail } from "@/services/payload/blog"; import { getDefaultMetadata } from "@/utils/metadata"; +import { RichText } from "@payloadcms/richtext-lexical/react"; import { Metadata } from "next"; +import { headers } from "next/headers"; import Image from "next/image"; +import { notFound } from "next/navigation"; const metaDesc = "Explore the latest insights, news, and resources on the Dynamic Realty blog. Read our articles today."; -export async function generateMetadata(): Promise { +export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise { const metadata = await getDefaultMetadata(); - metadata.title = `Blog - ${metadata.openGraph?.siteName}`; - metadata.description = metaDesc; + const params = await props.params; + + let title = "Page"; + let description = "Page"; + 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 ?? ""; + } + } + + 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 function BlogDetail() { +export default async function BlogDetail(props: { params: Promise<{ slug: string }> }) { + const params = await props.params; + const headersList = await headers(); + const fullUrl = headersList.get("x-full-url"); + const blog = await fetchBlogDetail(params.slug); + if (!blog) return notFound(); + + 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}`, + }; + return ( <> - +
@@ -25,10 +91,11 @@ export default function BlogDetail() {
-

- Showcasing a warm, traditional-style exterior and the highest caliber of contemporary European - finishes throughout, towering glass doors open to grand-scale living spaces. -

+
+ {blog.img.alt} +
+ +
@@ -36,7 +103,7 @@ export default function BlogDetail() {
@@ -46,16 +113,13 @@ export default function BlogDetail() { Share this post
  • - +
  • - +
  • - -
  • -
  • - +
  • @@ -116,22 +180,18 @@ export default function BlogDetail() {
    -
    +
    -
    - +
    diff --git a/src/app/(main)/blog/page.tsx b/src/app/(main)/blog/page.tsx index f1d6f75..d0eeeda 100644 --- a/src/app/(main)/blog/page.tsx +++ b/src/app/(main)/blog/page.tsx @@ -14,12 +14,12 @@ export async function generateMetadata(): Promise { return metadata; } -export default async function Blog({ searchParams }: { searchParams?: Promise<{ s?: string }> }) { - const params = await searchParams; +export default async function Blog(props: { searchParams?: Promise<{ s?: string }> }) { + const searchParams = await props?.searchParams; return ( <> - +
    @@ -32,14 +32,14 @@ export default async function Blog({ searchParams }: { searchParams?: Promise<{ type="text" name="s" autoComplete="off" - defaultValue={params?.s} + defaultValue={searchParams?.s} />
    - +
    diff --git a/src/app/(main)/globals.css b/src/app/(main)/globals.css index b45db09..08bd10e 100644 --- a/src/app/(main)/globals.css +++ b/src/app/(main)/globals.css @@ -24,6 +24,7 @@ --color-colorHeaderText: var(--color-colorExt20); --color-colorHeaderTextHover: var(--color-colorext40); --color-colorHeroOverlay: var(--color-colorExt50); + --color-colorImgPlaceholder: var(--color-colorExt50); --color-colorFooter: var(--color-colorExt10); --color-colorFooter2: var(--color-colorExt50); --color-colorFooterText: var(--color-colorExt20); @@ -32,6 +33,7 @@ --color-colorContactForm: var(--color-colorExt50); --color-colorText1: var(--color-colorExt10); --color-colorText2: var(--color-colorExt20); + --color-colorLoaderBackground: var(--color-colorExt20); } @layer components { diff --git a/src/components/HeroImage.tsx b/src/components/HeroImage.tsx index c0627e8..583bad4 100644 --- a/src/components/HeroImage.tsx +++ b/src/components/HeroImage.tsx @@ -1,10 +1,11 @@ import Image from "next/image"; type HeroImageProps = { + title?: string; imgSrc?: string; }; -export default function HeroImage({ imgSrc = "/images/breadcrumbs-bg-05-1922x441.jpg" }: HeroImageProps) { +export default function HeroImage({ title = "", imgSrc = "/images/breadcrumbs-bg-05-1922x441.jpg" }: HeroImageProps) { return (
    -

    Blog

    +

    {title}

    ); diff --git a/src/components/blogs/CardBlog.tsx b/src/components/blogs/CardBlog.tsx index 6d8e299..52e7afa 100644 --- a/src/components/blogs/CardBlog.tsx +++ b/src/components/blogs/CardBlog.tsx @@ -7,18 +7,20 @@ type CardBlogProps = { }; export default function CardBlog({ data }: CardBlogProps) { + const linkDetail = `/blog/${data.slug}`; + return (
    - + {data.img?.alt

    - {data.title} + {data.title}

    @@ -27,7 +29,7 @@ export default function CardBlog({ data }: CardBlogProps) {
    - {data.posted_at} + {data.posted_at}
    diff --git a/src/components/blogs/ListOfBlog.tsx b/src/components/blogs/ListOfBlog.tsx index f979e1d..12a2da8 100644 --- a/src/components/blogs/ListOfBlog.tsx +++ b/src/components/blogs/ListOfBlog.tsx @@ -1,6 +1,6 @@ "use client"; -import Loader from "@/components/Loader"; +import Loader from "@/components/loaders/Loader"; import { useBlogQuery } from "@/services/hooks/blog"; import { useEffect, useRef } from "react"; import CardBlog from "./CardBlog"; diff --git a/src/components/Loader.tsx b/src/components/loaders/Loader.tsx similarity index 100% rename from src/components/Loader.tsx rename to src/components/loaders/Loader.tsx diff --git a/src/components/loaders/LoaderFixed.tsx b/src/components/loaders/LoaderFixed.tsx new file mode 100644 index 0000000..6d3911e --- /dev/null +++ b/src/components/loaders/LoaderFixed.tsx @@ -0,0 +1,9 @@ +import Loader from "./Loader"; + +export default function LoaderFixed() { + return ( +
    + +
    + ); +} diff --git a/src/services/payload/blog.ts b/src/services/payload/blog.ts index bd4696d..4e57278 100644 --- a/src/services/payload/blog.ts +++ b/src/services/payload/blog.ts @@ -1,7 +1,9 @@ import payloadConfig from "@/payload.config"; +import { BlogData } from "@/schema/blog"; import { FetchBlogParams } from "@/schema/services/blog"; import { formatDate } from "@/utils/datetime"; import { getRandomNumber } from "@/utils/general"; +import { sanitizeBlogContentIntoStringPreview } from "@/utils/sanitize"; import { getPayload, Where } from "payload"; export async function fetchBlog({ page, search = "", categoryId, tagId }: FetchBlogParams = {}) { @@ -71,11 +73,13 @@ export async function fetchBlogSuggestion() { limit: limitPerPage, }); - const formattedData = blogDataQuery.docs.map((item) => { + const formattedData: BlogData[] = blogDataQuery.docs.map((item) => { return { - ...item, - imgFormatted: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined, - createdAtFormatted: formatDate(item.createdAt), + slug: item.slug, + title: item.title, + description: sanitizeBlogContentIntoStringPreview(item.content), + img: typeof item.img !== "number" ? { url: item?.img?.url ?? "", alt: item.img.alt } : undefined, + posted_at: formatDate(item.createdAt), }; }); @@ -102,13 +106,16 @@ export async function fetchBlogDetail(slug: string | undefined) { const data = blogDataQuery?.docs?.[0]; const createdAt = formatDate(data.createdAt); const updatedAt = formatDate(data.updatedAt); - const imgUrl = typeof data.img !== "number" ? (data?.img?.url ?? "") : ""; + const img = { + url: typeof data.img !== "number" ? (data?.img?.url ?? "") : "", + alt: typeof data.img !== "number" ? (data?.img?.alt ?? "") : "", + }; return { data, createdAt, updatedAt, - imgUrl, + img, }; } diff --git a/src/utils/datetime.ts b/src/utils/datetime.ts index f48879e..a7599ca 100644 --- a/src/utils/datetime.ts +++ b/src/utils/datetime.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -export function formatDate(iso: string, format: string = "MMM, D YYYY") { +export function formatDate(iso: string, format: string = "MMMM DD, YYYY") { return dayjs(iso).format(format); }