Inject CMS meta tags as full HTML from business-info.

metaTags is now { html }; parse into head <meta> elements instead of name/content metadata.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-03 18:39:21 +03:30
co-authored by Cursor
parent f60c98184b
commit 7ea3fe76c9
2 changed files with 32 additions and 8 deletions
+15 -6
View File
@@ -6,7 +6,12 @@ import { TopBar } from "@/components/home/TopBar";
import { Header } from "@/components/home/Header"; import { Header } from "@/components/home/Header";
import { Footer } from "@/components/home/Footer"; import { Footer } from "@/components/home/Footer";
import { Providers } from "@/context/Providers"; import { Providers } from "@/context/Providers";
import { getBusinessInfo, getCategories, getCategoryTree } from "@/lib/meshkee"; import {
getBusinessInfo,
getCategories,
getCategoryTree,
parseMetaTagHtml,
} from "@/lib/meshkee";
// Body text — headings use the bold weight instead, see globals.css. // Body text — headings use the bold weight instead, see globals.css.
const iranyekan = localFont({ const iranyekan = localFont({
@@ -29,16 +34,11 @@ const syne = Syne({
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const businessInfo = await getBusinessInfo(); const businessInfo = await getBusinessInfo();
const other: Record<string, string> = {};
for (const tag of businessInfo?.metaTags ?? []) {
if (tag.name && tag.content) other[tag.name] = tag.content;
}
return { return {
title: "صنیع هوم | فروشگاه لوازم خانگی", title: "صنیع هوم | فروشگاه لوازم خانگی",
description: "فروشگاه آنلاین لوازم خانگی — یخچال، لباسشویی، تلویزیون و بیشتر", description: "فروشگاه آنلاین لوازم خانگی — یخچال، لباسشویی، تلویزیون و بیشتر",
icons: businessInfo?.faviconUrl ? [{ url: businessInfo.faviconUrl }] : undefined, icons: businessInfo?.faviconUrl ? [{ url: businessInfo.faviconUrl }] : undefined,
...(Object.keys(other).length > 0 ? { other } : {}),
}; };
} }
@@ -49,12 +49,21 @@ export default async function RootLayout({ children }: LayoutProps<"/">) {
getBusinessInfo(), getBusinessInfo(),
]); ]);
const cmsMetaTags = (businessInfo?.metaTags ?? [])
.map((tag, index) => {
const attrs = parseMetaTagHtml(tag.html ?? "");
if (!attrs) return null;
return <meta key={`cms-meta-${index}`} {...attrs} />;
})
.filter(Boolean);
return ( return (
<html <html
lang="fa" lang="fa"
dir="rtl" dir="rtl"
className={`${iranyekan.variable} ${iranyekanBold.variable} ${syne.variable} h-full antialiased`} className={`${iranyekan.variable} ${iranyekanBold.variable} ${syne.variable} h-full antialiased`}
> >
{cmsMetaTags.length > 0 ? <head>{cmsMetaTags}</head> : null}
<body className="min-h-full flex flex-col"> <body className="min-h-full flex flex-col">
<Providers> <Providers>
<TopBar /> <TopBar />
+17 -2
View File
@@ -44,11 +44,26 @@ async function apiPost<T>(
} }
} }
/** Full sanitized `<meta …>` HTML from business-info. */
export type BusinessMetaTag = { export type BusinessMetaTag = {
name: string; html: string;
content: string;
}; };
/** Parse a single `<meta …>` tag into React/Next attribute props. */
export function parseMetaTagHtml(html: string): Record<string, string> | null {
const match = html.trim().match(/^<meta\b([^>]*)\/?\s*>$/i);
if (!match) return null;
const attrs: Record<string, string> = {};
const attrRe = /([:\w.-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
let m: RegExpExecArray | null;
while ((m = attrRe.exec(match[1]))) {
const key = m[1].toLowerCase();
if (key === '/') continue;
attrs[key] = m[2] ?? m[3] ?? m[4] ?? '';
}
return Object.keys(attrs).length > 0 ? attrs : null;
}
export type BusinessTrustBadge = { export type BusinessTrustBadge = {
kind: "enamad" | "samandehi"; kind: "enamad" | "samandehi";
embedHtml: string; embedHtml: string;