ts-glitter 16.2.4 → 16.2.8

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,357 @@
1
+ import {Manager} from "./api-public/services/manager.js";
2
+ import db from "./modules/database.js";
3
+ import {UtDatabase} from "./api-public/utils/ut-database.js";
4
+ import {Seo} from "./services/seo.js";
5
+ import {Shopping} from "./api-public/services/shopping.js";
6
+
7
+ const html = String.raw
8
+
9
+ export class SeoConfig {
10
+ //編輯器的SEO
11
+ public static editorSeo = html`<title>SHOPNEX後台系統</title>
12
+ <link rel="canonical" href="/index"/>
13
+ <meta name="keywords" content="SHOPNEX,電商平台"/>
14
+ <link
15
+ id="appImage"
16
+ rel="shortcut icon"
17
+ href="https://d3jnmi1tfjgtti.cloudfront.net/file/234285319/size1440_s*px$_sas0s9s0s1sesas0_1697354801736-Glitterlogo.png"
18
+ type="image/x-icon"
19
+ />
20
+ <link
21
+ rel="icon"
22
+ href="https://d3jnmi1tfjgtti.cloudfront.net/file/234285319/size1440_s*px$_sas0s9s0s1sesas0_1697354801736-Glitterlogo.png"
23
+ type="image/png"
24
+ sizes="128x128"
25
+ />
26
+ <meta property="og:image"
27
+ content="https://d3jnmi1tfjgtti.cloudfront.net/file/252530754/1718778766524-shopnex_banner.jpg"/>
28
+ <meta property="og:title" content="SHOPNEX後台系統"/>
29
+ <meta
30
+ name="description"
31
+ content="SHOPNEX電商開店平台,零抽成、免手續費。提供精美模板和豐富插件,操作簡單,3分鐘內快速打造專屬商店。購物車、金物流、SEO行銷、資料分析一站搞定。支援APP上架,並提供100%客製化設計,立即免費體驗30天。"
32
+ />
33
+ <meta
34
+ name="og:description"
35
+ content="SHOPNEX電商開店平台,零抽成、免手續費。提供精美模板和豐富插件,操作簡單,3分鐘內快速打造專屬商店。購物車、金物流、SEO行銷、資料分析一站搞定。支援APP上架,並提供100%客製化設計,立即免費體驗30天。"
36
+ />`
37
+
38
+ //分類頁的SEO
39
+ public static async collectionSeo(cf: {
40
+ appName: string,
41
+ language: string,
42
+ data: any,
43
+ page: string
44
+ }) {
45
+ const cols =
46
+ (
47
+ await Manager.getConfig({
48
+ appName: cf.appName,
49
+ key: 'collection',
50
+ language: cf.language as any,
51
+ })
52
+ )[0] ?? {};
53
+ const colJson = extractCols(cols);
54
+ const urlCode = decodeURI((cf.page as string).split('/')[1]);
55
+ const colData = colJson.find((item: any) => {
56
+ if (item.language_data && item.language_data[cf.language]) {
57
+ return item.language_data[cf.language].seo.domain === urlCode || item.title === urlCode
58
+ } else {
59
+ return (item.code === urlCode) || item.title === urlCode;
60
+ }
61
+ });
62
+ if (colData) {
63
+ if (colData.language_data && colData.language_data[cf.language]) {
64
+ cf.data.page_config.seo.title = colData.language_data[cf.language].seo.title || urlCode;
65
+ cf.data.page_config.seo.content = colData.language_data[cf.language].seo.content;
66
+ cf.data.tag = cf.page
67
+ } else {
68
+ cf.data.page_config.seo.title = colData.seo_title || urlCode;
69
+ cf.data.page_config.seo.content = colData.seo_content;
70
+ cf.data.page_config.seo.keywords = colData.seo_keywords;
71
+ cf.data.tag = cf.page
72
+ }
73
+ cf.data.page_config.seo.image = colData.seo_image;
74
+ }
75
+ }
76
+
77
+ //分銷連結的SEO
78
+ public static async distributionSEO(cf: {
79
+ appName: string,
80
+ page: string,
81
+ url: string,
82
+ link_prefix: string,
83
+ data: any,
84
+ language: string
85
+ }) {
86
+ const redURL = new URL(`https://127.0.0.1${cf.url}`);
87
+ const rec = await db.query(
88
+ `SELECT *
89
+ FROM \`${cf.appName}\`.t_recommend_links
90
+ WHERE content ->>'$.link' = ?;
91
+ `,
92
+ [(cf.page as string).split('/')[1]]
93
+ );
94
+ const page = rec[0] && rec[0].content ? rec[0].content : {status: false};
95
+ if (page.status && isCurrentTimeWithinRange(page)) {
96
+ let query = [`(content->>'$.type'='article')`, `(content->>'$.tag'='${page.redirect.split('/')[2]}')`];
97
+ const article: any = await new UtDatabase(cf.appName, `t_manager_post`).querySql(query, {
98
+ page: 0,
99
+ limit: 1,
100
+ });
101
+ cf.data.page_config = cf.data.page_config ?? {};
102
+ cf.data.page_config.seo = cf.data.page_config.seo ?? {};
103
+ if (article.data[0]) {
104
+ if (article.data[0].content.language_data[cf.language]) {
105
+ cf.data.page_config.seo.title = article.data[0].content.language_data[cf.language].seo.title;
106
+ cf.data.page_config.seo.content = article.data[0].content.language_data[cf.language].seo.content;
107
+ cf.data.page_config.seo.keywords = article.data[0].content.language_data[cf.language].seo.keywords;
108
+ } else {
109
+ cf.data.page_config.seo.title = article.data[0].content.seo.title;
110
+ cf.data.page_config.seo.content = article.data[0].content.seo.content;
111
+ cf.data.page_config.seo.keywords = article.data[0].content.seo.keywords;
112
+ }
113
+
114
+ }
115
+ return html`localStorage.setItem('distributionCode','${page.code}');
116
+ location.href = '${cf.link_prefix ? `/` : ``}${cf.link_prefix}${page.redirect}${redURL.search}';
117
+ `;
118
+ } else {
119
+ return html`location.href = '/';`;
120
+ }
121
+ }
122
+
123
+ //商品頁面SEO
124
+ public static async productSEO(cf: {
125
+ data: any,
126
+ language: any,
127
+ appName: string,
128
+ product_id: string,
129
+ page: string
130
+ }) {
131
+ const product_domain = (cf.page as string).split('/')[1];
132
+ const pd = await new Shopping(cf.appName, undefined).getProduct(
133
+ product_domain
134
+ ? {
135
+ page: 0,
136
+ limit: 1,
137
+ domain: decodeURIComponent(product_domain),
138
+ language: cf.language,
139
+ }
140
+ : {
141
+ page: 0,
142
+ limit: 1,
143
+ id: cf.product_id as string,
144
+ language: cf.language,
145
+ }
146
+ );
147
+ if (pd.data.content) {
148
+ pd.data.content.language_data = pd.data.content.language_data ?? {};
149
+ const productSeo = (pd.data.content.language_data[cf.language] && pd.data.content.language_data[cf.language].seo) || (pd.data.content.seo ?? {});
150
+ const language_data = pd.data.content.language_data
151
+ cf.data.page_config = cf.data.page_config ?? {};
152
+ cf.data.page_config.seo = cf.data.page_config.seo ?? {};
153
+ cf.data.page_config.seo.title = productSeo.title;
154
+ cf.data.page_config.seo.image = (language_data && language_data[cf.language] && language_data.preview_image && language_data.preview_image[0]) || pd.data.content.preview_image[0];
155
+ cf.data.page_config.seo.content = productSeo.content;
156
+ cf.data.tag = cf.page;
157
+ }
158
+ }
159
+
160
+ //網誌頁面SEO
161
+ public static async articleSeo(cf: {
162
+ article: any,
163
+ page: string,
164
+ appName: string,
165
+ data: any,
166
+ language: any
167
+ }) {
168
+ cf.article = cf.article || (cf.page as any).split('/')[1];
169
+ let query = [`(content->>'$.type'='article')`, `(content->>'$.tag'='${cf.article}')`];
170
+ const article: any = await new UtDatabase(cf.appName, `t_manager_post`).querySql(query, {
171
+ page: 0,
172
+ limit: 1,
173
+ });
174
+ cf.data.page_config = cf.data.page_config ?? {};
175
+ cf.data.page_config.seo = cf.data.page_config.seo ?? {};
176
+ if (article.data[0]) {
177
+ cf.data.tag = cf.page;
178
+ if (article.data[0].content.language_data && article.data[0].content.language_data[cf.language]) {
179
+ cf.data.page_config.seo.title = article.data[0].content.language_data[cf.language].seo.title;
180
+ cf.data.page_config.seo.content = article.data[0].content.language_data[cf.language].seo.content;
181
+ cf.data.page_config.seo.keywords = article.data[0].content.language_data[cf.language].seo.keywords;
182
+ } else {
183
+ cf.data.page_config.seo.title = article.data[0].content.seo.title;
184
+ cf.data.page_config.seo.content = article.data[0].content.seo.content;
185
+ cf.data.page_config.seo.keywords = article.data[0].content.seo.keywords;
186
+ }
187
+ }
188
+ }
189
+
190
+ //取得多國語言
191
+ public static async language(store_info: any, req: any) {
192
+ function checkIncludes(lan: string) {
193
+ return store_info.language_setting.support.includes(lan);
194
+ }
195
+
196
+ function checkEqual(lan: string) {
197
+ return `${req.query.page}`.startsWith(`${lan}/`) || req.query.page === lan;
198
+ }
199
+
200
+ function replace(lan: string) {
201
+ if (req.query.page === lan) {
202
+ req.query.page = '';
203
+ } else {
204
+ req.query.page = `${req.query.page}`.replace(lan + '/', '');
205
+ }
206
+ }
207
+
208
+ if (checkEqual('en') && checkIncludes('en-US')) {
209
+ replace('en');
210
+ return `en-US`;
211
+ } else if (checkEqual('cn') && checkIncludes('zh-CN')) {
212
+ replace('cn');
213
+ return `zh-CN`;
214
+ } else if (checkEqual('tw') && checkIncludes('zh-TW')) {
215
+ replace('tw');
216
+ return `zh-TW`;
217
+ } else {
218
+ return store_info.language_setting.def;
219
+ }
220
+ }
221
+
222
+ //FB像素
223
+ public static fbCode(FBCode: any) {
224
+ return FBCode && FBCode.pixel
225
+ ? html`<!-- Meta Pixel Code -->
226
+ <script>
227
+ !(function (f, b, e, v, n, t, s) {
228
+ if (f.fbq) return;
229
+ n = f.fbq = function () {
230
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
231
+ };
232
+ if (!f._fbq) f._fbq = n;
233
+ n.push = n;
234
+ n.loaded = !0;
235
+ n.version = '2.0';
236
+ n.queue = [];
237
+ t = b.createElement(e);
238
+ t.async = !0;
239
+ t.src = v;
240
+ s = b.getElementsByTagName(e)[0];
241
+ s.parentNode.insertBefore(t, s);
242
+ })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
243
+ fbq('init', '${FBCode.pixel}');
244
+ fbq('track', 'PageView');
245
+ </script>
246
+ <noscript><img height="1" width="1" style="display:none"
247
+ src="https://www.facebook.com/tr?id=617830100580621&ev=PageView&noscript=1"/>
248
+ </noscript>
249
+ <!-- End Meta Pixel Code -->`
250
+ : ''
251
+ }
252
+
253
+ //GA標籤
254
+ public static gTag(g_tag: any[]) {
255
+ return (g_tag || [])
256
+ .map((dd: any) => {
257
+ return html`<!-- Google tag (gtag.js) -->
258
+ <!-- Google Tag Manager -->
259
+ <script>
260
+ (function (w, d, s, l, i) {
261
+ w[l] = w[l] || [];
262
+ w[l].push({
263
+ 'gtm.start': new Date().getTime(),
264
+ event: 'gtm.js',
265
+ });
266
+ var f = d.getElementsByTagName(s)[0],
267
+ j = d.createElement(s),
268
+ dl = l != 'dataLayer' ? '&l=' + l : '';
269
+ j.async = true;
270
+ j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl;
271
+ f.parentNode.insertBefore(j, f);
272
+ })(window, document, 'script', 'dataLayer', '${dd.code}');
273
+ </script>
274
+ <!-- End Google Tag Manager -->`;
275
+ })
276
+ .join('')
277
+ }
278
+
279
+ //GA追蹤
280
+ public static gA4(ga4: any[]) {
281
+ return (ga4 || [])
282
+ .map((dd: any) => {
283
+ return html`<!-- Google tag (gtag.js) -->
284
+ <script async
285
+ src="https://www.googletagmanager.com/gtag/js?id=${dd.code}"></script>
286
+ <script>
287
+ window.dataLayer = window.dataLayer || [];
288
+
289
+ function gtag() {
290
+ dataLayer.push(arguments);
291
+ }
292
+
293
+ gtag('js', new Date());
294
+
295
+ gtag('config', '${dd.code}');
296
+ </script>`;
297
+ })
298
+ .join('')
299
+ }
300
+
301
+
302
+ }
303
+
304
+
305
+ export function extractCols(data: {
306
+ value: any[];
307
+ updated_at: Date;
308
+ }) {
309
+ let items: any = [];
310
+ const updated_at = new Date(data.updated_at).toISOString().replace(/\.\d{3}Z$/, '+00:00');
311
+ data.value.map((item: any) => {
312
+ items.push({
313
+ ...item,
314
+ updated_at
315
+ });
316
+ if (item.array && item.array.length > 0) {
317
+ items = items.concat(extractCols({
318
+ value: item.array,
319
+ updated_at: data.updated_at
320
+ }))
321
+ }
322
+ });
323
+ return items;
324
+ }
325
+
326
+ export function extractProds(data: any) {
327
+ const items: any = [];
328
+ data.map((item: any) => {
329
+ const updated_at = new Date(item.updated_time).toISOString().replace(/\.\d{3}Z$/, '+00:00');
330
+ items.push({items});
331
+ });
332
+ return items;
333
+ }
334
+
335
+ // 判斷現在時間是否在 start 和 end 之間的函數
336
+ function isCurrentTimeWithinRange(data: {
337
+ startDate: string;
338
+ startTime: string;
339
+ endDate?: string;
340
+ endTime?: string
341
+ }): boolean {
342
+ const now = new Date();
343
+ now.setTime(now.getTime() + 8 * 3600 * 1000);
344
+ // 組合 start 的完整日期時間
345
+ const startDateTime = new Date(`${data.startDate}T${data.startTime}`);
346
+
347
+ // 若 endDate 或 endTime 為 undefined,視為無期限
348
+ const hasEnd = data.endDate && data.endTime;
349
+ const endDateTime = hasEnd ? new Date(`${data.endDate}T${data.endTime}`) : null;
350
+
351
+ // 判斷現在時間是否在範圍內
352
+ if (hasEnd) {
353
+ return now >= startDateTime && now <= endDateTime!;
354
+ } else {
355
+ return now >= startDateTime;
356
+ }
357
+ }