feat: enhance blog functionality with new components and styles

- Added styles for WordPress table and button blocks in globals.css.
- Updated layout.tsx to include a favicon.
- Improved blog page layout and search functionality, including new search and filter icons.
- Implemented related posts feature in blog post pages with new BlogBlockquote and BlogPullquote components.
- Enhanced blog card display with a new variant for smaller cards.
- Updated blog collapse sections for better user interaction.
- Added new utility functions for extracting block data from WordPress content.
This commit is contained in:
2026-02-05 17:58:28 +03:30
parent bfb967fb43
commit aa6f3c9b35
13 changed files with 386 additions and 69 deletions
+81 -8
View File
@@ -12,7 +12,7 @@ import type { Element } from "domhandler";
// ─── انواع بلاک (برای گسترش بعدی) ───────────────────────────────────────────
/** نام نوع بلاک؛ با افزودن بلاک جدید این یونیون را گسترش دهید. */
export type BlockType = "collapse" | "blockquote" | "code";
export type BlockType = "collapse" | "blockquote" | "pullquote" | "code";
/** Payload بلاک collapse (سوالات متداول / accordion). */
export interface CollapseBlockPayload {
@@ -20,14 +20,21 @@ export interface CollapseBlockPayload {
items: { question: string; answer: string }[];
}
/** Payload بلاک blockquote — بعداً پیاده‌سازی می‌شود. */
// export interface BlockquoteBlockPayload { ... }
/** Payload بلاک blockquote (نقل‌قول و منبع). content به صورت HTML است و باید با dangerouslySetInnerHTML رندر شود. */
export interface BlockquoteBlockPayload {
/** HTML محتوای نقل‌قول (بدون تگ cite). */
content: string;
citation?: string;
}
/** Payload بلاک code — بعداً پیاده‌سازی می‌شود. */
// export interface CodeBlockPayload { ... }
/** یونیون همهٔ payloadها برای تایپ‌امن رندر. */
export type BlockPayload = CollapseBlockPayload; // | BlockquoteBlockPayload | CodeBlockPayload
export type BlockPayload =
| CollapseBlockPayload
| BlockquoteBlockPayload;
// | CodeBlockPayload
// ─── سگمنت خروجی برای لایه رندر ─────────────────────────────────────────────
@@ -84,6 +91,58 @@ function extractCollapsePayload(
return { title, items };
}
/**
* استخراج از بلاک آکاردئون وردپرس (core/accordion):
* div.wp-block-accordion > div.wp-block-accordion-item با
* .wp-block-accordion-heading__toggle-title (سؤال) و .wp-block-accordion-panel (جواب به صورت HTML).
*/
function extractAccordionPayload(
$: cheerio.CheerioAPI,
el: Element
): CollapseBlockPayload {
const $el = $(el);
const items: { question: string; answer: string }[] = [];
$el.find(".wp-block-accordion-item").each((_: number, itemEl: Element) => {
const $item = $(itemEl);
const question = $item.find(".wp-block-accordion-heading__toggle-title").first().text().trim();
const $panel = $item.find(".wp-block-accordion-panel").first();
const answer = ($panel.html() ?? $panel.text() ?? "").trim();
if (question || answer) items.push({ question, answer });
});
return { title: "", items };
}
function extractBlockquotePayload(
$: cheerio.CheerioAPI,
el: Element
): BlockquoteBlockPayload {
const $el = $(el);
const citation = $el.find("cite").first().text().trim() || undefined;
const $body = $el.clone();
$body.find("cite").remove();
const content = ($body.html() ?? "").trim();
return { content, citation };
}
/**
* استخراج از بلاک pullquote وردپرس: figure.wp-block-pullquote > blockquote > p + cite.
* همان شکل blockquote (content HTML + citation).
*/
function extractPullquotePayload(
$: cheerio.CheerioAPI,
el: Element
): BlockquoteBlockPayload {
const $el = $(el);
const $blockquote = $el.find("blockquote").first();
const citation = $blockquote.find("cite").first().text().trim() || undefined;
const $body = $blockquote.clone();
$body.find("cite").remove();
const content = ($body.html() ?? "").trim();
return { content, citation };
}
/**
* ثبت همهٔ بلاک‌های شناخته‌شده.
* برای بلاک‌های جدید (مثلاً blockquote, code) فقط یک مورد جدید به این آرایه اضافه کنید.
@@ -91,11 +150,24 @@ function extractCollapsePayload(
export const BLOCK_DEFINITIONS: BlockDefinition[] = [
{
blockType: "collapse",
selector: ".collpase-section",
selector: "div.wp-block-group.collapse-section",
extract: extractCollapsePayload as BlockDefinition["extract"],
},
// بعداً:
// { blockType: "blockquote", selector: "blockquote.wp-block-quote", extract: extractBlockquotePayload },
{
blockType: "collapse",
selector: "div.wp-block-accordion",
extract: extractAccordionPayload as BlockDefinition["extract"],
},
{
blockType: "blockquote",
selector: "blockquote.wp-block-quote",
extract: extractBlockquotePayload as BlockDefinition["extract"],
},
{
blockType: "pullquote",
selector: "figure.wp-block-pullquote",
extract: extractPullquotePayload as BlockDefinition["extract"],
},
// { blockType: "code", selector: "pre.wp-block-code", extract: extractCodePayload },
];
@@ -142,11 +214,12 @@ export function discoverBlocks(html: string): DiscoveredContent {
const blocksByType: Record<string, BlockPayload[]> = {
collapse: [],
blockquote: [],
pullquote: [],
code: [],
};
for (const def of BLOCK_DEFINITIONS) {
const elements = $('.collpase-section').toArray();
const elements = $(def.selector).toArray().filter(isElement);
for (const el of elements) {
const payload = def.extract($, el) as BlockPayload;
const blockId = blocksByType[def.blockType].length;
+24
View File
@@ -132,6 +132,30 @@ export async function getPostBySlug(slug: string): Promise<WpPost | null> {
return posts.length > 0 ? posts[0] : null;
}
export async function getRelatedPosts(
postId: number,
categoryIds: number[],
limit = 3
): Promise<WpPost[]> {
const params = new URLSearchParams();
params.set("exclude", String(postId));
params.set("per_page", String(limit));
params.set("_embed", "1");
if (categoryIds.length > 0) {
params.set("categories", String(categoryIds[0]));
}
const url = `${WP_API_BASE}/posts?${params.toString()}`;
const res = await fetchWithTimeout(url);
if (!res.ok) {
throw new Error(`WP API error: ${res.status} ${res.statusText}`);
}
const posts: WpPost[] = await res.json();
return posts;
}
export async function getCategories(): Promise<WpCategory[]> {
const url = `${WP_API_BASE}/categories?per_page=100`;
const res = await fetchWithTimeout(url);