Add Schema.org JSON-LD, SEO meta overrides, and resilient bulk SMS.

Expose Organization/LocalBusiness and Product/BlogPosting jsonLd, optional seoMeta fields on content/categories, and continue mass SMS after per-recipient failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-09 23:30:24 +03:30
co-authored by Cursor
parent db305c58a4
commit aaf7a83742
33 changed files with 1395 additions and 188 deletions
+11
View File
@@ -9,6 +9,7 @@ import {
IsNumber,
IsOptional,
IsString,
MaxLength,
Min,
MinLength,
ValidateNested,
@@ -166,6 +167,16 @@ export class CreateUserProductDto {
@IsArray()
@IsString({ each: true })
galleryMediaIds?: string[];
@IsOptional()
@IsString()
@MaxLength(70)
seoMetaTitle?: string | null;
@IsOptional()
@IsString()
@MaxLength(160)
seoMetaDescription?: string | null;
}
export class UpdateUserProductDto extends CreateUserProductDto {}
+19 -7
View File
@@ -14,6 +14,11 @@ import {
import { AuthUser } from '../auth/auth.types';
import { PermissionsService } from '../auth/permissions.service';
import { CategoryTechnicalFormService } from '../categories/category-technical-form.service';
import {
applySeoMetaToMetadata,
normalizeSeoMetaInput,
readSeoMetaFromMetadata,
} from '../common/seo-meta.util';
import { MediaService } from '../media/media.service';
import { PrismaService } from '../prisma/prisma.service';
import {
@@ -337,9 +342,12 @@ export class UserProductsService {
const content: Prisma.InputJsonValue = {
...(dto.titleEn?.trim() ? { titleEn: dto.titleEn.trim() } : {}),
};
const metadata: Prisma.InputJsonValue = {
priceByExpert: dto.priceByExpert === true,
};
const metadata = applySeoMetaToMetadata(
{
priceByExpert: dto.priceByExpert === true,
},
normalizeSeoMetaInput(dto.seoMetaTitle, dto.seoMetaDescription),
) as Prisma.InputJsonValue;
const created = await this.prisma.$transaction(async (tx) => {
const product = await tx.userProduct.create({
@@ -522,10 +530,13 @@ export class UserProductsService {
const content: Prisma.InputJsonValue = {
...(dto.titleEn?.trim() ? { titleEn: dto.titleEn.trim() } : {}),
};
const metadata: Prisma.InputJsonValue = {
...existingMetadata,
priceByExpert: dto.priceByExpert === true,
};
const metadata = applySeoMetaToMetadata(
{
...existingMetadata,
priceByExpert: dto.priceByExpert === true,
},
normalizeSeoMetaInput(dto.seoMetaTitle, dto.seoMetaDescription),
) as Prisma.InputJsonValue;
let slug = existing.slug;
if (titleFa !== existing.title) {
@@ -1269,6 +1280,7 @@ export class UserProductsService {
priceCurrency: product.priceCurrency,
priceByExpert,
promoted,
...readSeoMetaFromMetadata(metadata),
status: product.status,
condition: product.condition,
deliveryNote: product.deliveryNote,