Files
aragrp/lib/blog-utils.ts
T
alirez.br ff2632f8db feat: enhance image handling and layout adjustments
- Added image quality settings and device sizes in next.config.ts for better image optimization.
- Updated package dependencies, including cheerio and baseline-browser-mapping.
- Improved global styles for blog content images to ensure responsiveness.
- Refactored button components for consistent sizing and styling across the application.
- Enhanced blog post card layout with new variants and improved author name formatting.
- Adjusted hero section and content layout for better visual appeal and user experience.
2026-06-17 21:35:35 +03:30

126 lines
4.0 KiB
TypeScript

import {
discoverBlocks,
mergeHeadingIdsIntoContentSegments,
type ContentSegment,
} from "@/lib/blog-blocks";
import { getFullSizeWpImageUrl } from "@/lib/wp-api";
export type { ContentSegment } from "@/lib/blog-blocks";
const AUTHOR_NAME_OVERRIDES: Record<string, string> = {
adm: "احمدرضا بدیعی",
};
/** Maps WordPress author display names to site-facing labels. */
export function formatAuthorName(name: string | null | undefined): string | null {
if (!name?.trim()) return null;
const normalized = name.trim().toLowerCase();
return AUTHOR_NAME_OVERRIDES[normalized] ?? name.trim();
}
/**
* Creates a URL-safe slug from heading text (strip HTML, normalize, Persian-friendly).
*/
function slugify(text: string): string {
const stripped = text.replace(/<[^>]*>/g, "").trim();
return stripped
.replace(/\s+/g, "-")
.replace(/[^\p{L}\p{N}-]/gu, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase() || "section";
}
export interface TocItem {
id: string;
text: string;
level: number; // 2 = h2, 3 = h3, 4 = h4
}
const HEADING_REGEX = /<(h[234])[^>]*>([\s\S]*?)<\/\1>/gi;
const usedIds = new Map<string, number>();
function ensureUniqueId(baseId: string): string {
const count = usedIds.get(baseId) ?? 0;
usedIds.set(baseId, count + 1);
return count === 0 ? baseId : `${baseId}-${count}`;
}
/**
* پارس محتوا: ابتدا بلاک‌ها کشف و به JSON تبدیل می‌شوند (blog-blocks)،
* سپس id سرنویس‌ها تزریق می‌شود و سگمنت‌های نهایی برای رندر برمی‌گردند.
*/
export function parseContentHeadings(html: string): {
contentWithIds: string;
toc: TocItem[];
segments: ContentSegment[];
} {
const { htmlWithPlaceholders, segments: rawSegments } = discoverBlocks(
enhanceContentImages(html)
);
usedIds.clear();
const toc: TocItem[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
const parts: string[] = [];
const regex = new RegExp(HEADING_REGEX.source, "gi");
while ((match = regex.exec(htmlWithPlaceholders)) !== null) {
const [fullMatch, tag, inner] = match;
const level = parseInt(tag.charAt(1), 10) as 2 | 3 | 4;
const text = inner.replace(/<[^>]*>/g, "").trim();
const baseId = slugify(text);
const id = ensureUniqueId(baseId);
toc.push({ id, text, level });
const openTag = fullMatch.slice(0, fullMatch.indexOf(">") + 1);
const hasId = /^\s*<h[234][^>]*\bid\s*=/i.test(openTag);
const newOpenTag = hasId
? openTag
: openTag.replace(/^(<\s*h[234])(\s|>)/i, `$1 id="${id}"$2`);
parts.push(htmlWithPlaceholders.slice(lastIndex, match.index));
parts.push(newOpenTag + inner + `</${tag}>`);
lastIndex = regex.lastIndex;
}
parts.push(htmlWithPlaceholders.slice(lastIndex));
const contentWithIds = parts.join("");
const segments = mergeHeadingIdsIntoContentSegments(
contentWithIds,
rawSegments
);
return { contentWithIds, toc, segments };
}
/** Average reading speed in words per minute (Persian/English mix). */
const WPM = 200;
/**
* Estimates reading time in minutes from HTML content.
*/
export function getReadingTimeMinutes(html: string): number {
const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
const wordCount = text.split(/\s+/).filter(Boolean).length;
return Math.max(1, Math.ceil(wordCount / WPM));
}
/** Use full-size WordPress uploads and drop responsive srcset that picks smaller variants. */
export function enhanceContentImages(html: string): string {
return html.replace(/<img\b([^>]*)>/gi, (tag, attrs: string) => {
let next = attrs
.replace(/\s+srcset=(["'])[^"']*\1/gi, "")
.replace(/\s+sizes=(["'])[^"']*\1/gi, "")
.replace(/\s+src=(["'])([^"']+)\1/gi, (_m, quote: string, src: string) => {
return ` src=${quote}${getFullSizeWpImageUrl(src)}${quote}`;
});
if (!/\bloading=/i.test(next)) next += ' loading="lazy"';
if (!/\bdecoding=/i.test(next)) next += ' decoding="async"';
return `<img${next}>`;
});
}