From e9136ea42b2af381616212b2d21c8e4479ea4037 Mon Sep 17 00:00:00 2001
From: hamed <15238-genius.ha@users.noreply.drupalcode.org>
Date: Fri, 19 Jun 2026 00:35:11 +0330
Subject: [PATCH] feat(blog): enhance blog data handling with normalization and
improved image management
---
app/blog/[slug]/page.js | 46 +++++++++++++---------
components/blog/detail/Caption.js | 7 ++--
components/blog/relatedContent/Item.js | 12 +++---
components/blog/relatedContent/index.js | 4 +-
components/blogs/Head.js | 42 ++++----------------
components/blogs/index.js | 5 ++-
components/blogs/latestArticles/Article.js | 6 ++-
components/blogs/title/index.js | 2 +-
helper/index.js | 22 +++++++++++
services/response.js | 2 +-
10 files changed, 77 insertions(+), 71 deletions(-)
diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js
index 12b84b3..57145d9 100644
--- a/app/blog/[slug]/page.js
+++ b/app/blog/[slug]/page.js
@@ -2,6 +2,9 @@ import BlogPage from "@/components/blog";
import Layout from "@/components/layout/StLayout";
import { fetchReq } from "@/lib/req";
import { getStateInfo } from "@/lib/getStateInfo";
+import { normalizeBlog, imageUrl } from "@/helper";
+
+const FALLBACK_IMG = "https://www.nobat724.com/assets/images/logo.png";
export async function generateMetadata({ params }) {
const { slug } = await params;
@@ -10,12 +13,16 @@ export async function generateMetadata({ params }) {
const siteName = matchedCity?.site_name || "نوبت 724";
try {
- const blog = await fetchReq(`${API_URL}/api/v1/blog/${slug}`);
+ const res = await fetchReq(`${API_URL}/api/v1/blog/${slug}`);
+ const blog = res?.data?.data;
if (!blog) return {};
const title = `${blog.title} | ${siteName}`;
- const rawText = blog.body ? blog.body.replace(/<[^>]*>/g, "").trim() : "";
+ const rawText = (blog.summary || blog.body || "")
+ .replace(/<[^>]*>/g, "")
+ .trim();
const description = rawText.slice(0, 160) || blog.title;
+ const image = blog.image_url ? imageUrl(blog.image_url) : FALLBACK_IMG;
return {
title,
@@ -24,18 +31,16 @@ export async function generateMetadata({ params }) {
title,
description,
type: "article",
- ...(blog.created && { publishedTime: blog.created }),
- images: blog.images?.[0]
- ? [blog.images[0]]
- : ["https://www.nobat724.com/assets/images/logo.png"],
+ ...(blog.created_at && {
+ publishedTime: new Date(blog.created_at * 1000).toISOString(),
+ }),
+ images: [image],
},
twitter: {
card: "summary_large_image",
title,
description,
- images: blog.images?.[0]
- ? [blog.images[0]]
- : ["https://www.nobat724.com/assets/images/logo.png"],
+ images: [image],
},
};
} catch {
@@ -47,24 +52,27 @@ async function Blog({ params }) {
const { slug } = await params;
const API_URL = process.env.NEXT_PUBLIC_API_URL;
- const blog = await fetchReq(`${API_URL}/api/v1/blog/${slug}`);
+ const res = await fetchReq(`${API_URL}/api/v1/blog/${slug}`);
+ const blog = normalizeBlog(res?.data?.data);
let relatedBlogs = [];
- if (blog?.tag?.[0]?.id) {
- const tagId = blog.tag[0].id;
- const relatedResponse = await fetchReq(
- `${API_URL}/api/v1/blogs?page=1&limit=12&tag=${tagId}`
- );
- relatedBlogs = relatedResponse?.blogs || [];
- }
+ const relatedResponse = await fetchReq(
+ `${API_URL}/api/v1/blogs?page=1&limit=6`
+ );
+ relatedBlogs = (relatedResponse?.data || [])
+ .filter((b) => b.uuid !== blog?.uuid)
+ .slice(0, 4)
+ .map(normalizeBlog);
const jsonLd = blog
? {
"@context": "https://schema.org",
"@type": "Article",
headline: blog.title,
- ...(blog.images?.[0] && { image: blog.images[0] }),
- ...(blog.created && { datePublished: blog.created }),
+ ...(blog.images?.[0]?.url && { image: imageUrl(blog.images[0].url) }),
+ ...(blog.created && {
+ datePublished: new Date(blog.created * 1000).toISOString(),
+ }),
...(blog.author && { author: { "@type": "Person", name: blog.author } }),
}
: null;
diff --git a/components/blog/detail/Caption.js b/components/blog/detail/Caption.js
index ffc0a28..4a3f3a9 100644
--- a/components/blog/detail/Caption.js
+++ b/components/blog/detail/Caption.js
@@ -1,16 +1,17 @@
import React from "react";
import Image from "next/image";
import { sanitizeHtml } from "@/lib/sanitize";
+import { imageUrl } from "@/helper";
function Caption({ data }) {
- const imageUrl = data?.images?.[0]?.url;
+ const cover = data?.images?.[0]?.url ? imageUrl(data.images[0].url) : "";
return (
<>
- {imageUrl && imageUrl.trim() !== "" && (
+ {cover && cover.trim() !== "" && (
- {imageUrl && imageUrl.trim() !== "" ? (
+ {cover && cover.trim() !== "" ? (
) : (
diff --git a/components/blog/relatedContent/index.js b/components/blog/relatedContent/index.js
index d5b60e9..9dcb08c 100644
--- a/components/blog/relatedContent/index.js
+++ b/components/blog/relatedContent/index.js
@@ -11,9 +11,9 @@ function RelatedContent({ data, loading }) {
{data &&
- data.length &&
+ data.length > 0 &&
data?.map((item, idx) => (
-
+
))}
diff --git a/components/blogs/Head.js b/components/blogs/Head.js
index 7d5481d..e904daf 100644
--- a/components/blogs/Head.js
+++ b/components/blogs/Head.js
@@ -1,39 +1,13 @@
"use client";
-import { useState, useEffect } from "react";
-import CustomLoading from "@/app/component/loading/Custom";
-import TextLoading from "@/app/component/loading/Text";
-import Link from "next/link";
-import { request } from "@/services/response";
+// نمایش «بلاگهای برتر» فعلاً غیرفعال است (endpoint /api/v1/blogs/top در بکاند وجود ندارد).
+// markup زیر برای فعالسازی بعدی نگه داشته شده؛ تا آن زمان کامپوننت چیزی رندر نمیکند.
function Head() {
- const [topBlogs, setTopBlogs] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
+ return null;
+}
- useEffect(() => {
- const fetchTopBlogs = async () => {
- try {
- setIsLoading(true);
- const response = await request.getTopBlogs();
- setTopBlogs(response || []);
- } catch (error) {
- console.error("Error fetching top blogs:", error);
- setTopBlogs([]);
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchTopBlogs();
- }, []);
-
- if (isLoading || topBlogs.length === 0) {
- return null;
- }
-
- const [firstBlog, secondBlog, thirdBlog, fourthBlog] = topBlogs;
-
- return (
- <>{/*
+/* eslint-disable */
+/*
{firstBlog && (
@@ -104,8 +78,6 @@ function Head() {
)}
- */}>
- );
-}
+*/
export default Head;
diff --git a/components/blogs/index.js b/components/blogs/index.js
index 0bb5e45..cdfab08 100644
--- a/components/blogs/index.js
+++ b/components/blogs/index.js
@@ -5,6 +5,7 @@ import Head from "./Head";
import Title from "./title";
import LatestArticles from "./latestArticles";
import { request } from "@/services/response";
+import { normalizeBlog } from "@/helper";
function BlogsPage() {
const [blogs, setBlogs] = useState([]);
@@ -25,8 +26,8 @@ function BlogsPage() {
params.tag = selectedTag;
}
const response = await request.getBlogs(params);
- setBlogs(response.blogs || []);
- setTotalPages(response.page?.totalPages || 1);
+ setBlogs((response?.data || []).map(normalizeBlog));
+ setTotalPages(response?.meta?.totalPages || 1);
} catch (error) {
console.error("Error fetching blogs:", error);
setBlogs([]);
diff --git a/components/blogs/latestArticles/Article.js b/components/blogs/latestArticles/Article.js
index c0f4849..5e76d94 100644
--- a/components/blogs/latestArticles/Article.js
+++ b/components/blogs/latestArticles/Article.js
@@ -2,10 +2,12 @@ import CustomLoading from "@/app/component/loading/Custom";
import TextLoading from "@/app/component/loading/Text";
import Image from "next/image";
import Link from "next/link";
-import { convertTimestampToJalali } from "@/helper";
+import { convertTimestampToJalali, imageUrl } from "@/helper";
function Article({ data }) {
- const imageSrc = data.images?.[0]?.url || "/assets/images/default-blog.jpg";
+ const imageSrc = data.images?.[0]?.url
+ ? imageUrl(data.images[0].url, "/assets/images/cover-blog-1.png")
+ : "/assets/images/cover-blog-1.png";
const createdDate = data.created ? convertTimestampToJalali(data.created) : "";
return (
diff --git a/components/blogs/title/index.js b/components/blogs/title/index.js
index 3b2d2d5..a212943 100644
--- a/components/blogs/title/index.js
+++ b/components/blogs/title/index.js
@@ -15,7 +15,7 @@ function Title({ onTagChange }) {
limit: 100,
};
const response = await request.getBlogTags(params);
- setTags(response.data || []);
+ setTags(response?.data?.data || []);
} catch (error) {
console.error("Error fetching tags:", error);
setTags([]);
diff --git a/helper/index.js b/helper/index.js
index 08ddbe0..27da1ab 100644
--- a/helper/index.js
+++ b/helper/index.js
@@ -12,6 +12,28 @@ export const numberToArStyle = (num) => num?.toLocaleString("ar-AE") || "";
export const isActiveURL = (link, name) => link === name;
+// نگاشت پاسخ API بلاگ (image_url/tags/created_at/body/author-object) به شکلی که
+// کامپوننتهای بلاگ انتظار دارند (images[{url}]/tag[{name}]/created/body.value/author-string).
+export const normalizeBlog = (blog) => {
+ if (!blog) return null;
+ return {
+ ...blog,
+ images: blog.image_url ? [{ url: blog.image_url }] : [],
+ tag: Array.isArray(blog.tags)
+ ? blog.tags.map((t) => (typeof t === "string" ? { name: t } : t))
+ : [],
+ created: blog.created_at ?? blog.created ?? null,
+ body:
+ blog.body && typeof blog.body === "object"
+ ? blog.body
+ : { value: blog.body ?? "" },
+ author:
+ blog.author && typeof blog.author === "object"
+ ? blog.author.name
+ : blog.author ?? null,
+ };
+};
+
// تبدیل مسیر نسبی تصویر (/uploads/...) به URL کامل backend.
// مسیرهای absolute (http...) و asset های محلی (/assets، /default) دستنخورده میمانند.
export const imageUrl = (url, fallback = "/assets/images/user.png") => {
diff --git a/services/response.js b/services/response.js
index 082e47a..fe212ef 100644
--- a/services/response.js
+++ b/services/response.js
@@ -117,7 +117,7 @@ export const request = {
},
}),
getBlogTags: (params) =>
- api.get("api/v1/categorys/tag", {
+ api.get("api/v1/tags", {
params,
headers: {
Authorization: "",