@open-mercato/core 0.6.6-develop.5716.1.b108502d0d → 0.6.6-develop.5738.1.4182c2b2c7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ const MESSAGE_PREFIX = "SEO helper: ";
2
+ function buildSeoBlockMessage(issues) {
3
+ if (issues.length === 1) return `${MESSAGE_PREFIX}${issues[0]}`;
4
+ if (issues.length <= 3) return `${MESSAGE_PREFIX}${issues.join(" ")}`;
5
+ return `${MESSAGE_PREFIX}${issues.length} issues found. ${issues.slice(0, 2).join(" ")}`;
6
+ }
7
+ function evaluateProductSeo(data) {
8
+ const issues = [];
9
+ const fieldErrors = {};
10
+ const title = data?.title || data?.name;
11
+ if (typeof title === "string" && title.length > 0) {
12
+ if (title.length < 10) {
13
+ issues.push("Title is too short (min 10 characters).");
14
+ fieldErrors.title = "Title is too short for good SEO (min 10 characters).";
15
+ } else if (title.length > 60) {
16
+ issues.push("Title is too long (max 60 characters recommended).");
17
+ fieldErrors.title = "Title is too long for optimal SEO (max 60 characters).";
18
+ }
19
+ }
20
+ const description = data?.description;
21
+ if (typeof description === "string") {
22
+ if (description.trim().length === 0) {
23
+ issues.push("Add a product description for better SEO.");
24
+ fieldErrors.description = "Provide a description to help search engines understand this product.";
25
+ } else if (description.length < 50) {
26
+ issues.push("Description is too short (min 50 characters).");
27
+ fieldErrors.description = "Description is too short for good SEO (min 50 characters).";
28
+ }
29
+ }
30
+ if (issues.length) {
31
+ return { ok: false, issues, fieldErrors, message: buildSeoBlockMessage(issues) };
32
+ }
33
+ return { ok: true, issues: [], fieldErrors: {} };
34
+ }
35
+ export {
36
+ buildSeoBlockMessage,
37
+ evaluateProductSeo
38
+ };
39
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/catalog/widgets/injection/product-seo/validation.ts"],
4
+ "sourcesContent": ["export type ProductSeoEvaluation = {\n ok: boolean\n issues: string[]\n fieldErrors: Record<string, string>\n message?: string\n}\n\nconst MESSAGE_PREFIX = 'SEO helper: '\n\nexport function buildSeoBlockMessage(issues: string[]): string {\n if (issues.length === 1) return `${MESSAGE_PREFIX}${issues[0]}`\n if (issues.length <= 3) return `${MESSAGE_PREFIX}${issues.join(' ')}`\n return `${MESSAGE_PREFIX}${issues.length} issues found. ${issues.slice(0, 2).join(' ')}`\n}\n\nexport function evaluateProductSeo(data: Record<string, unknown> | null | undefined): ProductSeoEvaluation {\n const issues: string[] = []\n const fieldErrors: Record<string, string> = {}\n\n const title = (data?.title as unknown) || (data?.name as unknown)\n if (typeof title === 'string' && title.length > 0) {\n if (title.length < 10) {\n issues.push('Title is too short (min 10 characters).')\n fieldErrors.title = 'Title is too short for good SEO (min 10 characters).'\n } else if (title.length > 60) {\n issues.push('Title is too long (max 60 characters recommended).')\n fieldErrors.title = 'Title is too long for optimal SEO (max 60 characters).'\n }\n }\n\n const description = data?.description\n if (typeof description === 'string') {\n if (description.trim().length === 0) {\n issues.push('Add a product description for better SEO.')\n fieldErrors.description = 'Provide a description to help search engines understand this product.'\n } else if (description.length < 50) {\n issues.push('Description is too short (min 50 characters).')\n fieldErrors.description = 'Description is too short for good SEO (min 50 characters).'\n }\n }\n\n if (issues.length) {\n return { ok: false, issues, fieldErrors, message: buildSeoBlockMessage(issues) }\n }\n\n return { ok: true, issues: [], fieldErrors: {} }\n}\n"],
5
+ "mappings": "AAOA,MAAM,iBAAiB;AAEhB,SAAS,qBAAqB,QAA0B;AAC7D,MAAI,OAAO,WAAW,EAAG,QAAO,GAAG,cAAc,GAAG,OAAO,CAAC,CAAC;AAC7D,MAAI,OAAO,UAAU,EAAG,QAAO,GAAG,cAAc,GAAG,OAAO,KAAK,GAAG,CAAC;AACnE,SAAO,GAAG,cAAc,GAAG,OAAO,MAAM,kBAAkB,OAAO,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC;AACxF;AAEO,SAAS,mBAAmB,MAAwE;AACzG,QAAM,SAAmB,CAAC;AAC1B,QAAM,cAAsC,CAAC;AAE7C,QAAM,QAAS,MAAM,SAAsB,MAAM;AACjD,MAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,QAAI,MAAM,SAAS,IAAI;AACrB,aAAO,KAAK,yCAAyC;AACrD,kBAAY,QAAQ;AAAA,IACtB,WAAW,MAAM,SAAS,IAAI;AAC5B,aAAO,KAAK,oDAAoD;AAChE,kBAAY,QAAQ;AAAA,IACtB;AAAA,EACF;AAEA,QAAM,cAAc,MAAM;AAC1B,MAAI,OAAO,gBAAgB,UAAU;AACnC,QAAI,YAAY,KAAK,EAAE,WAAW,GAAG;AACnC,aAAO,KAAK,2CAA2C;AACvD,kBAAY,cAAc;AAAA,IAC5B,WAAW,YAAY,SAAS,IAAI;AAClC,aAAO,KAAK,+CAA+C;AAC3D,kBAAY,cAAc;AAAA,IAC5B;AAAA,EACF;AAEA,MAAI,OAAO,QAAQ;AACjB,WAAO,EAAE,IAAI,OAAO,QAAQ,aAAa,SAAS,qBAAqB,MAAM,EAAE;AAAA,EACjF;AAEA,SAAO,EAAE,IAAI,MAAM,QAAQ,CAAC,GAAG,aAAa,CAAC,EAAE;AACjD;",
6
+ "names": []
7
+ }
@@ -1,5 +1,6 @@
1
1
  import ProductSeoWidget from "./widget.client.js";
2
2
  import { publishProductSeoValidation } from "./state.js";
3
+ import { evaluateProductSeo } from "./validation.js";
3
4
  const widget = {
4
5
  metadata: {
5
6
  id: "catalog.injection.product-seo",
@@ -7,37 +8,16 @@ const widget = {
7
8
  description: "Helps optimize product metadata for search engines",
8
9
  features: ["catalog.products.edit"],
9
10
  priority: 90,
10
- enabled: true
11
+ enabled: true,
12
+ requiredFields: ["description"]
11
13
  },
12
14
  Widget: ProductSeoWidget,
13
15
  eventHandlers: {
14
- onBeforeSave: async (data, context) => {
15
- const issues = [];
16
- const fieldErrors = {};
17
- const title = data?.title || data?.name;
18
- if (typeof title === "string" && title.length > 0) {
19
- if (title.length < 10) {
20
- issues.push("Title is too short (min 10 characters).");
21
- fieldErrors.title = "Title is too short for good SEO (min 10 characters).";
22
- } else if (title.length > 60) {
23
- issues.push("Title is too long (max 60 characters recommended).");
24
- fieldErrors.title = "Title is too long for optimal SEO (max 60 characters).";
25
- }
26
- }
27
- const description = data?.description;
28
- if (typeof description === "string") {
29
- if (description.trim().length === 0) {
30
- issues.push("Add a product description for better SEO.");
31
- fieldErrors.description = "Provide a description to help search engines understand this product.";
32
- } else if (description.length < 50) {
33
- issues.push("Description is too short (min 50 characters).");
34
- fieldErrors.description = "Description is too short for good SEO (min 50 characters).";
35
- }
36
- }
37
- if (issues.length) {
38
- const message = "SEO helper blocked save. Improve the highlighted fields.";
39
- publishProductSeoValidation({ ok: false, issues, message });
40
- return { ok: false, message, fieldErrors };
16
+ onBeforeSave: async (data) => {
17
+ const evaluation = evaluateProductSeo(data);
18
+ if (!evaluation.ok) {
19
+ publishProductSeoValidation({ ok: false, issues: evaluation.issues, message: evaluation.message });
20
+ return { ok: false, message: evaluation.message, fieldErrors: evaluation.fieldErrors };
41
21
  }
42
22
  publishProductSeoValidation({ ok: true, issues: [] });
43
23
  return { ok: true };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/catalog/widgets/injection/product-seo/widget.ts"],
4
- "sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport ProductSeoWidget from './widget.client'\nimport { publishProductSeoValidation } from './state'\n\nconst widget: InjectionWidgetModule<any, any> = {\n metadata: {\n id: 'catalog.injection.product-seo',\n title: 'Product SEO Helper',\n description: 'Helps optimize product metadata for search engines',\n features: ['catalog.products.edit'],\n priority: 90,\n enabled: true,\n },\n Widget: ProductSeoWidget,\n eventHandlers: {\n onBeforeSave: async (data, context) => {\n const issues: string[] = []\n const fieldErrors: Record<string, string> = {}\n\n const title = data?.title || data?.name\n if (typeof title === 'string' && title.length > 0) {\n if (title.length < 10) {\n issues.push('Title is too short (min 10 characters).')\n fieldErrors.title = 'Title is too short for good SEO (min 10 characters).'\n } else if (title.length > 60) {\n issues.push('Title is too long (max 60 characters recommended).')\n fieldErrors.title = 'Title is too long for optimal SEO (max 60 characters).'\n }\n }\n\n const description = data?.description\n if (typeof description === 'string') {\n if (description.trim().length === 0) {\n issues.push('Add a product description for better SEO.')\n fieldErrors.description = 'Provide a description to help search engines understand this product.'\n } else if (description.length < 50) {\n issues.push('Description is too short (min 50 characters).')\n fieldErrors.description = 'Description is too short for good SEO (min 50 characters).'\n }\n }\n\n if (issues.length) {\n const message = 'SEO helper blocked save. Improve the highlighted fields.'\n publishProductSeoValidation({ ok: false, issues, message })\n return { ok: false, message, fieldErrors }\n }\n\n publishProductSeoValidation({ ok: true, issues: [] })\n return { ok: true }\n },\n },\n}\n\nexport default widget\n"],
5
- "mappings": "AACA,OAAO,sBAAsB;AAC7B,SAAS,mCAAmC;AAE5C,MAAM,SAA0C;AAAA,EAC9C,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,uBAAuB;AAAA,IAClC,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AAAA,EACA,QAAQ;AAAA,EACR,eAAe;AAAA,IACb,cAAc,OAAO,MAAM,YAAY;AACrC,YAAM,SAAmB,CAAC;AAC1B,YAAM,cAAsC,CAAC;AAE7C,YAAM,QAAQ,MAAM,SAAS,MAAM;AACnC,UAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG;AACjD,YAAI,MAAM,SAAS,IAAI;AACrB,iBAAO,KAAK,yCAAyC;AACrD,sBAAY,QAAQ;AAAA,QACtB,WAAW,MAAM,SAAS,IAAI;AAC5B,iBAAO,KAAK,oDAAoD;AAChE,sBAAY,QAAQ;AAAA,QACtB;AAAA,MACF;AAEA,YAAM,cAAc,MAAM;AAC1B,UAAI,OAAO,gBAAgB,UAAU;AACnC,YAAI,YAAY,KAAK,EAAE,WAAW,GAAG;AACnC,iBAAO,KAAK,2CAA2C;AACvD,sBAAY,cAAc;AAAA,QAC5B,WAAW,YAAY,SAAS,IAAI;AAClC,iBAAO,KAAK,+CAA+C;AAC3D,sBAAY,cAAc;AAAA,QAC5B;AAAA,MACF;AAEA,UAAI,OAAO,QAAQ;AACjB,cAAM,UAAU;AAChB,oCAA4B,EAAE,IAAI,OAAO,QAAQ,QAAQ,CAAC;AAC1D,eAAO,EAAE,IAAI,OAAO,SAAS,YAAY;AAAA,MAC3C;AAEA,kCAA4B,EAAE,IAAI,MAAM,QAAQ,CAAC,EAAE,CAAC;AACpD,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
4
+ "sourcesContent": ["import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'\nimport ProductSeoWidget from './widget.client'\nimport { publishProductSeoValidation } from './state'\nimport { evaluateProductSeo } from './validation'\n\nconst widget: InjectionWidgetModule<any, any> = {\n metadata: {\n id: 'catalog.injection.product-seo',\n title: 'Product SEO Helper',\n description: 'Helps optimize product metadata for search engines',\n features: ['catalog.products.edit'],\n priority: 90,\n enabled: true,\n requiredFields: ['description'],\n },\n Widget: ProductSeoWidget,\n eventHandlers: {\n onBeforeSave: async (data) => {\n const evaluation = evaluateProductSeo(data as Record<string, unknown>)\n\n if (!evaluation.ok) {\n publishProductSeoValidation({ ok: false, issues: evaluation.issues, message: evaluation.message })\n return { ok: false, message: evaluation.message, fieldErrors: evaluation.fieldErrors }\n }\n\n publishProductSeoValidation({ ok: true, issues: [] })\n return { ok: true }\n },\n },\n}\n\nexport default widget\n"],
5
+ "mappings": "AACA,OAAO,sBAAsB;AAC7B,SAAS,mCAAmC;AAC5C,SAAS,0BAA0B;AAEnC,MAAM,SAA0C;AAAA,EAC9C,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,aAAa;AAAA,IACb,UAAU,CAAC,uBAAuB;AAAA,IAClC,UAAU;AAAA,IACV,SAAS;AAAA,IACT,gBAAgB,CAAC,aAAa;AAAA,EAChC;AAAA,EACA,QAAQ;AAAA,EACR,eAAe;AAAA,IACb,cAAc,OAAO,SAAS;AAC5B,YAAM,aAAa,mBAAmB,IAA+B;AAErE,UAAI,CAAC,WAAW,IAAI;AAClB,oCAA4B,EAAE,IAAI,OAAO,QAAQ,WAAW,QAAQ,SAAS,WAAW,QAAQ,CAAC;AACjG,eAAO,EAAE,IAAI,OAAO,SAAS,WAAW,SAAS,aAAa,WAAW,YAAY;AAAA,MACvF;AAEA,kCAA4B,EAAE,IAAI,MAAM,QAAQ,CAAC,EAAE,CAAC;AACpD,aAAO,EAAE,IAAI,KAAK;AAAA,IACpB;AAAA,EACF;AACF;AAEA,IAAO,iBAAQ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.5716.1.b108502d0d",
3
+ "version": "0.6.6-develop.5738.1.4182c2b2c7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -245,16 +245,16 @@
245
245
  "zod": "^4.4.3"
246
246
  },
247
247
  "peerDependencies": {
248
- "@open-mercato/ai-assistant": "0.6.6-develop.5716.1.b108502d0d",
249
- "@open-mercato/shared": "0.6.6-develop.5716.1.b108502d0d",
250
- "@open-mercato/ui": "0.6.6-develop.5716.1.b108502d0d",
248
+ "@open-mercato/ai-assistant": "0.6.6-develop.5738.1.4182c2b2c7",
249
+ "@open-mercato/shared": "0.6.6-develop.5738.1.4182c2b2c7",
250
+ "@open-mercato/ui": "0.6.6-develop.5738.1.4182c2b2c7",
251
251
  "react": "^19.0.0",
252
252
  "react-dom": "^19.0.0"
253
253
  },
254
254
  "devDependencies": {
255
- "@open-mercato/ai-assistant": "0.6.6-develop.5716.1.b108502d0d",
256
- "@open-mercato/shared": "0.6.6-develop.5716.1.b108502d0d",
257
- "@open-mercato/ui": "0.6.6-develop.5716.1.b108502d0d",
255
+ "@open-mercato/ai-assistant": "0.6.6-develop.5738.1.4182c2b2c7",
256
+ "@open-mercato/shared": "0.6.6-develop.5738.1.4182c2b2c7",
257
+ "@open-mercato/ui": "0.6.6-develop.5738.1.4182c2b2c7",
258
258
  "@testing-library/dom": "^10.4.1",
259
259
  "@testing-library/jest-dom": "^6.9.1",
260
260
  "@testing-library/react": "^16.3.1",
@@ -830,11 +830,12 @@ export default function EditCatalogProductPage({
830
830
  {
831
831
  id: "details",
832
832
  column: 1,
833
- component: ({ values, setValue, errors }) => (
833
+ component: ({ values, setValue, errors, requiredFieldIds }) => (
834
834
  <ProductDetailsSection
835
835
  values={values as ProductFormValues}
836
836
  setValue={setValue}
837
837
  errors={errors}
838
+ requiredFieldIds={requiredFieldIds}
838
839
  productId={productId ?? ""}
839
840
  hasVariants={Boolean(
840
841
  (values as ProductFormValues).hasVariants,
@@ -1498,6 +1499,7 @@ function ProductDetailsSection({
1498
1499
  values,
1499
1500
  setValue,
1500
1501
  errors,
1502
+ requiredFieldIds,
1501
1503
  productId,
1502
1504
  hasVariants,
1503
1505
  variantMediaGroups,
@@ -1554,10 +1556,10 @@ function ProductDetailsSection({
1554
1556
 
1555
1557
  return (
1556
1558
  <div className="space-y-6">
1557
- <div className="space-y-2">
1559
+ <div className="space-y-2" data-crud-field-id="title">
1558
1560
  <Label className="flex items-center gap-1">
1559
1561
  {t("catalog.products.form.title", "Title")}
1560
- <span className="text-red-600">*</span>
1562
+ <span className="text-status-error-text">*</span>
1561
1563
  </Label>
1562
1564
  <Input
1563
1565
  value={values.title}
@@ -1568,13 +1570,18 @@ function ProductDetailsSection({
1568
1570
  )}
1569
1571
  />
1570
1572
  {errors.title ? (
1571
- <p className="text-xs text-red-600">{errors.title}</p>
1573
+ <p className="text-xs text-status-error-text">{errors.title}</p>
1572
1574
  ) : null}
1573
1575
  </div>
1574
1576
 
1575
- <div className="space-y-2">
1577
+ <div className="space-y-2" data-crud-field-id="description">
1576
1578
  <div className="flex items-center justify-between">
1577
- <Label>{t("catalog.products.form.description", "Description")}</Label>
1579
+ <Label className="flex items-center gap-1">
1580
+ {t("catalog.products.form.description", "Description")}
1581
+ {requiredFieldIds?.has("description") ? (
1582
+ <span className="text-status-error-text">*</span>
1583
+ ) : null}
1584
+ </Label>
1578
1585
  <Button
1579
1586
  type="button"
1580
1587
  variant="ghost"
@@ -1611,6 +1618,9 @@ function ProductDetailsSection({
1611
1618
  )}
1612
1619
  />
1613
1620
  )}
1621
+ {errors.description ? (
1622
+ <p className="text-xs text-status-error-text">{errors.description}</p>
1623
+ ) : null}
1614
1624
  </div>
1615
1625
 
1616
1626
  <ProductMediaManager
@@ -304,6 +304,7 @@ export default function CreateCatalogProductPage() {
304
304
  values,
305
305
  setValue,
306
306
  errors,
307
+ requiredFieldIds,
307
308
  }: CrudFormGroupComponentProps) => (
308
309
  <ProductBuilder
309
310
  values={values as ProductFormValues}
@@ -311,6 +312,7 @@ export default function CreateCatalogProductPage() {
311
312
  errors={errors}
312
313
  priceKinds={priceKinds}
313
314
  taxRates={taxRates}
315
+ requiredFieldIds={requiredFieldIds}
314
316
  />
315
317
  ),
316
318
  },
@@ -827,6 +829,7 @@ type ProductBuilderProps = {
827
829
  errors: Record<string, string>;
828
830
  priceKinds: PriceKindSummary[];
829
831
  taxRates: TaxRateSummary[];
832
+ requiredFieldIds?: ReadonlySet<string>;
830
833
  };
831
834
 
832
835
  type ProductMetaSectionProps = {
@@ -984,6 +987,7 @@ function ProductBuilder({
984
987
  errors,
985
988
  priceKinds,
986
989
  taxRates,
990
+ requiredFieldIds,
987
991
  }: ProductBuilderProps) {
988
992
  const t = useT();
989
993
  const steps = PRODUCT_FORM_STEPS;
@@ -1325,10 +1329,10 @@ function ProductBuilder({
1325
1329
 
1326
1330
  {currentStepKey === "general" ? (
1327
1331
  <div className="space-y-6">
1328
- <div className="space-y-2">
1332
+ <div className="space-y-2" data-crud-field-id="title">
1329
1333
  <Label className="flex items-center gap-1">
1330
1334
  {t("catalog.products.form.title", "Title")}
1331
- <span className="text-red-600">*</span>
1335
+ <span className="text-status-error-text">*</span>
1332
1336
  </Label>
1333
1337
  <Input
1334
1338
  value={values.title}
@@ -1339,14 +1343,17 @@ function ProductBuilder({
1339
1343
  )}
1340
1344
  />
1341
1345
  {errors.title ? (
1342
- <p className="text-xs text-red-600">{errors.title}</p>
1346
+ <p className="text-xs text-status-error-text">{errors.title}</p>
1343
1347
  ) : null}
1344
1348
  </div>
1345
1349
 
1346
- <div className="space-y-2">
1350
+ <div className="space-y-2" data-crud-field-id="description">
1347
1351
  <div className="flex items-center justify-between">
1348
- <Label>
1352
+ <Label className="flex items-center gap-1">
1349
1353
  {t("catalog.products.form.description", "Description")}
1354
+ {requiredFieldIds?.has("description") ? (
1355
+ <span className="text-status-error-text">*</span>
1356
+ ) : null}
1350
1357
  </Label>
1351
1358
  <Button
1352
1359
  type="button"
@@ -1389,6 +1396,9 @@ function ProductBuilder({
1389
1396
  )}
1390
1397
  />
1391
1398
  )}
1399
+ {errors.description ? (
1400
+ <p className="text-xs text-status-error-text">{errors.description}</p>
1401
+ ) : null}
1392
1402
  </div>
1393
1403
 
1394
1404
  <ProductMediaManager
@@ -0,0 +1,47 @@
1
+ export type ProductSeoEvaluation = {
2
+ ok: boolean
3
+ issues: string[]
4
+ fieldErrors: Record<string, string>
5
+ message?: string
6
+ }
7
+
8
+ const MESSAGE_PREFIX = 'SEO helper: '
9
+
10
+ export function buildSeoBlockMessage(issues: string[]): string {
11
+ if (issues.length === 1) return `${MESSAGE_PREFIX}${issues[0]}`
12
+ if (issues.length <= 3) return `${MESSAGE_PREFIX}${issues.join(' ')}`
13
+ return `${MESSAGE_PREFIX}${issues.length} issues found. ${issues.slice(0, 2).join(' ')}`
14
+ }
15
+
16
+ export function evaluateProductSeo(data: Record<string, unknown> | null | undefined): ProductSeoEvaluation {
17
+ const issues: string[] = []
18
+ const fieldErrors: Record<string, string> = {}
19
+
20
+ const title = (data?.title as unknown) || (data?.name as unknown)
21
+ if (typeof title === 'string' && title.length > 0) {
22
+ if (title.length < 10) {
23
+ issues.push('Title is too short (min 10 characters).')
24
+ fieldErrors.title = 'Title is too short for good SEO (min 10 characters).'
25
+ } else if (title.length > 60) {
26
+ issues.push('Title is too long (max 60 characters recommended).')
27
+ fieldErrors.title = 'Title is too long for optimal SEO (max 60 characters).'
28
+ }
29
+ }
30
+
31
+ const description = data?.description
32
+ if (typeof description === 'string') {
33
+ if (description.trim().length === 0) {
34
+ issues.push('Add a product description for better SEO.')
35
+ fieldErrors.description = 'Provide a description to help search engines understand this product.'
36
+ } else if (description.length < 50) {
37
+ issues.push('Description is too short (min 50 characters).')
38
+ fieldErrors.description = 'Description is too short for good SEO (min 50 characters).'
39
+ }
40
+ }
41
+
42
+ if (issues.length) {
43
+ return { ok: false, issues, fieldErrors, message: buildSeoBlockMessage(issues) }
44
+ }
45
+
46
+ return { ok: true, issues: [], fieldErrors: {} }
47
+ }
@@ -1,6 +1,7 @@
1
1
  import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
2
2
  import ProductSeoWidget from './widget.client'
3
3
  import { publishProductSeoValidation } from './state'
4
+ import { evaluateProductSeo } from './validation'
4
5
 
5
6
  const widget: InjectionWidgetModule<any, any> = {
6
7
  metadata: {
@@ -10,39 +11,16 @@ const widget: InjectionWidgetModule<any, any> = {
10
11
  features: ['catalog.products.edit'],
11
12
  priority: 90,
12
13
  enabled: true,
14
+ requiredFields: ['description'],
13
15
  },
14
16
  Widget: ProductSeoWidget,
15
17
  eventHandlers: {
16
- onBeforeSave: async (data, context) => {
17
- const issues: string[] = []
18
- const fieldErrors: Record<string, string> = {}
18
+ onBeforeSave: async (data) => {
19
+ const evaluation = evaluateProductSeo(data as Record<string, unknown>)
19
20
 
20
- const title = data?.title || data?.name
21
- if (typeof title === 'string' && title.length > 0) {
22
- if (title.length < 10) {
23
- issues.push('Title is too short (min 10 characters).')
24
- fieldErrors.title = 'Title is too short for good SEO (min 10 characters).'
25
- } else if (title.length > 60) {
26
- issues.push('Title is too long (max 60 characters recommended).')
27
- fieldErrors.title = 'Title is too long for optimal SEO (max 60 characters).'
28
- }
29
- }
30
-
31
- const description = data?.description
32
- if (typeof description === 'string') {
33
- if (description.trim().length === 0) {
34
- issues.push('Add a product description for better SEO.')
35
- fieldErrors.description = 'Provide a description to help search engines understand this product.'
36
- } else if (description.length < 50) {
37
- issues.push('Description is too short (min 50 characters).')
38
- fieldErrors.description = 'Description is too short for good SEO (min 50 characters).'
39
- }
40
- }
41
-
42
- if (issues.length) {
43
- const message = 'SEO helper blocked save. Improve the highlighted fields.'
44
- publishProductSeoValidation({ ok: false, issues, message })
45
- return { ok: false, message, fieldErrors }
21
+ if (!evaluation.ok) {
22
+ publishProductSeoValidation({ ok: false, issues: evaluation.issues, message: evaluation.message })
23
+ return { ok: false, message: evaluation.message, fieldErrors: evaluation.fieldErrors }
46
24
  }
47
25
 
48
26
  publishProductSeoValidation({ ok: true, issues: [] })