qumra-engine 2.0.54 → 2.0.56

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.
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const nunjucks_1 = require("nunjucks");
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
+ // أدوات مساعدة داخلية
4
10
  exports.default = new (class SeoExtension {
5
11
  constructor() {
6
12
  this.tags = ["seo"];
@@ -11,45 +17,170 @@ exports.default = new (class SeoExtension {
11
17
  return new nodes.CallExtension(this, "run", null);
12
18
  }
13
19
  run({ ctx }) {
14
- // const escapeHtml = (str: string) =>
15
- // String(str || "")
16
- // .replace(/&/g, "&")
17
- // .replace(/</g, "&lt;")
18
- // .replace(/>/g, "&gt;")
19
- // .replace(/"/g, "&quot;");
20
- const getTextOnly = (str) => String(str || "").replace(/<[^>]*>/g, "");
21
- const seo = ctx.context.seo || {};
22
- const page = ctx.context.page || {};
23
- const store = ctx.context.store || {};
24
- const pageUrl = seo.canonical || "";
25
- const pageTitle = seo.title || page.title || "Store";
26
- const pageDescription = seo.description || page.description || "";
27
- const pageImage = seo.image || "";
28
- const favicon = seo.logo || "/favicon.ico"; // الأولوية للوجو
29
- let tags = `
30
- <title>${getTextOnly(pageTitle)}</title>
31
- ${favicon
32
- ? `<link rel="icon" type="image/png" href="${getTextOnly(favicon)}">`
33
- : ""}
34
- ${pageDescription
35
- ? `<meta name="description" content="${getTextOnly(pageDescription)}">`
36
- : ""}
37
- ${pageUrl ? `<link rel="canonical" href="${getTextOnly(pageUrl)}">` : ""}
38
- <meta property="og:title" content="${getTextOnly(pageTitle)}">
39
- ${pageDescription
40
- ? `<meta property="og:description" content="${getTextOnly(pageDescription)}">`
41
- : ""}
42
- ${pageImage
43
- ? `<meta property="og:image" content="${getTextOnly(pageImage)}">`
44
- : ""}
45
- ${pageUrl
46
- ? `<meta property="og:url" content="${getTextOnly(pageUrl)}">`
47
- : ""}
48
- <meta property="og:type" content="website">
49
- ${pageImage
50
- ? `<meta name="twitter:card" content="summary_large_image">`
51
- : `<meta name="twitter:card" content="summary">`}
52
- `;
53
- return new nunjucks_1.runtime.SafeString(tags.trim());
20
+ const c = ctx.context || {};
21
+ const seo = c.seo || {};
22
+ const page = c.page || {};
23
+ const store = c.store || {};
24
+ const product = page.product || c.product || {};
25
+ // أساسيات
26
+ const siteName = (0, textOnly_1.textOnly)(store?.name || "Store", 70);
27
+ const titleRaw = seo.title || page.title || siteName;
28
+ const descRaw = seo.description || page.description || "";
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";
63
+ // بناء الوسوم
64
+ const parts = [];
65
+ // Title + Canonical + Favicon + Description
66
+ parts.push(`<title>${(0, escapeAttr_1.escapeAttr)(pageTitle)}</title>`);
67
+ if (canonical)
68
+ parts.push(`<link rel="canonical" href="${(0, escapeAttr_1.escapeAttr)(canonical)}">`);
69
+ if (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)}">`);
72
+ }
73
+ if (pageDesc)
74
+ parts.push(`<meta name="description" content="${(0, escapeAttr_1.escapeAttr)(pageDesc)}">`);
75
+ // Robots
76
+ if (seo.noindex) {
77
+ parts.push(`<meta name="robots" content="noindex, nofollow">`);
78
+ parts.push(`<meta property="og:robots" content="noindex, nofollow">`);
79
+ }
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)}">`);
83
+ if (pageDesc)
84
+ parts.push(`<meta property="og:description" content="${(0, escapeAttr_1.escapeAttr)(pageDesc)}">`);
85
+ if (canonical)
86
+ parts.push(`<meta property="og:url" content="${(0, escapeAttr_1.escapeAttr)(canonical)}">`);
87
+ if (siteName)
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
+ });
103
+ // Twitter
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)}">`);
106
+ if (pageDesc)
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)}">`);
112
+ if (pageDesc)
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
+ }
182
+ // إخراج منظّف
183
+ const html = parts.join("\n");
184
+ return new nunjucks_1.runtime.SafeString(html);
54
185
  }
55
186
  })();
@@ -0,0 +1 @@
1
+ export declare const asArray: <T = unknown>(x: T | T[] | undefined | null) => T[];
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.asArray = void 0;
4
+ const asArray = (x) => Array.isArray(x) ? x : x != null ? [x] : [];
5
+ exports.asArray = asArray;
@@ -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, "&amp;")
6
+ .replace(/</g, "&lt;")
7
+ .replace(/>/g, "&gt;")
8
+ .replace(/"/g, "&quot;")
9
+ .replace(/'/g, "&#39;");
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qumra-engine",
3
- "version": "2.0.54",
3
+ "version": "2.0.56",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {