Add website schema settings, SEO meta overrides, and resilient bulk SMS.

Business admins can configure site-wide Schema.org and optional per-item SEO title/description; mass SMS continues on failures with an end report.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-09 23:30:23 +03:30
co-authored by Cursor
parent 15aad9f9d8
commit c2ede07e73
37 changed files with 1217 additions and 89 deletions
@@ -7,6 +7,7 @@ import type {
ProductCategory,
UpdateCategoryPayload,
} from '../types/category'
import { seoMetaPayload } from '../utils/seoMeta'
const ENTITY_TYPE = 'blog' as const
@@ -61,6 +62,7 @@ export function toBlogCategoryFormPayload(data: CategoryFormData): CreateCategor
name: data.nameEn,
nameFa: data.nameFa,
description: data.description || undefined,
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
}
}
@@ -70,6 +72,8 @@ export function mapBlogCategoryToUi(category: ProductCategory) {
nameEn: category.name,
nameFa: category.nameFa ?? '',
description: category.description ?? '',
seoMetaTitle: category.seoMetaTitle ?? '',
seoMetaDescription: category.seoMetaDescription ?? '',
parentId: category.parentId,
variationCount: category.variationCount ?? 0,
}
@@ -17,6 +17,8 @@ export interface BlogApi {
categoryId: string | null
categoryName: string
tags: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
titleImageUrl: string | null
featuredMediaId: string | null
authorId?: string | null
@@ -60,6 +62,8 @@ export interface CreateBlogPayload {
status?: BlogStatus
featuredMediaId?: string
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export interface UpdateBlogPayload {
@@ -71,6 +75,8 @@ export interface UpdateBlogPayload {
status?: BlogStatus
featuredMediaId?: string | null
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export function resolveBlogTitleImageUrl(blog: BlogApi): string | null {
@@ -155,6 +161,8 @@ export function mapBlogApiToFormState(blog: BlogApi) {
mainTextHtml: blog.mainTextHtml,
categoryId: blog.categoryId ?? '',
tags: blog.tags,
seoMetaTitle: blog.seoMetaTitle ?? '',
seoMetaDescription: blog.seoMetaDescription ?? '',
titleImage: resolveBlogTitleImageUrl(blog),
featuredMediaId: blog.featuredMediaId,
}
@@ -106,6 +106,8 @@ export function mapBrandToSelectOption(brand: BrandApi): FlatCategory {
nameEn: brand.nameEn,
nameFa: brand.nameFa ?? '',
description: brand.about ?? '',
seoMetaTitle: '',
seoMetaDescription: '',
parentId: null,
variationCount: 0,
depth: 0,
@@ -189,7 +189,10 @@ export interface SendCustomersBulkSmsResult {
enabled: boolean
message: string
recipientCount: number
sentCount?: number
failedCount?: number
serverIds?: string[]
failures?: Array<{ cellNumber: string; error: string }>
source?: string
}
@@ -7,6 +7,7 @@ import type {
ProductCategory,
UpdateCategoryPayload,
} from '../types/category'
import { seoMetaPayload } from '../utils/seoMeta'
const ENTITY_TYPE = 'instruction' as const
@@ -61,6 +62,7 @@ export function toInstructionCategoryFormPayload(data: CategoryFormData): Create
name: data.nameEn,
nameFa: data.nameFa,
description: data.description || undefined,
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
}
}
@@ -70,6 +72,8 @@ export function mapInstructionCategoryToUi(category: ProductCategory) {
nameEn: category.name,
nameFa: category.nameFa ?? '',
description: category.description ?? '',
seoMetaTitle: category.seoMetaTitle ?? '',
seoMetaDescription: category.seoMetaDescription ?? '',
parentId: category.parentId,
variationCount: category.variationCount ?? 0,
}
@@ -25,6 +25,8 @@ export interface InstructionApi {
categoryId: string | null
categoryName: string
tags: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
coverImageUrl: string | null
featuredMediaId: string | null
authorId?: string | null
@@ -70,6 +72,8 @@ export interface CreateInstructionPayload {
status?: InstructionStatus
featuredMediaId?: string
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export interface UpdateInstructionPayload {
@@ -83,6 +87,8 @@ export interface UpdateInstructionPayload {
status?: InstructionStatus
featuredMediaId?: string | null
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export function resolveInstructionTitle(
@@ -170,6 +176,8 @@ export function mapInstructionApiToFormState(instruction: InstructionApi) {
embedCode: instruction.embedCode,
categoryId: instruction.categoryId ?? '',
tags: instruction.tags,
seoMetaTitle: instruction.seoMetaTitle ?? '',
seoMetaDescription: instruction.seoMetaDescription ?? '',
coverImage: instruction.coverImageUrl,
featuredMediaId: instruction.featuredMediaId,
}
@@ -7,6 +7,7 @@ import type {
ProductCategory,
UpdateCategoryPayload,
} from '../types/category'
import { seoMetaPayload } from '../utils/seoMeta'
const ENTITY_TYPE = 'portfolio' as const
@@ -61,6 +62,7 @@ export function toPortfolioCategoryFormPayload(data: CategoryFormData): CreateCa
name: data.nameEn,
nameFa: data.nameFa,
description: data.description || undefined,
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
}
}
@@ -70,6 +72,8 @@ export function mapPortfolioCategoryToUi(category: ProductCategory) {
nameEn: category.name,
nameFa: category.nameFa ?? '',
description: category.description ?? '',
seoMetaTitle: category.seoMetaTitle ?? '',
seoMetaDescription: category.seoMetaDescription ?? '',
parentId: category.parentId,
variationCount: category.variationCount ?? 0,
}
@@ -19,6 +19,8 @@ export interface PortfolioApi {
categoryId: string | null
categoryName: string
tags: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
titleImageUrl: string | null
featuredMediaId: string | null
gallery: PortfolioGalleryItem[]
@@ -66,6 +68,8 @@ export interface CreatePortfolioPayload {
featuredMediaId?: string
galleryMediaIds?: string[]
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
sortOrder?: number
}
@@ -82,6 +86,8 @@ export interface UpdatePortfolioPayload {
featuredMediaId?: string | null
galleryMediaIds?: string[]
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
sortOrder?: number
/** Pin to absolute top of the business portfolio list. */
moveToTop?: boolean
@@ -171,6 +177,8 @@ export function mapPortfolioApiToFormState(portfolio: PortfolioApi) {
projectUrl: portfolio.projectUrl?.trim() || '',
categoryId: portfolio.categoryId ?? '',
tags: portfolio.tags,
seoMetaTitle: portfolio.seoMetaTitle ?? '',
seoMetaDescription: portfolio.seoMetaDescription ?? '',
titleImage: resolvePortfolioTitleImageUrl(portfolio),
featuredMediaId: portfolio.featuredMediaId,
images: portfolio.gallery.map((item) => item.url),
@@ -7,6 +7,7 @@ import type {
ProductCategory,
UpdateCategoryPayload,
} from '../types/category'
import { seoMetaPayload } from '../utils/seoMeta'
const ENTITY_TYPE = 'product' as const
@@ -75,6 +76,7 @@ export function toCategoryFormPayload(data: CategoryFormData): CreateCategoryPay
name: data.nameEn,
nameFa: data.nameFa,
description: data.description || undefined,
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
}
}
@@ -84,6 +86,8 @@ export function mapProductCategoryToUi(category: ProductCategory) {
nameEn: category.name,
nameFa: category.nameFa ?? '',
description: category.description ?? '',
seoMetaTitle: category.seoMetaTitle ?? '',
seoMetaDescription: category.seoMetaDescription ?? '',
parentId: category.parentId,
variationCount: category.variationCount ?? 0,
translationCount: category.translationCount ?? 0,
@@ -27,6 +27,8 @@ export interface ProductApi {
imageUrl: string | null
} | null
tags: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
thumbnailUrl: string | null
thumbnailMediaId: string | null
image: string
@@ -58,6 +60,8 @@ export interface CreateProductPayload {
featuredMediaId?: string
galleryMediaIds?: string[]
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export interface UpdateProductPayload {
@@ -71,6 +75,8 @@ export interface UpdateProductPayload {
featuredMediaId?: string | null
galleryMediaIds?: string[]
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
function businessPath(suffix = '') {
@@ -145,6 +151,8 @@ export function mapProductApiToFormState(product: ProductApi) {
thumbnail: product.thumbnailUrl ?? product.thumbnail ?? null,
images: product.images.map((item) => item.url),
tags: product.tags,
seoMetaTitle: product.seoMetaTitle ?? '',
seoMetaDescription: product.seoMetaDescription ?? '',
thumbnailMediaId: product.thumbnailMediaId,
galleryMediaIds: product.galleryMediaIds,
}
@@ -91,6 +91,18 @@ export interface ModulesSettings {
export type SpecialProductsSource = 'product' | 'store_item'
export type WebsiteSchemaType = 'Organization' | 'LocalBusiness'
export interface WebsiteSchemaSettings {
enabled: boolean
type: WebsiteSchemaType
name: string | null
nameFa: string | null
description: string | null
priceRange: string | null
sameAs: string[]
}
export interface WebsiteSitemapConfig {
baseUrl?: string | null
staticPages?: Array<{
@@ -105,6 +117,7 @@ export interface WebsiteSitemapConfig {
export interface WebsiteSettings {
specialProductsSource: SpecialProductsSource
sitemapConfig?: WebsiteSitemapConfig | null
schema?: WebsiteSchemaSettings
}
export interface SmsSettings {
@@ -61,6 +61,8 @@ export interface CreateUserProductInput {
technicalValues?: UserProductTechnicalValueInput[]
featuredMediaId?: string
galleryMediaIds?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export type UpdateUserProductInput = CreateUserProductInput
@@ -77,6 +79,8 @@ export interface UserProductDetail extends UserProductListItem {
technicalNotes?: string | null
description?: string | null
titleEn?: string | null
seoMetaTitle?: string | null
seoMetaDescription?: string | null
priceCurrency?: string | null
priceByExpert?: boolean
promoted?: boolean
@@ -7,6 +7,7 @@ import type {
ProductCategory,
UpdateCategoryPayload,
} from '../types/category'
import { seoMetaPayload } from '../utils/seoMeta'
const ENTITY_TYPE = 'video' as const
@@ -61,6 +62,7 @@ export function toVideoCategoryFormPayload(data: CategoryFormData): CreateCatego
name: data.nameEn,
nameFa: data.nameFa,
description: data.description || undefined,
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
}
}
@@ -70,6 +72,8 @@ export function mapVideoCategoryToUi(category: ProductCategory) {
nameEn: category.name,
nameFa: category.nameFa ?? '',
description: category.description ?? '',
seoMetaTitle: category.seoMetaTitle ?? '',
seoMetaDescription: category.seoMetaDescription ?? '',
parentId: category.parentId,
variationCount: category.variationCount ?? 0,
}
@@ -25,6 +25,8 @@ export interface VideoApi {
categoryId: string | null
categoryName: string
tags: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
coverImageUrl: string | null
featuredMediaId: string | null
authorId?: string | null
@@ -70,6 +72,8 @@ export interface CreateVideoPayload {
status?: VideoStatus
featuredMediaId?: string
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export interface UpdateVideoPayload {
@@ -83,6 +87,8 @@ export interface UpdateVideoPayload {
status?: VideoStatus
featuredMediaId?: string | null
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
}
export function resolveVideoTitle(
@@ -170,6 +176,8 @@ export function mapVideoApiToFormState(video: VideoApi) {
embedCode: video.embedCode,
categoryId: video.categoryId ?? '',
tags: video.tags,
seoMetaTitle: video.seoMetaTitle ?? '',
seoMetaDescription: video.seoMetaDescription ?? '',
coverImage: video.coverImageUrl,
featuredMediaId: video.featuredMediaId,
}
@@ -7,6 +7,7 @@ import type {
ProductCategory,
UpdateCategoryPayload,
} from '../types/category'
import { seoMetaPayload } from '../utils/seoMeta'
const ENTITY_TYPE = 'workshop' as const
@@ -61,6 +62,7 @@ export function toWorkshopCategoryFormPayload(data: CategoryFormData): CreateCat
name: data.nameEn,
nameFa: data.nameFa,
description: data.description || undefined,
...seoMetaPayload(data.seoMetaTitle, data.seoMetaDescription),
}
}
@@ -70,6 +72,8 @@ export function mapWorkshopCategoryToUi(category: ProductCategory) {
nameEn: category.name,
nameFa: category.nameFa ?? '',
description: category.description ?? '',
seoMetaTitle: category.seoMetaTitle ?? '',
seoMetaDescription: category.seoMetaDescription ?? '',
parentId: category.parentId,
variationCount: category.variationCount ?? 0,
}
@@ -24,6 +24,8 @@ export interface WorkshopApi {
categoryId: string | null
categoryName: string
tags: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
coverImageUrl: string | null
featuredMediaId: string | null
gallery: WorkshopGalleryItem[]
@@ -93,6 +95,8 @@ export interface CreateWorkshopPayload {
featuredMediaId?: string
galleryMediaIds?: string[]
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
eventDate?: string | null
eventTime?: string | null
duration?: string | null
@@ -108,6 +112,8 @@ export interface UpdateWorkshopPayload {
featuredMediaId?: string | null
galleryMediaIds?: string[]
tags?: string[]
seoMetaTitle?: string | null
seoMetaDescription?: string | null
eventDate?: string | null
eventTime?: string | null
duration?: string | null
@@ -291,6 +297,8 @@ export function mapWorkshopApiToFormState(workshop: WorkshopApi) {
descriptionHtml: normalizeRichTextHtml(workshop.descriptionHtml),
categoryId: workshop.categoryId ?? '',
tags: Array.isArray(workshop.tags) ? workshop.tags : [],
seoMetaTitle: workshop.seoMetaTitle ?? '',
seoMetaDescription: workshop.seoMetaDescription ?? '',
eventDate: normalizeGregorianDate(workshop.eventDate),
eventTime: normalizeTimeInput(workshop.eventTime),
duration: