Add Meshkee sitemap config, technical-info labels, and footer credit.

Wire specs to /technical-info for field labels, match Safeteb footer credit, and publish build-time /meshkee/sitemap-config.json for SEO sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-27 07:11:52 +03:30
co-authored by Cursor
parent 386787b4b5
commit d249d958eb
15 changed files with 590 additions and 108 deletions
+3 -1
View File
@@ -3,10 +3,12 @@
"version": "0.1.0",
"private": true,
"scripts": {
"prebuild": "node scripts/generate-sitemap-config.mjs",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"generate:sitemap-config": "node scripts/generate-sitemap-config.mjs"
},
"dependencies": {
"next": "16.3.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+111
View File
@@ -0,0 +1,111 @@
/**
* Scans app page routes and writes src/lib/sitemap-config.generated.json
* for GET /meshkee/sitemap-config.json (Meshkee sitemap sync).
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const appDir = path.join(root, "src", "app");
const outFile = path.join(root, "src", "lib", "sitemap-config.generated.json");
const EXCLUDE_SEGMENTS = new Set([
"api",
"meshkee",
"admin",
"auth",
"login",
"checkout",
"cart",
"account",
]);
const PRIORITY = {
"/": { changefreq: "daily", priority: 1.0 },
"/user-products": { changefreq: "daily", priority: 0.9 },
"/categories": { changefreq: "weekly", priority: 0.8 },
"/blog": { changefreq: "weekly", priority: 0.8 },
"/about": { changefreq: "monthly", priority: 0.6 },
"/contact": { changefreq: "monthly", priority: 0.5 },
};
function websiteDomain() {
return (
process.env.NEXT_PUBLIC_SITE_DOMAIN ||
process.env.NEXT_PUBLIC_WEBSITE_DOMAIN ||
process.env.WEBSITE_DOMAIN ||
"www.mashinify.com"
);
}
function walkPages(dir, segments = []) {
/** @type {string[]} */
const pages = [];
if (!fs.existsSync(dir)) return pages;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (EXCLUDE_SEGMENTS.has(entry.name)) continue;
if (/^\[.+\]$/.test(entry.name) || /^\(.+\)$/.test(entry.name)) {
if (/^\(.+\)$/.test(entry.name)) {
pages.push(...walkPages(full, segments));
}
continue;
}
pages.push(...walkPages(full, [...segments, entry.name]));
continue;
}
if (
entry.name === "page.tsx" ||
entry.name === "page.ts" ||
entry.name === "page.jsx" ||
entry.name === "page.js"
) {
const routePath = segments.length ? `/${segments.join("/")}` : "/";
pages.push(routePath);
}
}
return pages;
}
function main() {
const paths = [...new Set(walkPages(appDir))].sort((a, b) => {
if (a === "/") return -1;
if (b === "/") return 1;
return a.localeCompare(b);
});
const staticPages = paths.map((routePath) => {
const meta = PRIORITY[routePath] ?? {
changefreq: "monthly",
priority: 0.5,
};
return { path: routePath, ...meta };
});
const domain = websiteDomain().replace(/^https?:\/\//, "").replace(/\/+$/, "");
const config = {
baseUrl: `https://${domain}`,
staticPages,
// Mashinify public paths differ from Meshkee catalog defaults.
templates: {
userProduct: "/user-products/{slug}",
blog: "/blog/{slug}",
},
};
fs.mkdirSync(path.dirname(outFile), { recursive: true });
fs.writeFileSync(outFile, `${JSON.stringify(config, null, 2)}\n`, "utf8");
console.log(
`Wrote ${path.relative(root, outFile)} (${staticPages.length} static pages)`,
);
}
main();
+8 -1
View File
@@ -15,9 +15,16 @@ export async function generateMetadata({
const { slug } = await params;
const blog = await getBlogBySlugOrNull(slug);
if (!blog) return { title: "مقاله یافت نشد" };
const description = blog.abstract ?? blog.title;
const image = resolveBlogImage(blog);
return {
title: blog.title,
description: blog.abstract ?? blog.title,
description,
openGraph: {
title: blog.title,
description,
...(image ? { images: [{ url: image, alt: blog.title }] } : {}),
},
};
}
+161
View File
@@ -279,6 +279,163 @@ img {
border-color: rgba(255, 255, 255, 0.8);
}
/* Footer bottom — Meshkee credit (Safeteb / Havaran style) */
.footer-bottom-bar {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
padding: 4px 0;
}
.footer-bottom-site {
color: #9c9c9c;
font-size: 12px;
}
.meshkee-credit {
align-items: center;
color: #9c9c9c;
display: inline-flex;
gap: 10px;
padding: 6px 10px;
position: relative;
text-decoration: none;
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover {
color: #e0e0e0;
text-decoration: none;
}
.meshkee-credit__frame {
inset: 0;
pointer-events: none;
position: absolute;
}
.meshkee-credit__line {
background: #777;
position: absolute;
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit__line--l {
height: 100%;
left: 0;
top: 0;
transform: scaleY(0);
transform-origin: top;
transition-delay: 0s;
width: 1px;
}
.meshkee-credit__line--b {
bottom: 0;
height: 1px;
left: 0;
transform: scaleX(0);
transform-origin: left;
transition-delay: 0.14s;
width: 100%;
}
.meshkee-credit__line--r {
height: 100%;
right: 0;
top: 0;
transform: scaleY(0);
transform-origin: bottom;
transition-delay: 0.28s;
width: 1px;
}
.meshkee-credit__line--t {
height: 1px;
left: 0;
top: 0;
transform: scaleX(0);
transform-origin: right;
transition-delay: 0.42s;
width: 100%;
}
.meshkee-credit:hover .meshkee-credit__line--l,
.meshkee-credit:hover .meshkee-credit__line--r {
transform: scaleY(1);
}
.meshkee-credit:hover .meshkee-credit__line--t,
.meshkee-credit:hover .meshkee-credit__line--b {
transform: scaleX(1);
}
.meshkee-credit:not(:hover) .meshkee-credit__line--l {
transition-delay: 0.42s;
}
.meshkee-credit:not(:hover) .meshkee-credit__line--b {
transition-delay: 0.28s;
}
.meshkee-credit:not(:hover) .meshkee-credit__line--r {
transition-delay: 0.14s;
}
.meshkee-credit:not(:hover) .meshkee-credit__line--t {
transition-delay: 0s;
}
.meshkee-credit__logo {
display: block;
flex-shrink: 0;
height: 42px;
width: auto;
aspect-ratio: 1 / 1;
object-fit: contain;
opacity: 0.7;
position: relative;
transition: opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover .meshkee-credit__logo {
opacity: 1;
}
.meshkee-credit__text {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.25;
min-height: 42px;
position: relative;
}
.meshkee-credit__label {
font-size: 11px;
}
.meshkee-credit__name {
color: #cfcfcf;
font-size: 13px;
font-weight: 600;
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover .meshkee-credit__name {
color: #fff;
}
@media only screen and (max-width: 575px) {
.footer-bottom-bar {
flex-direction: column;
justify-content: center;
text-align: center;
}
}
@media (prefers-reduced-motion: reduce) {
.animate-rise,
.animate-rise-delay-1,
@@ -287,4 +444,8 @@ img {
.search-shell:focus-within {
animation: none;
}
.meshkee-credit__line {
transition: none;
}
}
@@ -0,0 +1,17 @@
import sitemapConfig from "@/lib/sitemap-config.generated.json";
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
"Cache-Control": "public, max-age=60",
"Content-Type": "application/json; charset=utf-8",
};
export async function OPTIONS() {
return new Response(null, { status: 204, headers });
}
export async function GET() {
return Response.json(sitemapConfig, { headers });
}
+20 -13
View File
@@ -7,11 +7,11 @@ import { ProductTabs } from "@/components/ProductTabs";
import { Products } from "@/components/Products";
import { SiteShell } from "@/components/SiteShell";
import { site } from "@/data/home";
import { getCategoryVariationFieldMap } from "@/lib/api/product-fields";
import {
formatCondition,
formatPrice,
getUserProductBySlugOrNull,
getUserProductTechnicalInfo,
listUserProducts,
productCategory,
productTitle,
@@ -29,9 +29,17 @@ export async function generateMetadata({
const product = await getUserProductBySlugOrNull(slug);
if (!product) return { title: "محصول یافت نشد" };
const title = productTitle(product);
const description =
product.descriptionFa || product.description || product.descriptionEn || title;
const image = product.imageUrl || product.images?.[0]?.url || undefined;
return {
title,
description: product.descriptionFa || product.description || title,
description,
openGraph: {
title,
description,
...(image ? { images: [{ url: image, alt: title }] } : {}),
},
};
}
@@ -50,16 +58,15 @@ export default async function UserProductDetailPage({ params }: PageProps) {
const description =
product.descriptionFa || product.description || product.descriptionEn;
const deliveryNotes = product.deliveryNotes ?? product.deliveryNote ?? null;
const technicalFieldLabels = await getCategoryVariationFieldMap(
product.categoryId,
);
const similarResult = product.categoryId
? await listUserProducts({
categoryId: product.categoryId,
pageSize: 5,
})
: { items: [], total: 0 };
const [technicalInfo, similarResult] = await Promise.all([
getUserProductTechnicalInfo(slug),
product.categoryId
? listUserProducts({
categoryId: product.categoryId,
pageSize: 5,
})
: Promise.resolve({ items: [], total: 0 }),
]);
const similarProducts = similarResult.items
.filter((item) => item.id !== product.id)
@@ -149,7 +156,7 @@ export default async function UserProductDetailPage({ params }: PageProps) {
technicalNotes={product.technicalNotes}
deliveryNotes={deliveryNotes}
technicalValues={product.technicalValues}
technicalFieldLabels={technicalFieldLabels}
technicalInfo={technicalInfo}
/>
{similarProducts.length > 0 ? (
+36 -5
View File
@@ -56,11 +56,42 @@ export function Footer() {
</div>
<div className="border-t border-white/10">
<div className="container-page flex flex-col items-start justify-between gap-3 py-5 text-xs text-white/50 sm:flex-row sm:items-center">
<p>
© {new Date().getFullYear()} {site.name} — {site.nameEn}
</p>
<p>پاسخگوی پرسش‌های شما هستیم</p>
<div className="container-page py-5">
<div className="footer-bottom-bar">
<div className="footer-bottom-site">
© {new Date().getFullYear()} {site.name}
</div>
<a
href="https://meshkee.com"
target="_blank"
rel="noopener noreferrer"
dir="ltr"
className="meshkee-credit"
>
<span className="meshkee-credit__frame" aria-hidden>
<span className="meshkee-credit__line meshkee-credit__line--l" />
<span className="meshkee-credit__line meshkee-credit__line--t" />
<span className="meshkee-credit__line meshkee-credit__line--r" />
<span className="meshkee-credit__line meshkee-credit__line--b" />
</span>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/footer/MeshkeeLogo-White.png"
alt="Meshkee"
className="meshkee-credit__logo"
width={42}
height={42}
/>
<span className="meshkee-credit__text">
<span className="meshkee-credit__label">
Designed And Developed By
</span>
<span className="meshkee-credit__name">
Meshkee E-Commerce Team
</span>
</span>
</a>
</div>
</div>
</div>
</footer>
+1 -1
View File
@@ -11,7 +11,7 @@ export function PageBanner({ kicker, title, description }: PageBannerProps) {
<section className="relative overflow-hidden bg-brand-ink text-white">
<Image
src="/images/hero/search-bg.jpg"
alt=""
alt={`بنر ${title}`}
fill
priority
className="object-cover object-[center_40%]"
+34 -17
View File
@@ -3,13 +3,15 @@
import { useMemo, useState } from "react";
import { site } from "@/data/home";
import { TechnicalValuesTable } from "@/components/TechnicalValuesTable";
import type { UserProductTechnicalInfo } from "@/lib/api/user-products";
import { rowsFromTechnicalInfo } from "@/lib/product-technical-values";
type ProductTabsProps = {
description?: string | null;
technicalNotes?: string | null;
deliveryNotes?: string | null;
technicalValues?: unknown;
technicalFieldLabels?: Record<string, string>;
technicalInfo?: UserProductTechnicalInfo | null;
};
type TabId = "description" | "technical" | "comments";
@@ -23,13 +25,28 @@ const tabMeta: Record<
comments: { label: "نظرات", labelEn: "Comments" },
};
function hasTechnicalValues(value: unknown) {
if (value === null || value === undefined) return false;
if (Array.isArray(value)) return value.length > 0;
if (typeof value === "object") {
return Object.keys(value as object).length > 0;
}
if (typeof value === "string") return Boolean(value.trim());
return true;
}
export function ProductTabs({
description,
technicalNotes,
deliveryNotes,
technicalValues,
technicalFieldLabels = {},
technicalInfo = null,
}: ProductTabsProps) {
const technicalRows = useMemo(
() => rowsFromTechnicalInfo(technicalInfo),
[technicalInfo],
);
const tabs = useMemo(() => {
const items: { id: TabId; label: string; labelEn: string }[] = [];
@@ -40,12 +57,8 @@ export function ProductTabs({
const hasTechnical =
Boolean(technicalNotes?.trim()) ||
Boolean(deliveryNotes?.trim()) ||
(technicalValues !== null &&
technicalValues !== undefined &&
!(typeof technicalValues === "object" &&
!Array.isArray(technicalValues) &&
Object.keys(technicalValues as object).length === 0) &&
!(Array.isArray(technicalValues) && technicalValues.length === 0));
technicalRows.length > 0 ||
hasTechnicalValues(technicalValues);
if (hasTechnical) {
items.push({ id: "technical", ...tabMeta.technical });
@@ -54,7 +67,13 @@ export function ProductTabs({
items.push({ id: "comments", ...tabMeta.comments });
return items;
}, [deliveryNotes, description, technicalNotes, technicalValues]);
}, [
deliveryNotes,
description,
technicalNotes,
technicalRows.length,
technicalValues,
]);
const [activeTab, setActiveTab] = useState<TabId>(tabs[0]?.id ?? "comments");
@@ -120,12 +139,10 @@ export function ProductTabs({
</div>
) : null}
{technicalValues ? (
<TechnicalValuesTable
value={technicalValues}
fieldLabels={technicalFieldLabels}
/>
) : null}
<TechnicalValuesTable
technicalInfo={technicalInfo}
value={technicalValues}
/>
{deliveryNotes ? (
<div>
@@ -141,8 +158,8 @@ export function ProductTabs({
) : null}
{activeTab === "comments" ? (
<div role="tabpanel" className="text-center">
<div className="mx-auto max-w-md py-3">
<div role="tabpanel">
<div className="mx-auto max-w-md py-3 text-center">
<p className="font-industrial text-sm tracking-[0.16em] text-brand">
COMMENTS
</p>
+17 -14
View File
@@ -1,18 +1,27 @@
import {
normalizeTechnicalValueRows,
rowsFromTechnicalInfo,
type TechnicalValueRow,
} from "@/lib/product-technical-values";
import type { UserProductTechnicalInfo } from "@/lib/api/user-products";
type TechnicalValuesTableProps = {
value: unknown;
value?: unknown;
fieldLabels?: Record<string, string>;
technicalInfo?: UserProductTechnicalInfo | null;
};
export function TechnicalValuesTable({
value,
fieldLabels = {},
technicalInfo,
}: TechnicalValuesTableProps) {
const rows = normalizeTechnicalValueRows(value, fieldLabels);
const fromInfo = rowsFromTechnicalInfo(technicalInfo);
const rows: TechnicalValueRow[] =
fromInfo.length > 0
? fromInfo
: normalizeTechnicalValueRows(value, fieldLabels);
if (rows.length === 0) return null;
return (
@@ -34,7 +43,12 @@ export function TechnicalValuesTable({
</thead>
<tbody>
{rows.map((row) => (
<TechnicalValueTableRow key={row.id} row={row} />
<tr key={row.id} className="border-b border-line last:border-b-0">
<th className="px-4 py-3 text-right font-bold text-brand-ink">
{row.label || "—"}
</th>
<td className="px-4 py-3 text-muted">{row.value}</td>
</tr>
))}
</tbody>
</table>
@@ -42,14 +56,3 @@ export function TechnicalValuesTable({
</div>
);
}
function TechnicalValueTableRow({ row }: { row: TechnicalValueRow }) {
return (
<tr className="border-b border-line last:border-b-0">
<th className="px-4 py-3 text-right font-bold text-brand-ink">
{row.label || "—"}
</th>
<td className="px-4 py-3 text-muted">{row.value}</td>
</tr>
);
}
-47
View File
@@ -1,47 +0,0 @@
import { apiGet } from "./client";
import { findCategoryById, listCategories } from "./categories";
export type CategoryVariationField = {
id: string;
name: string;
nameFa: string | null;
sortOrder?: number;
};
function variationFieldLabel(
field: Pick<CategoryVariationField, "nameFa" | "name">,
) {
return field.nameFa || field.name;
}
export async function listCategoryVariationFields(categoryId: string) {
try {
const data = await apiGet<{ items: CategoryVariationField[] }>(
"/product-category-variation-fields",
{ categoryId },
);
return data.items ?? [];
} catch (error) {
console.error("listCategoryVariationFields failed", error);
return [] as CategoryVariationField[];
}
}
export async function getCategoryVariationFieldMap(categoryId?: string | null) {
if (!categoryId) return {};
const categories = await listCategories("product");
const category = findCategoryById(categories, categoryId);
const categoryIds = [categoryId, category?.parentId].filter(Boolean) as string[];
for (const id of categoryIds) {
const fields = await listCategoryVariationFields(id);
if (fields.length > 0) {
return Object.fromEntries(
fields.map((field) => [field.id, variationFieldLabel(field)]),
);
}
}
return {};
}
+62
View File
@@ -18,6 +18,42 @@ export type ListUserProductsParams = {
promoted?: boolean;
};
export type TechnicalFormFieldOption = {
id: string;
label?: string | null;
value?: string | null;
sortOrder?: number;
};
export type TechnicalFormField = {
id: string;
label: string;
key?: string | null;
type?: string | null;
isRequired?: boolean;
sortOrder?: number;
options?: TechnicalFormFieldOption[];
};
export type TechnicalForm = {
id?: string;
categoryId?: string;
fields?: TechnicalFormField[];
} | null;
export type TechnicalInfoValue = {
fieldId: string;
textValue?: string | null;
numberValue?: number | string | null;
optionId?: string | null;
optionIds?: string[] | null;
};
export type UserProductTechnicalInfo = {
form: TechnicalForm;
values: TechnicalInfoValue[];
};
const emptyPage = <T,>(page = 1, pageSize = 12): Paginated<T> => ({
items: [],
total: 0,
@@ -61,6 +97,32 @@ export async function getUserProductBySlugOrNull(slug: string) {
}
}
/** Labels + values for specs table. Join form.fields[].id ↔ values[].fieldId. */
export async function getUserProductTechnicalInfo(slug: string) {
try {
return await apiGet<UserProductTechnicalInfo>(
`/user-products/${encodeURIComponent(slug)}/technical-info`,
);
} catch (error) {
if (error instanceof ApiError && error.status === 404) {
return { form: null, values: [] } satisfies UserProductTechnicalInfo;
}
console.error("getUserProductTechnicalInfo failed", error);
return { form: null, values: [] } satisfies UserProductTechnicalInfo;
}
}
export function technicalFieldLabelMap(
info: UserProductTechnicalInfo | null | undefined,
): Record<string, string> {
const fields = info?.form?.fields ?? [];
return Object.fromEntries(
fields
.filter((field) => field.id && field.label)
.map((field) => [String(field.id), field.label]),
);
}
export const conditionLabels: Record<string, string> = {
new: "نو",
stock: "کارکرده",
+81 -9
View File
@@ -1,7 +1,15 @@
import type {
TechnicalFormField,
TechnicalInfoValue,
UserProductTechnicalInfo,
} from "@/lib/api/user-products";
export type TechnicalValueItem = {
fieldId?: string | number | null;
textValue?: string | null;
numberValue?: number | string | null;
optionId?: string | null;
optionIds?: string[] | null;
value?: string | number | null;
fieldName?: string | null;
fieldNameFa?: string | null;
@@ -20,6 +28,7 @@ export type TechnicalValueRow = {
id: string;
label: string;
value: string;
sortOrder: number;
};
function pickString(...values: Array<string | number | null | undefined>) {
@@ -48,6 +57,71 @@ function parseJsonObject(value: string) {
}
}
function resolveOptionLabel(
field: TechnicalFormField | undefined,
optionId: string | null | undefined,
) {
if (!field || !optionId) return "";
const option = field.options?.find((item) => String(item.id) === String(optionId));
return pickString(option?.label, option?.value, optionId);
}
function resolveValueText(
value: TechnicalInfoValue | TechnicalValueItem,
field?: TechnicalFormField,
) {
const optionIds =
value.optionIds?.filter(Boolean).map(String) ??
(value.optionId ? [String(value.optionId)] : []);
if (optionIds.length > 0) {
const labels = optionIds
.map((id) => resolveOptionLabel(field, id))
.filter(Boolean);
if (labels.length > 0) return labels.join("، ");
}
return pickString(
formatValue(value.textValue),
formatValue(value.numberValue),
formatValue("value" in value ? value.value : undefined),
);
}
/** Preferred path: join technical-info form fields with submitted values. */
export function rowsFromTechnicalInfo(
info: UserProductTechnicalInfo | null | undefined,
): TechnicalValueRow[] {
const fields = info?.form?.fields ?? [];
const values = info?.values ?? [];
if (fields.length === 0 || values.length === 0) return [];
const fieldById = new Map(fields.map((field) => [String(field.id), field]));
const sortById = new Map(
fields.map((field, index) => [
String(field.id),
field.sortOrder ?? index,
]),
);
return values
.map((value, index) => {
const id = String(value.fieldId ?? `item-${index}`);
const field = fieldById.get(id);
const rowValue = resolveValueText(value, field);
if (!rowValue) return null;
return {
id,
label: pickString(field?.label) || "",
value: rowValue,
sortOrder: sortById.get(id) ?? index,
};
})
.filter((row): row is TechnicalValueRow => Boolean(row))
.sort((a, b) => a.sortOrder - b.sortOrder || a.label.localeCompare(b.label, "fa"));
}
export function normalizeTechnicalValueRows(
value: unknown,
fieldLabels: Record<string, string> = {},
@@ -59,7 +133,7 @@ export function normalizeTechnicalValueRows(
if (parsed !== null) return normalizeTechnicalValueRows(parsed, fieldLabels);
const text = value.trim();
return text ? [{ id: "text", label: "", value: text }] : [];
return text ? [{ id: "text", label: "", value: text, sortOrder: 0 }] : [];
}
if (Array.isArray(value)) {
@@ -75,6 +149,7 @@ export function normalizeTechnicalValueRows(
id: `item-${index}`,
label: "",
value: item.trim(),
sortOrder: index,
};
}
@@ -96,29 +171,26 @@ export function normalizeTechnicalValueRows(
id ? fieldLabels[id] : undefined,
) || "";
const rowValue = pickString(
formatValue(field.textValue),
formatValue(field.numberValue),
formatValue(field.value),
);
const rowValue = resolveValueText(field);
if (!rowValue) return null;
return { id, label, value: rowValue };
return { id, label, value: rowValue, sortOrder: index };
})
.filter((row): row is TechnicalValueRow => Boolean(row));
}
if (typeof value === "object") {
return Object.entries(value as Record<string, unknown>)
.map(([key, val]) => ({
.map(([key, val], index) => ({
id: key,
label: fieldLabels[key] || key,
value: formatValue(val),
sortOrder: index,
}))
.filter((row) => row.value);
}
const text = formatValue(value);
return text ? [{ id: "value", label: "", value: text }] : [];
return text ? [{ id: "value", label: "", value: text, sortOrder: 0 }] : [];
}
+39
View File
@@ -0,0 +1,39 @@
{
"baseUrl": "https://www.mashinify.com",
"staticPages": [
{
"path": "/",
"changefreq": "daily",
"priority": 1
},
{
"path": "/about",
"changefreq": "monthly",
"priority": 0.6
},
{
"path": "/blog",
"changefreq": "weekly",
"priority": 0.8
},
{
"path": "/categories",
"changefreq": "weekly",
"priority": 0.8
},
{
"path": "/contact",
"changefreq": "monthly",
"priority": 0.5
},
{
"path": "/user-products",
"changefreq": "daily",
"priority": 0.9
}
],
"templates": {
"userProduct": "/user-products/{slug}",
"blog": "/blog/{slug}"
}
}