@blocklet/editor 2.4.92 → 2.4.94
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,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* 为外部 URL 添加 UTM 追踪参数
|
|
4
|
+
* @param url - 原始 URL 地址
|
|
5
|
+
* @returns 添加 UTM 参数后的 URL
|
|
6
|
+
* @see https://team.arcblock.io/comment/discussions/7504c5ce-7453-4223-a539-27620efcf38e#6fd5adb6-49ab-4ead-9233-240aa53d1058
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
export declare function getUTMUrl(url: string): string;
|
package/lib/libs/utm.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { withQuery, getQuery } from 'ufo';
|
|
2
|
+
function isExternal(url) {
|
|
3
|
+
try {
|
|
4
|
+
const u = new URL(url);
|
|
5
|
+
return u.origin !== window.location.origin;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* 为外部 URL 添加 UTM 追踪参数
|
|
14
|
+
* @param url - 原始 URL 地址
|
|
15
|
+
* @returns 添加 UTM 参数后的 URL
|
|
16
|
+
* @see https://team.arcblock.io/comment/discussions/7504c5ce-7453-4223-a539-27620efcf38e#6fd5adb6-49ab-4ead-9233-240aa53d1058
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
export function getUTMUrl(url) {
|
|
20
|
+
try {
|
|
21
|
+
if (!isExternal(url)) {
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
const type = window.__discussKitPostType__ ?? 'discussion';
|
|
25
|
+
const existsQueryParams = getQuery(url);
|
|
26
|
+
const queryParams = {
|
|
27
|
+
// 自身站点的 hostname
|
|
28
|
+
utm_source: window.location.hostname,
|
|
29
|
+
// 标记导航点击
|
|
30
|
+
utm_medium: `${type}_link`,
|
|
31
|
+
// 全局导航统一标签
|
|
32
|
+
utm_campaign: 'default',
|
|
33
|
+
// 目标站点标识
|
|
34
|
+
utm_content: new URL(url).hostname,
|
|
35
|
+
...existsQueryParams,
|
|
36
|
+
};
|
|
37
|
+
return withQuery(url, queryParams);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('Failed to generate UTM URL:', error);
|
|
41
|
+
return url;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { getUTMUrl } from './utm';
|
|
3
|
+
// Mock window 对象
|
|
4
|
+
const mockWindow = {
|
|
5
|
+
location: {
|
|
6
|
+
origin: 'https://example.com',
|
|
7
|
+
hostname: 'example.com',
|
|
8
|
+
},
|
|
9
|
+
__discussKitPostType__: 'discussion',
|
|
10
|
+
};
|
|
11
|
+
describe('getUTMUrl', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
global.window = mockWindow;
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
delete global.window;
|
|
20
|
+
});
|
|
21
|
+
describe('Internal Link Handling', () => {
|
|
22
|
+
it('should return original URL when URL is internal link', () => {
|
|
23
|
+
const internalUrl = 'https://example.com/path';
|
|
24
|
+
expect(getUTMUrl(internalUrl)).toBe(internalUrl);
|
|
25
|
+
});
|
|
26
|
+
it('should return original URL when URL is relative path', () => {
|
|
27
|
+
const relativeUrl = '/path/to/page';
|
|
28
|
+
expect(getUTMUrl(relativeUrl)).toBe(relativeUrl);
|
|
29
|
+
});
|
|
30
|
+
it('should return original URL when URL format is invalid', () => {
|
|
31
|
+
const invalidUrl = 'not-a-valid-url';
|
|
32
|
+
expect(getUTMUrl(invalidUrl)).toBe(invalidUrl);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('External Link UTM Parameter Addition', () => {
|
|
36
|
+
it('should add UTM parameters to external URL', () => {
|
|
37
|
+
const externalUrl = 'https://external.com/path';
|
|
38
|
+
const result = getUTMUrl(externalUrl);
|
|
39
|
+
expect(result).toContain('utm_source=example.com');
|
|
40
|
+
expect(result).toContain('utm_medium=discussion_link');
|
|
41
|
+
expect(result).toContain('utm_campaign=default');
|
|
42
|
+
expect(result).toContain('utm_content=external.com');
|
|
43
|
+
});
|
|
44
|
+
it('should preserve existing query parameters', () => {
|
|
45
|
+
const urlWithParams = 'https://external.com/path?existing=param&another=value';
|
|
46
|
+
const result = getUTMUrl(urlWithParams);
|
|
47
|
+
expect(result).toContain('existing=param');
|
|
48
|
+
expect(result).toContain('another=value');
|
|
49
|
+
expect(result).toContain('utm_source=example.com');
|
|
50
|
+
});
|
|
51
|
+
it('should handle different port numbers', () => {
|
|
52
|
+
const urlWithPort = 'https://external.com:8080/path';
|
|
53
|
+
const result = getUTMUrl(urlWithPort);
|
|
54
|
+
// URL constructor treats port as part of hostname, but ufo library may only take hostname
|
|
55
|
+
expect(result).toContain('utm_content=external.com');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('Different Post Type Handling', () => {
|
|
59
|
+
it('should use default value when __discussKitPostType__ is undefined', () => {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
delete global.window.__discussKitPostType__;
|
|
62
|
+
const externalUrl = 'https://external.com/path';
|
|
63
|
+
const result = getUTMUrl(externalUrl);
|
|
64
|
+
expect(result).toContain('utm_medium=discussion_link');
|
|
65
|
+
});
|
|
66
|
+
it('should use specified post type', () => {
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
global.window.__discussKitPostType__ = 'blog';
|
|
69
|
+
const externalUrl = 'https://external.com/path';
|
|
70
|
+
const result = getUTMUrl(externalUrl);
|
|
71
|
+
expect(result).toContain('utm_medium=blog_link');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('Error Handling', () => {
|
|
75
|
+
it('should return original URL when error occurs', () => {
|
|
76
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
77
|
+
// Mock scenario that causes error - break window.location.hostname access
|
|
78
|
+
const originalWindow = global.window;
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
global.window = {
|
|
81
|
+
location: {
|
|
82
|
+
origin: 'https://example.com',
|
|
83
|
+
get hostname() {
|
|
84
|
+
throw new Error('Hostname access error');
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
__discussKitPostType__: 'discussion',
|
|
88
|
+
};
|
|
89
|
+
const url = 'https://external.com/path';
|
|
90
|
+
const result = getUTMUrl(url);
|
|
91
|
+
expect(result).toBe(url);
|
|
92
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to generate UTM URL:', expect.any(Error));
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
global.window = originalWindow;
|
|
95
|
+
consoleSpy.mockRestore();
|
|
96
|
+
});
|
|
97
|
+
it('should handle empty string URL', () => {
|
|
98
|
+
const result = getUTMUrl('');
|
|
99
|
+
expect(result).toBe('');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('URL Construction Validation', () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
// Reset window object state
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
global.window = {
|
|
107
|
+
location: {
|
|
108
|
+
origin: 'https://example.com',
|
|
109
|
+
hostname: 'example.com',
|
|
110
|
+
},
|
|
111
|
+
__discussKitPostType__: 'discussion',
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
it('should generate valid URL format', () => {
|
|
115
|
+
const externalUrl = 'https://external.com/path';
|
|
116
|
+
const result = getUTMUrl(externalUrl);
|
|
117
|
+
// Verify result is a valid URL
|
|
118
|
+
expect(() => new URL(result)).not.toThrow();
|
|
119
|
+
const parsedUrl = new URL(result);
|
|
120
|
+
expect(parsedUrl.searchParams.get('utm_source')).toBe('example.com');
|
|
121
|
+
expect(parsedUrl.searchParams.get('utm_medium')).toBe('discussion_link');
|
|
122
|
+
expect(parsedUrl.searchParams.get('utm_campaign')).toBe('default');
|
|
123
|
+
expect(parsedUrl.searchParams.get('utm_content')).toBe('external.com');
|
|
124
|
+
});
|
|
125
|
+
it('should correctly handle URLs with existing UTM parameters', () => {
|
|
126
|
+
const urlWithUTM = 'https://external.com/path?utm_source=existing&other=param';
|
|
127
|
+
const result = getUTMUrl(urlWithUTM);
|
|
128
|
+
const parsedUrl = new URL(result);
|
|
129
|
+
// According to code logic, existsQueryParams comes last, overriding new UTM parameters
|
|
130
|
+
expect(parsedUrl.searchParams.get('utm_source')).toBe('existing');
|
|
131
|
+
expect(parsedUrl.searchParams.get('other')).toBe('param');
|
|
132
|
+
// Verify other UTM parameters are correctly added
|
|
133
|
+
expect(result).toContain('utm_medium=discussion_link');
|
|
134
|
+
expect(result).toContain('utm_campaign=default');
|
|
135
|
+
expect(result).toContain('utm_content=external.com');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('Special Character and Encoding Handling', () => {
|
|
139
|
+
it('should correctly handle hostname with special characters', () => {
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
global.window.location.hostname = 'test-site.co.uk';
|
|
142
|
+
const externalUrl = 'https://external.com/path';
|
|
143
|
+
const result = getUTMUrl(externalUrl);
|
|
144
|
+
expect(result).toContain('utm_source=test-site.co.uk');
|
|
145
|
+
});
|
|
146
|
+
it('should correctly handle Chinese domain names', () => {
|
|
147
|
+
const externalUrl = 'https://测试.com/path';
|
|
148
|
+
const result = getUTMUrl(externalUrl);
|
|
149
|
+
// Verify result contains encoded content
|
|
150
|
+
expect(result).toContain('utm_content=xn--');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -5,6 +5,7 @@ import { $getNearestNodeFromDOMNode, $getSelection, $isElementNode, $isRangeSele
|
|
|
5
5
|
import { useEffect } from 'react';
|
|
6
6
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
7
7
|
import { useEditorConfig } from '../../../config';
|
|
8
|
+
import { getUTMUrl } from '../../../libs/utm';
|
|
8
9
|
function findMatchingDOM(startNode, predicate) {
|
|
9
10
|
let node = startNode;
|
|
10
11
|
while (node != null) {
|
|
@@ -93,7 +94,7 @@ export default function LexicalClickableLinkPlugin({ newTab = true }) {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
96
|
-
window.open(url, newTab || isMiddle || event.metaKey || event.ctrlKey || urlTarget === '_blank' ? '_blank' : '_self');
|
|
97
|
+
window.open(getUTMUrl(url), newTab || isMiddle || event.metaKey || event.ctrlKey || urlTarget === '_blank' ? '_blank' : '_self');
|
|
97
98
|
}
|
|
98
99
|
event.preventDefault();
|
|
99
100
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.94",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"ufo": "^1.5.4",
|
|
74
74
|
"url-join": "^4.0.1",
|
|
75
75
|
"zustand": "^4.5.5",
|
|
76
|
-
"@blocklet/pdf": "2.4.
|
|
76
|
+
"@blocklet/pdf": "2.4.94"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@babel/core": "^7.25.2",
|