@symbo.ls/smbls-utils 3.7.4 → 3.7.5
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/cjs/metadata.js +130 -8
- package/dist/esm/metadata.js +130 -8
- package/dist/iife/index.js +130 -8
- package/package.json +3 -3
- package/src/metadata.js +186 -8
package/dist/cjs/metadata.js
CHANGED
|
@@ -180,17 +180,139 @@ function resolveFileReferences(metadata, files) {
|
|
|
180
180
|
}
|
|
181
181
|
return result;
|
|
182
182
|
}
|
|
183
|
-
function
|
|
183
|
+
function resolveDotPath(obj, path) {
|
|
184
|
+
if (!obj || !path) return void 0;
|
|
185
|
+
const parts = path.split(".");
|
|
186
|
+
let v = obj;
|
|
187
|
+
for (const p of parts) {
|
|
188
|
+
if (v == null || typeof v !== "object") return void 0;
|
|
189
|
+
v = v[p];
|
|
190
|
+
}
|
|
191
|
+
return v;
|
|
192
|
+
}
|
|
193
|
+
function resolveMetadataTemplates(metadata, data, ssrContext) {
|
|
194
|
+
const config = data.config || data.settings || {};
|
|
195
|
+
const polyglot = config.polyglot || data.polyglot;
|
|
196
|
+
const defaultLang = ssrContext?.lang || polyglot?.defaultLang || "en";
|
|
197
|
+
const translations = polyglot?.translations || {};
|
|
198
|
+
const langMap = translations[defaultLang] || {};
|
|
199
|
+
const state = ssrContext?.state || {};
|
|
200
|
+
const result = { ...metadata };
|
|
201
|
+
for (const [key, value] of Object.entries(result)) {
|
|
202
|
+
if (typeof value !== "string" || !value.includes("{{")) continue;
|
|
203
|
+
result[key] = value.replace(/\{\{\s*([^|{}]+?)\s*(?:\|\s*(\w+)\s*)?\}\}/g, (match, k, filter) => {
|
|
204
|
+
const trimmed = k.trim();
|
|
205
|
+
if (filter === "polyglot") {
|
|
206
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match;
|
|
207
|
+
}
|
|
208
|
+
if (filter === "getLocalStateLang") {
|
|
209
|
+
const resolved = resolveDotPath(state, trimmed + defaultLang);
|
|
210
|
+
return resolved ?? match;
|
|
211
|
+
}
|
|
212
|
+
const fromState = resolveDotPath(state, trimmed);
|
|
213
|
+
if (fromState !== void 0) return fromState;
|
|
214
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
function convertFunctionMetaToTemplates(metadata) {
|
|
220
|
+
const result = { ...metadata };
|
|
221
|
+
for (const [key, value] of Object.entries(result)) {
|
|
222
|
+
if (typeof value === "function") {
|
|
223
|
+
const src = value.toString();
|
|
224
|
+
const langMatch = src.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
225
|
+
if (langMatch) {
|
|
226
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const polyMatch = src.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
230
|
+
if (polyMatch) {
|
|
231
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const stateMatch = src.match(/s\.(\w+(?:\.\w+)+)/);
|
|
235
|
+
if (stateMatch) {
|
|
236
|
+
result[key] = `{{ ${stateMatch[1]} }}`;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
delete result[key];
|
|
240
|
+
} else if (typeof value === "string") {
|
|
241
|
+
const langMatch = value.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
242
|
+
if (langMatch) {
|
|
243
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const polyMatch = value.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
247
|
+
if (polyMatch) {
|
|
248
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (value.includes("=>") && value.includes("s.")) {
|
|
252
|
+
const stateMatch = value.match(/s\.(\w+(?:\.\w+)+)/);
|
|
253
|
+
if (stateMatch) {
|
|
254
|
+
result[key] = `{{ ${stateMatch[1]} }}`;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
function getPageMetadata(data, pathname, ssrContext) {
|
|
184
263
|
const currentPage = data.pages?.[pathname];
|
|
185
264
|
const stateObject = (0, import_utils.isObject)(currentPage?.state) && currentPage?.state;
|
|
186
|
-
|
|
265
|
+
const rawPageMeta = currentPage?.metadata || currentPage?.helmet || stateObject || {};
|
|
266
|
+
const pageMetadata = convertFunctionMetaToTemplates(rawPageMeta);
|
|
267
|
+
const pageExplicitKeys = new Set(Object.keys(pageMetadata));
|
|
187
268
|
const appMetadata = data.app?.metadata || {};
|
|
269
|
+
const cleanAppMeta = convertFunctionMetaToTemplates(appMetadata);
|
|
270
|
+
let merged = {};
|
|
188
271
|
if (data.integrations?.seo) {
|
|
189
|
-
|
|
190
|
-
} else if (Object.keys(
|
|
191
|
-
|
|
272
|
+
merged = { ...data.integrations.seo, ...cleanAppMeta, ...pageMetadata };
|
|
273
|
+
} else if (Object.keys(cleanAppMeta).length) {
|
|
274
|
+
merged = { ...cleanAppMeta, ...pageMetadata };
|
|
275
|
+
} else {
|
|
276
|
+
merged = { ...pageMetadata };
|
|
277
|
+
}
|
|
278
|
+
if (!merged.title) merged.title = data.name + " / symbo.ls" || "Symbols demo";
|
|
279
|
+
merged = resolveMetadataTemplates(merged, data, ssrContext);
|
|
280
|
+
if (merged.title && (pageExplicitKeys.has("title") || !merged["og:title"])) {
|
|
281
|
+
if (!pageExplicitKeys.has("og:title")) {
|
|
282
|
+
merged["og:title"] = merged.title;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (merged.description && (pageExplicitKeys.has("description") || !merged["og:description"])) {
|
|
286
|
+
if (!pageExplicitKeys.has("og:description")) {
|
|
287
|
+
merged["og:description"] = merged.description;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (merged.title && !merged["twitter:title"]) {
|
|
291
|
+
merged["twitter:title"] = merged.title;
|
|
292
|
+
}
|
|
293
|
+
if (merged.description && !merged["twitter:description"]) {
|
|
294
|
+
merged["twitter:description"] = merged.description;
|
|
295
|
+
}
|
|
296
|
+
const routeForUrl = ssrContext?.actualPathname || pathname;
|
|
297
|
+
if (merged["og:url"] || merged.url) {
|
|
298
|
+
const baseUrl = (merged["og:url"] || merged.url || "").replace(/\/$/, "");
|
|
299
|
+
if (baseUrl && routeForUrl && routeForUrl !== "/") {
|
|
300
|
+
const cleanRoute = routeForUrl.startsWith("/") ? routeForUrl : "/" + routeForUrl;
|
|
301
|
+
merged["og:url"] = baseUrl + cleanRoute;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
merged = resolveFileReferences(merged, data.files);
|
|
305
|
+
const siteName = data.name || data.app?.metadata?.title || data.app?.name || "";
|
|
306
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
307
|
+
if (typeof value === "string" && value.includes("{{")) {
|
|
308
|
+
const cleaned = value.replace(/\{\{[^}]*\}\}/g, "").trim();
|
|
309
|
+
if (!cleaned) {
|
|
310
|
+
if (key === "title") merged[key] = siteName;
|
|
311
|
+
else delete merged[key];
|
|
312
|
+
} else {
|
|
313
|
+
merged[key] = cleaned;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
192
316
|
}
|
|
193
|
-
|
|
194
|
-
pageMetadata = resolveFileReferences(pageMetadata, data.files);
|
|
195
|
-
return pageMetadata;
|
|
317
|
+
return merged;
|
|
196
318
|
}
|
package/dist/esm/metadata.js
CHANGED
|
@@ -156,19 +156,141 @@ function resolveFileReferences(metadata, files) {
|
|
|
156
156
|
}
|
|
157
157
|
return result;
|
|
158
158
|
}
|
|
159
|
-
function
|
|
159
|
+
function resolveDotPath(obj, path) {
|
|
160
|
+
if (!obj || !path) return void 0;
|
|
161
|
+
const parts = path.split(".");
|
|
162
|
+
let v = obj;
|
|
163
|
+
for (const p of parts) {
|
|
164
|
+
if (v == null || typeof v !== "object") return void 0;
|
|
165
|
+
v = v[p];
|
|
166
|
+
}
|
|
167
|
+
return v;
|
|
168
|
+
}
|
|
169
|
+
function resolveMetadataTemplates(metadata, data, ssrContext) {
|
|
170
|
+
const config = data.config || data.settings || {};
|
|
171
|
+
const polyglot = config.polyglot || data.polyglot;
|
|
172
|
+
const defaultLang = ssrContext?.lang || polyglot?.defaultLang || "en";
|
|
173
|
+
const translations = polyglot?.translations || {};
|
|
174
|
+
const langMap = translations[defaultLang] || {};
|
|
175
|
+
const state = ssrContext?.state || {};
|
|
176
|
+
const result = { ...metadata };
|
|
177
|
+
for (const [key, value] of Object.entries(result)) {
|
|
178
|
+
if (typeof value !== "string" || !value.includes("{{")) continue;
|
|
179
|
+
result[key] = value.replace(/\{\{\s*([^|{}]+?)\s*(?:\|\s*(\w+)\s*)?\}\}/g, (match, k, filter) => {
|
|
180
|
+
const trimmed = k.trim();
|
|
181
|
+
if (filter === "polyglot") {
|
|
182
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match;
|
|
183
|
+
}
|
|
184
|
+
if (filter === "getLocalStateLang") {
|
|
185
|
+
const resolved = resolveDotPath(state, trimmed + defaultLang);
|
|
186
|
+
return resolved ?? match;
|
|
187
|
+
}
|
|
188
|
+
const fromState = resolveDotPath(state, trimmed);
|
|
189
|
+
if (fromState !== void 0) return fromState;
|
|
190
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
function convertFunctionMetaToTemplates(metadata) {
|
|
196
|
+
const result = { ...metadata };
|
|
197
|
+
for (const [key, value] of Object.entries(result)) {
|
|
198
|
+
if (typeof value === "function") {
|
|
199
|
+
const src = value.toString();
|
|
200
|
+
const langMatch = src.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
201
|
+
if (langMatch) {
|
|
202
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const polyMatch = src.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
206
|
+
if (polyMatch) {
|
|
207
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const stateMatch = src.match(/s\.(\w+(?:\.\w+)+)/);
|
|
211
|
+
if (stateMatch) {
|
|
212
|
+
result[key] = `{{ ${stateMatch[1]} }}`;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
delete result[key];
|
|
216
|
+
} else if (typeof value === "string") {
|
|
217
|
+
const langMatch = value.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
218
|
+
if (langMatch) {
|
|
219
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const polyMatch = value.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
223
|
+
if (polyMatch) {
|
|
224
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (value.includes("=>") && value.includes("s.")) {
|
|
228
|
+
const stateMatch = value.match(/s\.(\w+(?:\.\w+)+)/);
|
|
229
|
+
if (stateMatch) {
|
|
230
|
+
result[key] = `{{ ${stateMatch[1]} }}`;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
function getPageMetadata(data, pathname, ssrContext) {
|
|
160
239
|
const currentPage = data.pages?.[pathname];
|
|
161
240
|
const stateObject = isObject(currentPage?.state) && currentPage?.state;
|
|
162
|
-
|
|
241
|
+
const rawPageMeta = currentPage?.metadata || currentPage?.helmet || stateObject || {};
|
|
242
|
+
const pageMetadata = convertFunctionMetaToTemplates(rawPageMeta);
|
|
243
|
+
const pageExplicitKeys = new Set(Object.keys(pageMetadata));
|
|
163
244
|
const appMetadata = data.app?.metadata || {};
|
|
245
|
+
const cleanAppMeta = convertFunctionMetaToTemplates(appMetadata);
|
|
246
|
+
let merged = {};
|
|
164
247
|
if (data.integrations?.seo) {
|
|
165
|
-
|
|
166
|
-
} else if (Object.keys(
|
|
167
|
-
|
|
248
|
+
merged = { ...data.integrations.seo, ...cleanAppMeta, ...pageMetadata };
|
|
249
|
+
} else if (Object.keys(cleanAppMeta).length) {
|
|
250
|
+
merged = { ...cleanAppMeta, ...pageMetadata };
|
|
251
|
+
} else {
|
|
252
|
+
merged = { ...pageMetadata };
|
|
253
|
+
}
|
|
254
|
+
if (!merged.title) merged.title = data.name + " / symbo.ls" || "Symbols demo";
|
|
255
|
+
merged = resolveMetadataTemplates(merged, data, ssrContext);
|
|
256
|
+
if (merged.title && (pageExplicitKeys.has("title") || !merged["og:title"])) {
|
|
257
|
+
if (!pageExplicitKeys.has("og:title")) {
|
|
258
|
+
merged["og:title"] = merged.title;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (merged.description && (pageExplicitKeys.has("description") || !merged["og:description"])) {
|
|
262
|
+
if (!pageExplicitKeys.has("og:description")) {
|
|
263
|
+
merged["og:description"] = merged.description;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (merged.title && !merged["twitter:title"]) {
|
|
267
|
+
merged["twitter:title"] = merged.title;
|
|
268
|
+
}
|
|
269
|
+
if (merged.description && !merged["twitter:description"]) {
|
|
270
|
+
merged["twitter:description"] = merged.description;
|
|
271
|
+
}
|
|
272
|
+
const routeForUrl = ssrContext?.actualPathname || pathname;
|
|
273
|
+
if (merged["og:url"] || merged.url) {
|
|
274
|
+
const baseUrl = (merged["og:url"] || merged.url || "").replace(/\/$/, "");
|
|
275
|
+
if (baseUrl && routeForUrl && routeForUrl !== "/") {
|
|
276
|
+
const cleanRoute = routeForUrl.startsWith("/") ? routeForUrl : "/" + routeForUrl;
|
|
277
|
+
merged["og:url"] = baseUrl + cleanRoute;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
merged = resolveFileReferences(merged, data.files);
|
|
281
|
+
const siteName = data.name || data.app?.metadata?.title || data.app?.name || "";
|
|
282
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
283
|
+
if (typeof value === "string" && value.includes("{{")) {
|
|
284
|
+
const cleaned = value.replace(/\{\{[^}]*\}\}/g, "").trim();
|
|
285
|
+
if (!cleaned) {
|
|
286
|
+
if (key === "title") merged[key] = siteName;
|
|
287
|
+
else delete merged[key];
|
|
288
|
+
} else {
|
|
289
|
+
merged[key] = cleaned;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
168
292
|
}
|
|
169
|
-
|
|
170
|
-
pageMetadata = resolveFileReferences(pageMetadata, data.files);
|
|
171
|
-
return pageMetadata;
|
|
293
|
+
return merged;
|
|
172
294
|
}
|
|
173
295
|
export {
|
|
174
296
|
generateMetaTags,
|
package/dist/iife/index.js
CHANGED
|
@@ -735,19 +735,141 @@ var SmblsSmblsUtils = (() => {
|
|
|
735
735
|
}
|
|
736
736
|
return result;
|
|
737
737
|
}
|
|
738
|
-
function
|
|
738
|
+
function resolveDotPath(obj, path) {
|
|
739
|
+
if (!obj || !path) return void 0;
|
|
740
|
+
const parts = path.split(".");
|
|
741
|
+
let v = obj;
|
|
742
|
+
for (const p of parts) {
|
|
743
|
+
if (v == null || typeof v !== "object") return void 0;
|
|
744
|
+
v = v[p];
|
|
745
|
+
}
|
|
746
|
+
return v;
|
|
747
|
+
}
|
|
748
|
+
function resolveMetadataTemplates(metadata, data, ssrContext) {
|
|
749
|
+
const config = data.config || data.settings || {};
|
|
750
|
+
const polyglot = config.polyglot || data.polyglot;
|
|
751
|
+
const defaultLang = ssrContext?.lang || polyglot?.defaultLang || "en";
|
|
752
|
+
const translations = polyglot?.translations || {};
|
|
753
|
+
const langMap = translations[defaultLang] || {};
|
|
754
|
+
const state = ssrContext?.state || {};
|
|
755
|
+
const result = { ...metadata };
|
|
756
|
+
for (const [key, value] of Object.entries(result)) {
|
|
757
|
+
if (typeof value !== "string" || !value.includes("{{")) continue;
|
|
758
|
+
result[key] = value.replace(/\{\{\s*([^|{}]+?)\s*(?:\|\s*(\w+)\s*)?\}\}/g, (match, k, filter) => {
|
|
759
|
+
const trimmed = k.trim();
|
|
760
|
+
if (filter === "polyglot") {
|
|
761
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match;
|
|
762
|
+
}
|
|
763
|
+
if (filter === "getLocalStateLang") {
|
|
764
|
+
const resolved = resolveDotPath(state, trimmed + defaultLang);
|
|
765
|
+
return resolved ?? match;
|
|
766
|
+
}
|
|
767
|
+
const fromState = resolveDotPath(state, trimmed);
|
|
768
|
+
if (fromState !== void 0) return fromState;
|
|
769
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match;
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return result;
|
|
773
|
+
}
|
|
774
|
+
function convertFunctionMetaToTemplates(metadata) {
|
|
775
|
+
const result = { ...metadata };
|
|
776
|
+
for (const [key, value] of Object.entries(result)) {
|
|
777
|
+
if (typeof value === "function") {
|
|
778
|
+
const src = value.toString();
|
|
779
|
+
const langMatch = src.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
780
|
+
if (langMatch) {
|
|
781
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const polyMatch = src.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
785
|
+
if (polyMatch) {
|
|
786
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`;
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const stateMatch = src.match(/s\.(\w+(?:\.\w+)+)/);
|
|
790
|
+
if (stateMatch) {
|
|
791
|
+
result[key] = `{{ ${stateMatch[1]} }}`;
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
delete result[key];
|
|
795
|
+
} else if (typeof value === "string") {
|
|
796
|
+
const langMatch = value.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
797
|
+
if (langMatch) {
|
|
798
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const polyMatch = value.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/);
|
|
802
|
+
if (polyMatch) {
|
|
803
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (value.includes("=>") && value.includes("s.")) {
|
|
807
|
+
const stateMatch = value.match(/s\.(\w+(?:\.\w+)+)/);
|
|
808
|
+
if (stateMatch) {
|
|
809
|
+
result[key] = `{{ ${stateMatch[1]} }}`;
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
function getPageMetadata(data, pathname, ssrContext) {
|
|
739
818
|
const currentPage = data.pages?.[pathname];
|
|
740
819
|
const stateObject = isObject(currentPage?.state) && currentPage?.state;
|
|
741
|
-
|
|
820
|
+
const rawPageMeta = currentPage?.metadata || currentPage?.helmet || stateObject || {};
|
|
821
|
+
const pageMetadata = convertFunctionMetaToTemplates(rawPageMeta);
|
|
822
|
+
const pageExplicitKeys = new Set(Object.keys(pageMetadata));
|
|
742
823
|
const appMetadata = data.app?.metadata || {};
|
|
824
|
+
const cleanAppMeta = convertFunctionMetaToTemplates(appMetadata);
|
|
825
|
+
let merged = {};
|
|
743
826
|
if (data.integrations?.seo) {
|
|
744
|
-
|
|
745
|
-
} else if (Object.keys(
|
|
746
|
-
|
|
827
|
+
merged = { ...data.integrations.seo, ...cleanAppMeta, ...pageMetadata };
|
|
828
|
+
} else if (Object.keys(cleanAppMeta).length) {
|
|
829
|
+
merged = { ...cleanAppMeta, ...pageMetadata };
|
|
830
|
+
} else {
|
|
831
|
+
merged = { ...pageMetadata };
|
|
832
|
+
}
|
|
833
|
+
if (!merged.title) merged.title = data.name + " / symbo.ls" || "Symbols demo";
|
|
834
|
+
merged = resolveMetadataTemplates(merged, data, ssrContext);
|
|
835
|
+
if (merged.title && (pageExplicitKeys.has("title") || !merged["og:title"])) {
|
|
836
|
+
if (!pageExplicitKeys.has("og:title")) {
|
|
837
|
+
merged["og:title"] = merged.title;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (merged.description && (pageExplicitKeys.has("description") || !merged["og:description"])) {
|
|
841
|
+
if (!pageExplicitKeys.has("og:description")) {
|
|
842
|
+
merged["og:description"] = merged.description;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (merged.title && !merged["twitter:title"]) {
|
|
846
|
+
merged["twitter:title"] = merged.title;
|
|
847
|
+
}
|
|
848
|
+
if (merged.description && !merged["twitter:description"]) {
|
|
849
|
+
merged["twitter:description"] = merged.description;
|
|
850
|
+
}
|
|
851
|
+
const routeForUrl = ssrContext?.actualPathname || pathname;
|
|
852
|
+
if (merged["og:url"] || merged.url) {
|
|
853
|
+
const baseUrl = (merged["og:url"] || merged.url || "").replace(/\/$/, "");
|
|
854
|
+
if (baseUrl && routeForUrl && routeForUrl !== "/") {
|
|
855
|
+
const cleanRoute = routeForUrl.startsWith("/") ? routeForUrl : "/" + routeForUrl;
|
|
856
|
+
merged["og:url"] = baseUrl + cleanRoute;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
merged = resolveFileReferences(merged, data.files);
|
|
860
|
+
const siteName = data.name || data.app?.metadata?.title || data.app?.name || "";
|
|
861
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
862
|
+
if (typeof value === "string" && value.includes("{{")) {
|
|
863
|
+
const cleaned = value.replace(/\{\{[^}]*\}\}/g, "").trim();
|
|
864
|
+
if (!cleaned) {
|
|
865
|
+
if (key === "title") merged[key] = siteName;
|
|
866
|
+
else delete merged[key];
|
|
867
|
+
} else {
|
|
868
|
+
merged[key] = cleaned;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
747
871
|
}
|
|
748
|
-
|
|
749
|
-
pageMetadata = resolveFileReferences(pageMetadata, data.files);
|
|
750
|
-
return pageMetadata;
|
|
872
|
+
return merged;
|
|
751
873
|
}
|
|
752
874
|
|
|
753
875
|
// src/index.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/smbls-utils",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.5",
|
|
4
4
|
"author": "symbo.ls",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"license": "CC-BY-NC-4.0",
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@domql/element": "^3.7.
|
|
36
|
-
"@domql/utils": "^3.7.
|
|
35
|
+
"@domql/element": "^3.7.5",
|
|
36
|
+
"@domql/utils": "^3.7.5"
|
|
37
37
|
},
|
|
38
38
|
"gitHead": "9fc1b79b41cdc725ca6b24aec64920a599634681",
|
|
39
39
|
"browser": "./dist/esm/index.js",
|
package/src/metadata.js
CHANGED
|
@@ -209,17 +209,195 @@ function resolveFileReferences (metadata, files) {
|
|
|
209
209
|
return result
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Resolve a dot-path like "item.title_ka" against an object.
|
|
214
|
+
*/
|
|
215
|
+
function resolveDotPath (obj, path) {
|
|
216
|
+
if (!obj || !path) return undefined
|
|
217
|
+
const parts = path.split('.')
|
|
218
|
+
let v = obj
|
|
219
|
+
for (const p of parts) {
|
|
220
|
+
if (v == null || typeof v !== 'object') return undefined
|
|
221
|
+
v = v[p]
|
|
222
|
+
}
|
|
223
|
+
return v
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolve {{ key | polyglot }} and {{ key | getLocalStateLang }} templates
|
|
228
|
+
* in metadata string values using project polyglot translations and SSR state.
|
|
229
|
+
*/
|
|
230
|
+
function resolveMetadataTemplates (metadata, data, ssrContext) {
|
|
231
|
+
const config = data.config || data.settings || {}
|
|
232
|
+
const polyglot = config.polyglot || data.polyglot
|
|
233
|
+
const defaultLang = ssrContext?.lang || polyglot?.defaultLang || 'en'
|
|
234
|
+
const translations = polyglot?.translations || {}
|
|
235
|
+
const langMap = translations[defaultLang] || {}
|
|
236
|
+
const state = ssrContext?.state || {}
|
|
237
|
+
|
|
238
|
+
const result = { ...metadata }
|
|
239
|
+
for (const [key, value] of Object.entries(result)) {
|
|
240
|
+
if (typeof value !== 'string' || !value.includes('{{')) continue
|
|
241
|
+
|
|
242
|
+
result[key] = value.replace(/\{\{\s*([^|{}]+?)\s*(?:\|\s*(\w+)\s*)?\}\}/g, (match, k, filter) => {
|
|
243
|
+
const trimmed = k.trim()
|
|
244
|
+
|
|
245
|
+
if (filter === 'polyglot') {
|
|
246
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (filter === 'getLocalStateLang') {
|
|
250
|
+
// key is like "item.title_" — append lang
|
|
251
|
+
const resolved = resolveDotPath(state, trimmed + defaultLang)
|
|
252
|
+
return resolved ?? match
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// No filter — try state lookup, then polyglot
|
|
256
|
+
const fromState = resolveDotPath(state, trimmed)
|
|
257
|
+
if (fromState !== undefined) return fromState
|
|
258
|
+
|
|
259
|
+
return langMap[trimmed] ?? resolveDotPath(langMap, trimmed) ?? match
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
return result
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Convert function-string metadata values to {{ }} templates.
|
|
267
|
+
* Handles common patterns from Symbols page definitions:
|
|
268
|
+
* (el, s) => s.item ? el.call("getLocalStateLang", "item.title_") : ""
|
|
269
|
+
* (el, s) => s.item ? s.item.image_url : ""
|
|
270
|
+
*/
|
|
271
|
+
function convertFunctionMetaToTemplates (metadata) {
|
|
272
|
+
const result = { ...metadata }
|
|
273
|
+
for (const [key, value] of Object.entries(result)) {
|
|
274
|
+
if (typeof value === 'function') {
|
|
275
|
+
const src = value.toString()
|
|
276
|
+
// Pattern: el.call("getLocalStateLang", "item.title_")
|
|
277
|
+
const langMatch = src.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
278
|
+
if (langMatch) {
|
|
279
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
// Pattern: el.call("polyglot", "item.description")
|
|
283
|
+
const polyMatch = src.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
284
|
+
if (polyMatch) {
|
|
285
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
// Pattern: s.item.field or s.item?.field
|
|
289
|
+
const stateMatch = src.match(/s\.(\w+(?:\.\w+)+)/)
|
|
290
|
+
if (stateMatch) {
|
|
291
|
+
result[key] = `{{ ${stateMatch[1]} }}`
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
// Could not parse — drop function value
|
|
295
|
+
delete result[key]
|
|
296
|
+
} else if (typeof value === 'string') {
|
|
297
|
+
// Function-string (not yet destringified)
|
|
298
|
+
const langMatch = value.match(/el\.call\(\s*["']getLocalStateLang["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
299
|
+
if (langMatch) {
|
|
300
|
+
result[key] = `{{ ${langMatch[1]} | getLocalStateLang }}`
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
const polyMatch = value.match(/el\.call\(\s*["']polyglot["']\s*,\s*["']([^"']+)["']\s*\)/)
|
|
304
|
+
if (polyMatch) {
|
|
305
|
+
result[key] = `{{ ${polyMatch[1]} | polyglot }}`
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
// Function-string with s.item.field
|
|
309
|
+
if (value.includes('=>') && value.includes('s.')) {
|
|
310
|
+
const stateMatch = value.match(/s\.(\w+(?:\.\w+)+)/)
|
|
311
|
+
if (stateMatch) {
|
|
312
|
+
result[key] = `{{ ${stateMatch[1]} }}`
|
|
313
|
+
continue
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return result
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Extract page-level metadata from project data for a given pathname.
|
|
323
|
+
* @param {object} data - Full project data
|
|
324
|
+
* @param {string} pathname - Route path (e.g. '/blog/:id')
|
|
325
|
+
* @param {object} [ssrContext] - SSR context for resolving dynamic metadata
|
|
326
|
+
* @param {object} [ssrContext.state] - Page state with prefetched data
|
|
327
|
+
* @param {string} [ssrContext.lang] - Active language code (e.g. 'ka')
|
|
328
|
+
* @param {string} [ssrContext.actualPathname] - Actual URL pathname (e.g. '/blog/abc-123')
|
|
329
|
+
*/
|
|
330
|
+
export function getPageMetadata (data, pathname, ssrContext) {
|
|
213
331
|
const currentPage = data.pages?.[pathname]
|
|
214
332
|
const stateObject = isObject(currentPage?.state) && currentPage?.state
|
|
215
|
-
|
|
333
|
+
const rawPageMeta = currentPage?.metadata || currentPage?.helmet || stateObject || {}
|
|
334
|
+
|
|
335
|
+
// Convert function values to {{ }} templates instead of filtering them out
|
|
336
|
+
const pageMetadata = convertFunctionMetaToTemplates(rawPageMeta)
|
|
337
|
+
|
|
338
|
+
// Track which keys the page explicitly sets
|
|
339
|
+
const pageExplicitKeys = new Set(Object.keys(pageMetadata))
|
|
340
|
+
|
|
216
341
|
const appMetadata = data.app?.metadata || {}
|
|
342
|
+
const cleanAppMeta = convertFunctionMetaToTemplates(appMetadata)
|
|
343
|
+
|
|
344
|
+
let merged = {}
|
|
217
345
|
if (data.integrations?.seo) {
|
|
218
|
-
|
|
219
|
-
} else if (Object.keys(
|
|
220
|
-
|
|
346
|
+
merged = { ...data.integrations.seo, ...cleanAppMeta, ...pageMetadata }
|
|
347
|
+
} else if (Object.keys(cleanAppMeta).length) {
|
|
348
|
+
merged = { ...cleanAppMeta, ...pageMetadata }
|
|
349
|
+
} else {
|
|
350
|
+
merged = { ...pageMetadata }
|
|
221
351
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
352
|
+
|
|
353
|
+
if (!merged.title) merged.title = data.name + ' / symbo.ls' || 'Symbols demo'
|
|
354
|
+
|
|
355
|
+
// Resolve {{ }} templates in metadata values (polyglot, state, getLocalStateLang)
|
|
356
|
+
merged = resolveMetadataTemplates(merged, data, ssrContext)
|
|
357
|
+
|
|
358
|
+
// Auto-cascade page-level title/description to OG/Twitter tags
|
|
359
|
+
if (merged.title && (pageExplicitKeys.has('title') || !merged['og:title'])) {
|
|
360
|
+
if (!pageExplicitKeys.has('og:title')) {
|
|
361
|
+
merged['og:title'] = merged.title
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (merged.description && (pageExplicitKeys.has('description') || !merged['og:description'])) {
|
|
365
|
+
if (!pageExplicitKeys.has('og:description')) {
|
|
366
|
+
merged['og:description'] = merged.description
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (merged.title && !merged['twitter:title']) {
|
|
370
|
+
merged['twitter:title'] = merged.title
|
|
371
|
+
}
|
|
372
|
+
if (merged.description && !merged['twitter:description']) {
|
|
373
|
+
merged['twitter:description'] = merged.description
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Make og:url route-aware — use actual pathname if available
|
|
377
|
+
const routeForUrl = ssrContext?.actualPathname || pathname
|
|
378
|
+
if (merged['og:url'] || merged.url) {
|
|
379
|
+
const baseUrl = (merged['og:url'] || merged.url || '').replace(/\/$/, '')
|
|
380
|
+
if (baseUrl && routeForUrl && routeForUrl !== '/') {
|
|
381
|
+
const cleanRoute = routeForUrl.startsWith('/') ? routeForUrl : '/' + routeForUrl
|
|
382
|
+
merged['og:url'] = baseUrl + cleanRoute
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
merged = resolveFileReferences(merged, data.files)
|
|
387
|
+
|
|
388
|
+
// Clean up any unresolved {{ }} templates (when prefetch fails or returns no data)
|
|
389
|
+
const siteName = data.name || data.app?.metadata?.title || data.app?.name || ''
|
|
390
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
391
|
+
if (typeof value === 'string' && value.includes('{{')) {
|
|
392
|
+
const cleaned = value.replace(/\{\{[^}]*\}\}/g, '').trim()
|
|
393
|
+
if (!cleaned) {
|
|
394
|
+
if (key === 'title') merged[key] = siteName
|
|
395
|
+
else delete merged[key]
|
|
396
|
+
} else {
|
|
397
|
+
merged[key] = cleaned
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return merged
|
|
225
403
|
}
|