qumra-engine 2.0.55 → 2.0.57
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.
- package/dist/extensions/logic/seo.js +146 -59
- package/dist/utils/asArray.d.ts +1 -0
- package/dist/utils/asArray.js +5 -0
- package/dist/utils/escapeAttr.d.ts +1 -0
- package/dist/utils/escapeAttr.js +10 -0
- package/dist/utils/safeUrl.d.ts +1 -0
- package/dist/utils/safeUrl.js +14 -0
- package/dist/utils/stripEmpty.d.ts +1 -0
- package/dist/utils/stripEmpty.js +20 -0
- package/dist/utils/textOnly.d.ts +1 -0
- package/dist/utils/textOnly.js +11 -0
- package/dist/utils/validateUiData.js +11 -1
- package/package.json +1 -1
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const nunjucks_1 = require("nunjucks");
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const textOnly = (str, limit = 160) => {
|
|
11
|
-
const s = String(str ?? "")
|
|
12
|
-
.replace(/<[^>]*>/g, "") // شيل أي HTML
|
|
13
|
-
.replace(/\s+/g, " ") // سطر واحد
|
|
14
|
-
.trim();
|
|
15
|
-
return limit > 0 && s.length > limit ? s.slice(0, limit - 1).trim() + "…" : s;
|
|
16
|
-
};
|
|
17
|
-
const safeUrl = (str) => {
|
|
18
|
-
const s = String(str ?? "").trim();
|
|
19
|
-
if (!s)
|
|
20
|
-
return "";
|
|
21
|
-
if (/^https?:\/\//i.test(s))
|
|
22
|
-
return s;
|
|
23
|
-
if (s.startsWith("/"))
|
|
24
|
-
return s; // مسار داخلي
|
|
25
|
-
return ""; // غير مسموح
|
|
26
|
-
};
|
|
4
|
+
const escapeAttr_1 = require("../../utils/escapeAttr");
|
|
5
|
+
const textOnly_1 = require("../../utils/textOnly");
|
|
6
|
+
const safeUrl_1 = require("../../utils/safeUrl");
|
|
7
|
+
const stripEmpty_1 = require("../../utils/stripEmpty");
|
|
8
|
+
const asArray_1 = require("../../utils/asArray");
|
|
9
|
+
// أدوات مساعدة داخلية
|
|
27
10
|
exports.default = new (class SeoExtension {
|
|
28
11
|
constructor() {
|
|
29
12
|
this.tags = ["seo"];
|
|
@@ -38,60 +21,164 @@ exports.default = new (class SeoExtension {
|
|
|
38
21
|
const seo = c.seo || {};
|
|
39
22
|
const page = c.page || {};
|
|
40
23
|
const store = c.store || {};
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
const product = page.product || c.product || {};
|
|
25
|
+
// أساسيات
|
|
26
|
+
const siteName = (0, textOnly_1.textOnly)(store?.name || "Store", 70);
|
|
43
27
|
const titleRaw = seo.title || page.title || siteName;
|
|
44
28
|
const descRaw = seo.description || page.description || "";
|
|
45
|
-
const pageTitle = textOnly(titleRaw, 70);
|
|
46
|
-
const pageDesc = textOnly(descRaw, 160);
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const favicon = safeUrl(seo.logo || "/favicon.ico");
|
|
50
|
-
const
|
|
29
|
+
const pageTitle = (0, textOnly_1.textOnly)(titleRaw, 70);
|
|
30
|
+
const pageDesc = (0, textOnly_1.textOnly)(descRaw, 160);
|
|
31
|
+
// روابط وصور
|
|
32
|
+
const canonical = (0, safeUrl_1.safeUrl)(seo.canonical || page.canonical || page.url || "");
|
|
33
|
+
const favicon = (0, safeUrl_1.safeUrl)(seo.logo || store?.logo || "/favicon.ico");
|
|
34
|
+
const ogLocale = String((seo.locale || store.locale || "ar_EG")).replace("-", "_");
|
|
35
|
+
const updatedAt = seo.updatedAt || page.updatedAt;
|
|
36
|
+
// اجمع الصور من مصادر متعددة
|
|
37
|
+
const imageItems = [
|
|
38
|
+
...(0, asArray_1.asArray)(seo.images),
|
|
39
|
+
...(0, asArray_1.asArray)(page.images),
|
|
40
|
+
...(0, asArray_1.asArray)(seo.image),
|
|
41
|
+
...(0, asArray_1.asArray)(page.image),
|
|
42
|
+
...(0, asArray_1.asArray)(product?.images?.map((i) => ({ url: i.fileUrl }))),
|
|
43
|
+
]
|
|
44
|
+
.map((img) => {
|
|
45
|
+
if (!img)
|
|
46
|
+
return null;
|
|
47
|
+
if (typeof img === "string")
|
|
48
|
+
return { url: img, alt: pageTitle };
|
|
49
|
+
return {
|
|
50
|
+
url: img.url || img.src || img.fileUrl,
|
|
51
|
+
width: img.width,
|
|
52
|
+
height: img.height,
|
|
53
|
+
alt: (0, textOnly_1.textOnly)(img.alt || pageTitle, 100),
|
|
54
|
+
};
|
|
55
|
+
})
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.map((img) => ({ ...img, url: (0, safeUrl_1.safeUrl)(img.url) }))
|
|
58
|
+
.filter((img) => !!img.url);
|
|
59
|
+
// نوع الصفحة
|
|
60
|
+
const typeIn = String(seo.type || page.type || (product?._id ? "product" : "website")).toLowerCase();
|
|
61
|
+
const isProduct = typeIn === "product";
|
|
62
|
+
const ogType = isProduct ? "product" : "website";
|
|
51
63
|
// بناء الوسوم
|
|
52
64
|
const parts = [];
|
|
53
|
-
// Title
|
|
54
|
-
parts.push(`<title>${escapeAttr(pageTitle)}</title>`);
|
|
55
|
-
// Canonical
|
|
65
|
+
// Title + Canonical + Favicon + Description
|
|
66
|
+
parts.push(`<title>${(0, escapeAttr_1.escapeAttr)(pageTitle)}</title>`);
|
|
56
67
|
if (canonical)
|
|
57
|
-
parts.push(`<link rel="canonical" href="${escapeAttr(canonical)}">`);
|
|
58
|
-
// Favicon
|
|
68
|
+
parts.push(`<link rel="canonical" href="${(0, escapeAttr_1.escapeAttr)(canonical)}">`);
|
|
59
69
|
if (favicon) {
|
|
60
|
-
parts.push(`<link rel="icon" href="${escapeAttr(favicon)}">`);
|
|
61
|
-
parts.push(`<link rel="shortcut icon" href="${escapeAttr(favicon)}">`);
|
|
70
|
+
parts.push(`<link rel="icon" href="${(0, escapeAttr_1.escapeAttr)(favicon)}">`);
|
|
71
|
+
parts.push(`<link rel="shortcut icon" href="${(0, escapeAttr_1.escapeAttr)(favicon)}">`);
|
|
62
72
|
}
|
|
63
|
-
// Meta description
|
|
64
73
|
if (pageDesc)
|
|
65
|
-
parts.push(`<meta name="description" content="${escapeAttr(pageDesc)}">`);
|
|
74
|
+
parts.push(`<meta name="description" content="${(0, escapeAttr_1.escapeAttr)(pageDesc)}">`);
|
|
66
75
|
// Robots
|
|
67
|
-
if (noindex) {
|
|
76
|
+
if (seo.noindex) {
|
|
68
77
|
parts.push(`<meta name="robots" content="noindex, nofollow">`);
|
|
69
78
|
parts.push(`<meta property="og:robots" content="noindex, nofollow">`);
|
|
70
79
|
}
|
|
71
|
-
// Open Graph
|
|
72
|
-
parts.push(`<meta property="og:type" content="
|
|
73
|
-
parts.push(`<meta property="og:title" content="${escapeAttr(pageTitle)}">`);
|
|
80
|
+
// Open Graph (شامل)
|
|
81
|
+
parts.push(`<meta property="og:type" content="${(0, escapeAttr_1.escapeAttr)(ogType)}">`);
|
|
82
|
+
parts.push(`<meta property="og:title" content="${(0, escapeAttr_1.escapeAttr)(pageTitle)}">`);
|
|
74
83
|
if (pageDesc)
|
|
75
|
-
parts.push(`<meta property="og:description" content="${escapeAttr(pageDesc)}">`);
|
|
84
|
+
parts.push(`<meta property="og:description" content="${(0, escapeAttr_1.escapeAttr)(pageDesc)}">`);
|
|
76
85
|
if (canonical)
|
|
77
|
-
parts.push(`<meta property="og:url" content="${escapeAttr(canonical)}">`);
|
|
86
|
+
parts.push(`<meta property="og:url" content="${(0, escapeAttr_1.escapeAttr)(canonical)}">`);
|
|
78
87
|
if (siteName)
|
|
79
|
-
parts.push(`<meta property="og:site_name" content="${escapeAttr(siteName)}">`);
|
|
80
|
-
if (
|
|
81
|
-
parts.push(`<meta property="og:
|
|
88
|
+
parts.push(`<meta property="og:site_name" content="${(0, escapeAttr_1.escapeAttr)(siteName)}">`);
|
|
89
|
+
if (ogLocale)
|
|
90
|
+
parts.push(`<meta property="og:locale" content="${(0, escapeAttr_1.escapeAttr)(ogLocale)}">`);
|
|
91
|
+
if (updatedAt)
|
|
92
|
+
parts.push(`<meta property="og:updated_time" content="${(0, escapeAttr_1.escapeAttr)(new Date(updatedAt).toISOString())}">`);
|
|
93
|
+
// صور متعددة + alt
|
|
94
|
+
imageItems.forEach((img) => {
|
|
95
|
+
parts.push(`<meta property="og:image" content="${(0, escapeAttr_1.escapeAttr)(img.url)}">`);
|
|
96
|
+
if (img.alt)
|
|
97
|
+
parts.push(`<meta property="og:image:alt" content="${(0, escapeAttr_1.escapeAttr)(img.alt)}">`);
|
|
98
|
+
if (img.width)
|
|
99
|
+
parts.push(`<meta property="og:image:width" content="${(0, escapeAttr_1.escapeAttr)(img.width)}">`);
|
|
100
|
+
if (img.height)
|
|
101
|
+
parts.push(`<meta property="og:image:height" content="${(0, escapeAttr_1.escapeAttr)(img.height)}">`);
|
|
102
|
+
});
|
|
82
103
|
// Twitter
|
|
83
|
-
parts.push(`<meta name="twitter:card" content="${
|
|
84
|
-
parts.push(`<meta name="twitter:title" content="${escapeAttr(pageTitle)}">`);
|
|
104
|
+
parts.push(`<meta name="twitter:card" content="${imageItems.length ? "summary_large_image" : "summary"}">`);
|
|
105
|
+
parts.push(`<meta name="twitter:title" content="${(0, escapeAttr_1.escapeAttr)(pageTitle)}">`);
|
|
85
106
|
if (pageDesc)
|
|
86
|
-
parts.push(`<meta name="twitter:description" content="${escapeAttr(pageDesc)}">`);
|
|
87
|
-
if (
|
|
88
|
-
parts.push(`<meta name="twitter:image" content="${escapeAttr(
|
|
89
|
-
// Itemprop
|
|
90
|
-
parts.push(`<meta itemprop="name" content="${escapeAttr(pageTitle)}">`);
|
|
107
|
+
parts.push(`<meta name="twitter:description" content="${(0, escapeAttr_1.escapeAttr)(pageDesc)}">`);
|
|
108
|
+
if (imageItems[0]?.url)
|
|
109
|
+
parts.push(`<meta name="twitter:image" content="${(0, escapeAttr_1.escapeAttr)(imageItems[0].url)}">`);
|
|
110
|
+
// Itemprop
|
|
111
|
+
parts.push(`<meta itemprop="name" content="${(0, escapeAttr_1.escapeAttr)(pageTitle)}">`);
|
|
91
112
|
if (pageDesc)
|
|
92
|
-
parts.push(`<meta itemprop="description" content="${escapeAttr(pageDesc)}">`);
|
|
93
|
-
if (
|
|
94
|
-
parts.push(`<meta itemprop="image" content="${escapeAttr(
|
|
113
|
+
parts.push(`<meta itemprop="description" content="${(0, escapeAttr_1.escapeAttr)(pageDesc)}">`);
|
|
114
|
+
if (imageItems[0]?.url)
|
|
115
|
+
parts.push(`<meta itemprop="image" content="${(0, escapeAttr_1.escapeAttr)(imageItems[0].url)}">`);
|
|
116
|
+
// ——— JSON-LD Schemas ———
|
|
117
|
+
const schemas = [];
|
|
118
|
+
// Organization (لو فيه اسم المتجر/لوجو)
|
|
119
|
+
if (store?.name || favicon) {
|
|
120
|
+
schemas.push((0, stripEmpty_1.stripEmpty)({
|
|
121
|
+
"@context": "https://schema.org",
|
|
122
|
+
"@type": "Organization",
|
|
123
|
+
"name": siteName || undefined,
|
|
124
|
+
"url": canonical || undefined,
|
|
125
|
+
"logo": favicon || undefined,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
// WebSite (+ SearchAction لو عندك مسار بحث في store.searchUrl أو page.searchUrl)
|
|
129
|
+
const searchUrl = (0, safeUrl_1.safeUrl)(seo.searchUrl || page.searchUrl || store.searchUrl);
|
|
130
|
+
schemas.push((0, stripEmpty_1.stripEmpty)({
|
|
131
|
+
"@context": "https://schema.org",
|
|
132
|
+
"@type": "WebSite",
|
|
133
|
+
"name": siteName || undefined,
|
|
134
|
+
"url": canonical || undefined,
|
|
135
|
+
"potentialAction": searchUrl ? {
|
|
136
|
+
"@type": "SearchAction",
|
|
137
|
+
"target": `${searchUrl}?q={search_term_string}`,
|
|
138
|
+
"query-input": "required name=search_term_string"
|
|
139
|
+
} : undefined
|
|
140
|
+
}));
|
|
141
|
+
// BreadcrumbList لو متاح page.breadcrumbs = [{name,url}]
|
|
142
|
+
const breadcrumbs = (0, asArray_1.asArray)(page.breadcrumbs).map((b, i) => (0, stripEmpty_1.stripEmpty)({
|
|
143
|
+
"@type": "ListItem",
|
|
144
|
+
"position": i + 1,
|
|
145
|
+
"name": (0, textOnly_1.textOnly)(b?.name, 80),
|
|
146
|
+
"item": (0, safeUrl_1.safeUrl)(b?.url)
|
|
147
|
+
})).filter(Boolean);
|
|
148
|
+
if (breadcrumbs.length) {
|
|
149
|
+
schemas.push({
|
|
150
|
+
"@context": "https://schema.org",
|
|
151
|
+
"@type": "BreadcrumbList",
|
|
152
|
+
"itemListElement": breadcrumbs
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// Product Schema لو الصفحة منتج
|
|
156
|
+
if (isProduct) {
|
|
157
|
+
const price = product?.price ?? product?.pricing?.price;
|
|
158
|
+
const currency = product?.currency || product?.pricing?.currency || store?.currency || "EGP";
|
|
159
|
+
const inStock = product?.inStock ?? (typeof product?.quantity === "number" ? product.quantity > 0 : undefined);
|
|
160
|
+
const availability = inStock == null ? undefined : (inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock");
|
|
161
|
+
const offers = (0, stripEmpty_1.stripEmpty)({
|
|
162
|
+
"@type": "Offer",
|
|
163
|
+
"price": price != null ? Number(price) : undefined,
|
|
164
|
+
"priceCurrency": currency,
|
|
165
|
+
"availability": availability,
|
|
166
|
+
"url": canonical || undefined,
|
|
167
|
+
});
|
|
168
|
+
schemas.push((0, stripEmpty_1.stripEmpty)({
|
|
169
|
+
"@context": "https://schema.org",
|
|
170
|
+
"@type": "Product",
|
|
171
|
+
"name": (0, textOnly_1.textOnly)(product?.title || pageTitle, 120),
|
|
172
|
+
"description": pageDesc || undefined,
|
|
173
|
+
"image": imageItems.map(i => i.url),
|
|
174
|
+
"sku": product?.sku || product?.id || product?.handle || undefined,
|
|
175
|
+
"brand": store?.name ? { "@type": "Brand", "name": siteName } : undefined,
|
|
176
|
+
"offers": offers
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
if (schemas.length) {
|
|
180
|
+
parts.push(`<script type="application/ld+json">${JSON.stringify((0, stripEmpty_1.stripEmpty)(schemas), null, 0)}</script>`);
|
|
181
|
+
}
|
|
95
182
|
// إخراج منظّف
|
|
96
183
|
const html = parts.join("\n");
|
|
97
184
|
return new nunjucks_1.runtime.SafeString(html);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const asArray: <T = unknown>(x: T | T[] | undefined | null) => T[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const escapeAttr: (str: unknown) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.escapeAttr = void 0;
|
|
4
|
+
const escapeAttr = (str) => String(str ?? "")
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/</g, "<")
|
|
7
|
+
.replace(/>/g, ">")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
.replace(/'/g, "'");
|
|
10
|
+
exports.escapeAttr = escapeAttr;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const safeUrl: (str: unknown) => string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.safeUrl = void 0;
|
|
4
|
+
const safeUrl = (str) => {
|
|
5
|
+
const s = String(str ?? "").trim();
|
|
6
|
+
if (!s)
|
|
7
|
+
return "";
|
|
8
|
+
if (/^https?:\/\//i.test(s))
|
|
9
|
+
return s;
|
|
10
|
+
if (s.startsWith("/"))
|
|
11
|
+
return s; // مسار داخلي
|
|
12
|
+
return ""; // غير مسموح
|
|
13
|
+
};
|
|
14
|
+
exports.safeUrl = safeUrl;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const stripEmpty: (obj: any) => any;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripEmpty = void 0;
|
|
4
|
+
const stripEmpty = (obj) => {
|
|
5
|
+
if (obj == null)
|
|
6
|
+
return obj;
|
|
7
|
+
if (Array.isArray(obj))
|
|
8
|
+
return obj.map(exports.stripEmpty).filter(v => v != null && (typeof v !== "object" || Object.keys(v).length));
|
|
9
|
+
if (typeof obj === "object") {
|
|
10
|
+
const out = {};
|
|
11
|
+
for (const k of Object.keys(obj)) {
|
|
12
|
+
const v = (0, exports.stripEmpty)(obj[k]);
|
|
13
|
+
if (v !== undefined && v !== null && !(typeof v === "string" && v.trim() === ""))
|
|
14
|
+
out[k] = v;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
return obj;
|
|
19
|
+
};
|
|
20
|
+
exports.stripEmpty = stripEmpty;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const textOnly: (str: unknown, limit?: number) => string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.textOnly = void 0;
|
|
4
|
+
const textOnly = (str, limit = 160) => {
|
|
5
|
+
const s = String(str ?? "")
|
|
6
|
+
.replace(/<[^>]*>/g, "") // شيل أي HTML
|
|
7
|
+
.replace(/\s+/g, " ") // سطر واحد
|
|
8
|
+
.trim();
|
|
9
|
+
return limit > 0 && s.length > limit ? s.slice(0, limit - 1).trim() + "…" : s;
|
|
10
|
+
};
|
|
11
|
+
exports.textOnly = textOnly;
|
|
@@ -14,7 +14,17 @@ function validateUiData(data) {
|
|
|
14
14
|
for (const [key, value] of Object.entries(item)) {
|
|
15
15
|
if (value === true)
|
|
16
16
|
continue; // استبعاد مثل __keywords: true
|
|
17
|
-
|
|
17
|
+
// دعم الأنواع المسموح بها
|
|
18
|
+
if (typeof value === "string" ||
|
|
19
|
+
typeof value === "number" ||
|
|
20
|
+
typeof value === "boolean" ||
|
|
21
|
+
Array.isArray(value) ||
|
|
22
|
+
(typeof value === "object" && value !== null)) {
|
|
23
|
+
result[key] = value;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new Error(`❌ نوع قيمة غير مدعوم في المفتاح "${key}": ${JSON.stringify(value)}`);
|
|
27
|
+
}
|
|
18
28
|
}
|
|
19
29
|
}
|
|
20
30
|
else {
|