Skip to content

Commit 6613e6c

Browse files
authored
Knowledge Link Validator (#2320)
* Create Readme.md * Create uiaction.js * Create uipage.js
1 parent a49022b commit 6613e6c

File tree

3 files changed

+214
-0
lines changed

3 files changed

+214
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
This utility script helps ServiceNow administrators and content managers ensure the integrity and usability of hyperlinks embedded within knowledge articles. It scans article content to identify and classify links pointing to catalog items and other knowledge articles, providing detailed insights into:
2+
3+
Catalog Item Links: Detects and categorizes links as active, inactive, or not found.
4+
Knowledge Article Links: Flags outdated articles based on workflow state and expiration (valid_to).
5+
Non-Permalink KB Links: Identifies knowledge article links that do not follow the recommended permalink format (i.e., missing sysparm_article=KBxxxxxxx), even if they use kb_view.do.
6+
The solution includes a Jelly-based UI that displays categorized results with direct links to the affected records, enabling quick remediation. It's ideal for improving content quality, ensuring consistent user experience, and maintaining best practices in knowledge management.
7+
8+
<img width="815" height="231" alt="image" src="https://github.com/user-attachments/assets/7a1d8947-077b-45cd-8b5a-a2bc8e4b50e8" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*
2+
This script should be placed in the UI action on the table kb_knowledge form view.
3+
This UI action should be marked as client.
4+
Use validateLinksInArticle() function in the Onclick field.
5+
*/
6+
7+
function validateLinksInArticle() {
8+
var articleSysId = g_form.getUniqueValue();
9+
var gdw = new GlideDialogWindow('validate_links_dialog');
10+
gdw.setTitle('Validate Article Links');
11+
gdw.setPreference('sysparm_article_id', articleSysId);
12+
gdw.render();
13+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<j:jelly trim="false" xmlns:j="jelly:core" xmlns:g="glide">
2+
<j:set var="jvar_article_id" value="${sysparm_article_id}" />
3+
<g:evaluate jelly="true">
4+
<![CDATA[
5+
var articleId = trim(jelly.sysparm_article_id);
6+
var activeIds = [];
7+
var inactiveIds = [];
8+
var notFoundIds = [];
9+
var outdatedArticles = [];
10+
var badPermalinks = [];
11+
var inActiveCount =0;
12+
var activeCount = 0;
13+
var notFoundCount =0;
14+
var outdatedCount =0;
15+
var badPermalinkCount =0;
16+
var inactiveQuery;
17+
var activeQuery;
18+
var notFoundQuery;
19+
var outdatedQuery;
20+
var badPermalinkQuery;
21+
if (articleId) {
22+
var grArticle = new GlideRecord('kb_knowledge');
23+
if (grArticle.get(articleId)) {
24+
var content = (grArticle.text || '').toString();
25+
// Extract hrefs from <a> tags
26+
var regex = /<a[^>]+href=["']([^"']+)["']/gi;
27+
var urls = [];
28+
var match;
29+
while ((match = regex.exec(content)) !== null) {
30+
31+
urls.push(match[1]);
32+
}
33+
for (var i = 0; i < urls.length; i++) {
34+
var url = urls[i];
35+
36+
// --- 1. Check if link is a Catalog Item ---
37+
var sysId = extractSysId(url, 'sysparm_id') || extractSysId(url, 'sys_id');
38+
if (sysId) {
39+
var grItem = new GlideRecord('sc_cat_item');
40+
if (grItem.get(sysId)) {
41+
if (grItem.active){
42+
activeIds.push(sysId);
43+
activeCount++;
44+
}
45+
else if(grItem.active == false){
46+
inactiveIds.push(sysId);
47+
inActiveCount++;
48+
}
49+
} else {
50+
notFoundIds.push(sysId);
51+
notFoundCount++;
52+
}
53+
}
54+
// --- 2. Check if link is a Knowledge Article ---
55+
// --- 1. Check for outdated knowledge articles via permalink ---
56+
57+
// --- 1. Check for outdated knowledge articles via permalink ---
58+
var decodedUrl = decodeURIComponent(url + '');
59+
decodedUrl = decodedUrl.replace(/&amp;amp;amp;amp;/g, '&');
60+
61+
// Extract KB number or sys_id
62+
var kbNumber = extractSysId(decodedUrl, 'sysparm_article');
63+
var kbSysId = extractSysId(decodedUrl, 'sys_kb_id') || extractSysId(decodedUrl, 'sys_id');
64+
65+
var grKb = new GlideRecord('kb_knowledge');
66+
67+
if (kbNumber && grKb.get('number', kbNumber)) {
68+
var isOutdated = false;
69+
if (grKb.workflow_state != 'published') {
70+
isOutdated = true;
71+
} else if (grKb.valid_to && grKb.valid_to.getGlideObject()) {
72+
var now = new GlideDateTime();
73+
if (grKb.valid_to.getGlideObject().compareTo(now) <= 0) {
74+
isOutdated = true;
75+
}
76+
}
77+
78+
if (isOutdated) {
79+
outdatedArticles.push(grKb.sys_id.toString());
80+
outdatedCount++;
81+
}
82+
} else if (kbSysId && grKb.get(kbSysId)) {
83+
var isOutdated = false;
84+
if (grKb.workflow_state != 'published') {
85+
isOutdated = true;
86+
} else if (grKb.valid_to && grKb.valid_to.getGlideObject()) {
87+
var now = new GlideDateTime();
88+
if (grKb.valid_to.getGlideObject().compareTo(now) <= 0) {
89+
isOutdated = true;
90+
}
91+
}
92+
93+
if (isOutdated) {
94+
outdatedArticles.push(grKb.sys_id.toString());
95+
outdatedCount++;
96+
}
97+
}
98+
99+
// --- 2. Check for non-permalink knowledge links ---
100+
if (
101+
decodedUrl.indexOf('kb_knowledge.do?sys_id=') !== -1 || // form view
102+
(
103+
decodedUrl.indexOf('/kb_view.do') !== -1 &&
104+
decodedUrl.indexOf('sysparm_article=KB') === -1 // missing KB number
105+
)
106+
) {
107+
var kbSysId = extractSysId(decodedUrl, 'sys_kb_id') || extractSysId(decodedUrl, 'sys_id');
108+
if (kbSysId) {
109+
var grBadKB = new GlideRecord('kb_knowledge');
110+
if (grBadKB.get(kbSysId)) {
111+
badPermalinks.push(kbSysId);
112+
badPermalinkCount++;
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}
119+
function extractSysId(url, param) {
120+
try {
121+
var decoded = decodeURIComponent(url + '');
122+
decoded = decoded
123+
.replace(/&amp;amp;amp;/g, '&')
124+
.replace(/&amp;amp;/g, '&')
125+
.replace(/&amp;/g, '&')
126+
.replace(/&#61;/g, '=')
127+
.replace(/&amp;#61;/g, '=');
128+
129+
var parts = decoded.split(param + '=');
130+
if (parts.length > 1) {
131+
var id = parts[1].split('&')[0];
132+
return id && id.length === 32 ? id : null;
133+
}
134+
} catch (e) {
135+
var parts = url.split(param + '=');
136+
if (parts.length > 1) {
137+
var id = parts[1].split('&')[0];
138+
return id && id.length === 32 ? id : null;
139+
}
140+
}
141+
return null;
142+
}
143+
// Expose variables to Jelly
144+
inactiveQuery = "sys_idIN"+inactiveIds.join(',');
145+
activeQuery = "sys_idIN"+activeIds.join(',');
146+
notFoundQuery = "sys_idIN"+notFoundIds.join(',');
147+
outdatedQuery = "sys_idIN"+outdatedArticles.join(',');
148+
badPermalinkQuery = "sys_idIN"+badPermalinks.join(',');
149+
]]>
150+
</g:evaluate>
151+
<table width="600px" border="1" style="border-collapse:collapse;">
152+
<tr style="font-weight:bold; background-color:#f2f2f2;">
153+
<td>Module</td>
154+
<td>Records</td>
155+
<td>Details</td>
156+
</tr>
157+
<tr class="breadcrumb">
158+
<td>Active Catalog Items</td>
159+
<td>${activeCount}</td>
160+
<td>
161+
<a href="sc_cat_item_list.do?sysparm_query=${activeQuery}" target="_blank">View records</a>
162+
</td>
163+
</tr>
164+
<tr class="breadcrumb">
165+
<td>Inactive Catalog Items</td>
166+
<td>${inActiveCount}</td>
167+
<td>
168+
<a href="sc_cat_item_list.do?sysparm_query=${inactiveQuery}" target="_blank">View records</a>
169+
</td>
170+
</tr>
171+
<tr class="breadcrumb">
172+
<td>Not Found Items</td>
173+
<td>${notFoundCount}</td>
174+
<td>
175+
<a href="sc_cat_item_list.do?sysparm_query=${notFoundQuery}" target="_blank">View records</a>
176+
</td>
177+
</tr>
178+
<tr class="breadcrumb">
179+
<td>Outdated Knowledge Articles</td>
180+
<td>${outdatedCount}</td>
181+
<td>
182+
<a href="kb_knowledge_list.do?sysparm_query=${outdatedQuery}" target="_blank">View records</a>
183+
</td>
184+
</tr>
185+
<tr class="breadcrumb">
186+
<td>Non-Permalink Knowledge Links</td>
187+
<td>${badPermalinkCount}</td>
188+
<td>
189+
<a href="kb_knowledge_list.do?sysparm_query=${badPermalinkQuery}" target="_blank">View records</a>
190+
</td>
191+
</tr>
192+
</table>
193+
</j:jelly>

0 commit comments

Comments
 (0)