Name

global.KBSemanticHTMLCheckerUtilSNC

Description

Base class with functions to get SEO issues in an article Contract while extending this class - The return object of getSEOIssuesFromArticle should return the an object with the same structure as below. /******************************************************************************************************** { can_access true, kb_number KB0000001 , kb_short_description US Vacation/Sick Policy , seo_issues_info { total_count 3, fields { Article body { h1 { count 1, issue_list {

this is a heading level 1 text

{ tag_location this is a heading level 1 text , issues More than one H1 tag is not recommended } } }, img { count 1, issue_list { <img height= 75 src= sys_attachment.do?sys_id=a228606b6cb16510f87753cf6bec9b4c width= 100 /> { tag_location <img alt= height= 75 src= sys_attachment.do?sys_id=a228606b6cb16510f87753cf6bec9b4c width= 100 /> , issues Missing alternative description , Missing title } } }, }, KBB0000101 - US vacation days { a { count 1, issue_list { <a href= http //www.google.com rel= nofollow >click here { tag_location click here , issues Missing title } } } } } }, issue_count_msg 3 SEO improvements found } *********************************************************************************************************/

Script

var KBSemanticHTMLCheckerUtilSNC = Class.create();
KBSemanticHTMLCheckerUtilSNC.prototype = {
  duplicateCheckElementObj: {
      'h1': 1
  },
  requiredElemAttrsObj: {
      'img': ['alt', 'title'],
      'a': ['href', 'title'],
  },
  issueMsgsObj: {
      'img': {
          'alt': gs.getMessage('Missing alternative description'),
          'title': gs.getMessage('Missing title'),
      },
      'a': {
          'href': gs.getMessage('Missing href'),
          'title': gs.getMessage('Missing title'),
      },
      'h1': {
          'duplicate_tag': gs.getMessage('More than one H1 tag is not recommended')
      },
  },
  initialize: function() {},

  /**
   * Check if SEO property 'sn_km_portal.glide.knowman.serviceportal.seo_portals' is enabled for the knowledge base of the article
   * @param {String} articleKnowledgeBaseId - sys_id of the knowledge base of article
   * @returns {Boolean} - True if SEO property is enabled; false otherwise
   */
  isSEOPropertyEnabled: function(articleKnowledgeBaseId) {
      var portalIds = gs.getProperty('sn_km_portal.glide.knowman.serviceportal.seo_portals', '').trim().replace(/,+$/, '').split(',');
      var gr = new GlideRecord('m2m_sp_portal_knowledge_base');
      var encodedQuery = 'sp_portal=' + portalIds.join('^ORsp_portal=') + '^kb_knowledge_base=' + articleKnowledgeBaseId;
      gr.addEncodedQuery(encodedQuery);
      gr.query();
      if (gr.next())
          return true;
      return false;
  },

  /**
  * Find all Nodes for a list of HTML tag names
  * @param {Array} tagList - List of all tag names to be searched
  * @param {Node} tempRootElementNode - Temporary root node of the htmlDoc
  * @returns {Object} - Key value pairs of tag name and corresponding Node List
  Sample Object returned:
  {
  	'h1': [Node1, Node2...],
  	'a': [Node1, Node2...],
  	'img': [Node1, Node2...]
  }
  */
  getTagElementNodes: function(tagList, tempRootElementNode) {
      var uniquetagList = tagList.filter(function(value, index, array) {
          return array.indexOf(value) === index;
      });
      var foundTagNodesObj = {};
      for (var i = 0; i < uniquetagList.length; i++) {
          var allTagElements = GlideXMLUtil.selectNodes(tempRootElementNode, './/' + uniquetagList[i]);
          if (!foundTagNodesObj[uniquetagList[i]]) {
              foundTagNodesObj[uniquetagList[i]] = allTagElements;
          }
      }
      return foundTagNodesObj;
  },

  /**
   * Get text found inside the opening and closing tag of an HTML element
   * @param {Node} tagNode - HTML element Node
   * @param {String} tagName - Name of the tag for which the issues need to be found
   * @returns {String} - Text found inside the opening and closing tag of an HTML element
   */
  getTextInsideElementTags: function(tagNode, tagName) {
      var textInsideElementTags = GlideXMLUtil.getText(tagNode);
      if (tagName == 'img' && !textInsideElementTags) {
          textInsideElementTags = GlideXMLUtil.getAttribute(tagNode, 'title');
          textInsideElementTags = textInsideElementTags ? textInsideElementTags : GlideXMLUtil.toString(tagNode, true);
      } else {
          textInsideElementTags = textInsideElementTags ? textInsideElementTags : GlideXMLUtil.toString(tagNode, true);
      }
      return textInsideElementTags;
  },

  /**
  * Update the foundIssuesObj Object by adding a new SEO issue
  * @param {String} tableColumn - Name of the column in the article
  * @param {String} tagName - name of the tag for which the issues need to be found
  * @param {String} nodeHTML - The html string of a Node used as a unique identifier for the Node
  * @param {String} issueType - Category of issue like 'href', 'title', 'alt', 'duplicate_tag'
  * @param {String} textInsidetagNode - Text found inside the opening and closing tag of an HTML element
  * @param {Object} foundIssuesObj - Object containing all information about all SEO issues found till particular point of time
  Sample foundIssuesObj Object:
	{
      "total_count": 3,
      "fields": {
          "Article body": {
              "h1": {
                  "count": 1,
                  "issue_list": {
                      "<h1>this is a heading level 1 text</h1>": {
                          "tag_location": "this is a heading level 1 text",
                          "issues": [
                              "More than one H1 tag is not recommended"
                          ]
                      }
                  }
              },
              "img": {
                  "count": 1,
                  "issue_list": {
                      "<img height='75' src='sys_attachment.do?sys_id=a228606b6cb16510f87753cf6bec9b4c' width='100'/>": {
                          "tag_location": '<img alt="" height="75" src="sys_attachment.do?sys_id=a228606b6cb16510f87753cf6bec9b4c" width="100"/>',
                          "issues": [
                              "Missing alternative description",
                              "Missing title"
                          ]
                      }
                  }
              },
          },
          "KBB0000101": {
              "a": {
                  "count": 1,
                  "issue_list": {
                      "<a href='http://www.google.com' rel='nofollow'>click here</a>": {
                          "tag_location": "click here",
                          "issues": [
                              "Missing title"
                          ]
                      }
                  }
              }
          }
      }
  }
  */
  _addIssueTofoundIssuesObj: function(tableColumn, tagName, nodeHTML, foundIssuesObj, issueType, textInsidetagNode) {
  	if(!foundIssuesObj['fields'][tableColumn])
  		foundIssuesObj['fields'][tableColumn] = {};
      if (foundIssuesObj['fields'][tableColumn] && foundIssuesObj['fields'][tableColumn][tagName]) {
          if (foundIssuesObj['fields'][tableColumn][tagName]['issue_list'][nodeHTML] && foundIssuesObj['fields'][tableColumn][tagName]['issue_list'][nodeHTML]['issues'].indexOf(issueType) == -1) {
  			foundIssuesObj['fields'][tableColumn][tagName]['issue_list'][nodeHTML]['issues'].push(issueType);
          }
  		else if(!foundIssuesObj['fields'][tableColumn][tagName]['issue_list'][nodeHTML]) {
              foundIssuesObj['fields'][tableColumn][tagName]['issue_list'][nodeHTML] = {
                  'tag_location': textInsidetagNode,
                  'issues': [issueType]
              };
              foundIssuesObj['fields'][tableColumn][tagName]['count'] += 1;
              foundIssuesObj['total_count'] += 1;
          }
      }
  	else {
          foundIssuesObj['fields'][tableColumn][tagName] = {
              'count': 1,
              'issue_list': {}
          };
          foundIssuesObj['fields'][tableColumn][tagName]['issue_list'][nodeHTML] = {
              'tag_location': textInsidetagNode,
              'issues': [issueType],
          };
          foundIssuesObj['total_count'] += 1;
      }
  },

  /**
   * Finds SEO issues where a duplicate tag elements has been identified, by updating the foundIssuesObj Object
   * @param {String} tableColumn - Name of the column in the article
   * @param {String} tagName - Name of the tag for which the issues need to be found
   * @param {Array} tagNodeList - List of Element Nodes found for the particular tag name
   * @param {Integer} countThreshold - Threshold value for HTML element to be called as duplicate
   * @param {Object} foundIssuesObj - Object containing all information about all SEO issues found till particular point of time
   * @param {String} checkForSingleIssue - Check for the first SEO issue found and return
   * @returns {Boolean} when checkForSingleIssue is set to true
   */
  _getDuplicateElementIssuesForTag: function(tableColumn, tagName, tagNodeList, countThreshold, foundIssuesObj, checkForSingleIssue) {
      if (tagNodeList.length >= countThreshold) {
          if (checkForSingleIssue)
  			return true;
          for (var i = 0; i < tagNodeList.length; i++) {
              var tagNode = tagNodeList.item(i);
              var textInsidetagNode = this.getTextInsideElementTags(tagNode, tagName);
              this._addIssueTofoundIssuesObj(tableColumn, tagName, GlideXMLUtil.toString(tagNode, true), foundIssuesObj, this.issueMsgsObj[tagName]['duplicate_tag'], textInsidetagNode);
          }
      }
  },

  /**
   * Finds tags that have the required attributes missing, by updating the foundIssuesObj Object
   * @param {String} tableColumn - Name of the column in the article
   * @param {String} tagName - name of the tag for which the issues need to be found
   * @param {Array} tagNodeList - List of Element Nodes found for the particular tag name
   * @param {Object} foundIssuesObj - Object containing all information about all SEO issues found till particular point of time
   * @param {String} checkForSingleIssue - Check for the first SEO issue found and return
   * @returns {Boolean} when checkForSingleIssue is set to true
   */
  _getMissingTagAttributeIssuesForTag: function(tableColumn, tagName, tagNodeList, foundIssuesObj, checkForSingleIssue) {
      for (var i = 0; i < tagNodeList.length; i++) {
          var tagNode = tagNodeList.item(i);
          var allAttrsList = this.requiredElemAttrsObj[tagName];
          for (var j = 0; j < allAttrsList.length; j++) {
              var attrName = allAttrsList[j];
              var attrValue = GlideXMLUtil.getAttribute(tagNode, attrName);
              if (!attrValue) {
                  if (checkForSingleIssue)
  			        return true;
                  var textInsidetagNode = this.getTextInsideElementTags(tagNode, tagName);
                  this._addIssueTofoundIssuesObj(tableColumn, tagName, GlideXMLUtil.toString(tagNode, true), foundIssuesObj, this.issueMsgsObj[tagName][attrName], textInsidetagNode);
              }
          }
      }
  },

  /**
   * Finds the SEO related issues for the supplied HTML string
   * @param {String} tableColumn - Name of the column in the article
   * @param {String} htmlStr - String content of the knowledge record column
   * @param {Object} foundIssuesObj - Object containing all information about all SEO issues found till particular point of time
   * @param {Boolean} checkForSingleIssue - Check for the first SEO issue found and return
   * @returns {Boolean} when checkForSingleIssue is set to true else returns {Object} -  Object containing the issue count and the list of all SEO issues associated with the article
   */
  getIssuesInfo: function(tableColumn, htmlStr, foundIssuesObj, checkForSingleIssue) {
      var htmlDoc = GlideXMLUtil.parse("<temp_root_element>" + htmlStr + "</temp_root_element>");
      var tempRootElementNode = GlideXMLUtil.getElementByTagName(htmlDoc, 'temp_root_element');
      var foundTagNodesObj = this.getTagElementNodes([].concat(Object.keys(this.duplicateCheckElementObj)).concat(Object.keys(this.requiredElemAttrsObj)), tempRootElementNode);
      var tagNameList = Object.keys(foundTagNodesObj);
      for (var i = 0; i < tagNameList.length; i++) {
          var tagName = tagNameList[i];
          var tagNodeList = foundTagNodesObj[tagName];
          if (this.duplicateCheckElementObj[tagName]) {
              var isIssueFound = this._getDuplicateElementIssuesForTag(tableColumn, tagName, tagNodeList, this.duplicateCheckElementObj[tagName], foundIssuesObj, checkForSingleIssue);
              if (isIssueFound && checkForSingleIssue)
  				return true;
          }
          if (this.requiredElemAttrsObj[tagName]) {
              var isIssueFound = this._getMissingTagAttributeIssuesForTag(tableColumn, tagName, tagNodeList, foundIssuesObj, checkForSingleIssue);
              if (isIssueFound && checkForSingleIssue)
  				return true;
          }
      }
      if (checkForSingleIssue)
  		return false;
      return foundIssuesObj;
  },

  /**
  * Find HTML field names and labels
  * @param {String} tableName - Table name of the article template
  * @returns {Object} - Information about the HTML fields name and label for article template
  */
  getHTMLFieldsDataForTemplate: function(tableName){
  	var fields = [];
  	
  	var kbViewModel = new KBViewModel();
  	if (GlidePluginManager.isActive("com.snc.knowledge_advanced") && kbViewModel.isArticleTemplate(tableName)) {
  		var gr = new GlideRecord('kb_article_template_definition');
  		gr.addQuery('article_template.child_table',tableName);
  		gr.addActiveQuery();
  		gr.addQuery('column_type','IN','html,translated_html');
  		gr.orderBy('order');
  		gr.query();

  		while (gr.next()) {
  			fields.push({
  				'column_name': gr.getValue('table_column'),
  				'column_label': gr.getValue('column_label')
  			});
  		}
  	} else {
  		fields.push({
  			'column_name': 'text',
  			'column_label': 'Article body'
  		});
  	}
  	
  	return fields;
  },
  
  /**
  * Driver function to find all SEO related issues in an article
  * @param {GlideRecord Object} kbGr - KB glide record object
  * @param {Boolean} checkForSingleIssue - Check for the first SEO issue found and return
  * @param {String} articleSysId - sys_id of the article
  * @returns {Boolean} when checkForSingleIssue is set to true else returns {Object} 
  */
  getSEOIssuesFromArticle: function(kbGr, checkForSingleIssue, articleSysId){		
  	if(!kbGr && articleSysId){
          // if kbGr is not passed, but articleSysId is passed
  		kbGr = new GlideRecord('kb_knowledge');
  		kbGr.get(articleSysId);
  	}
  	if( kbGr && new KnowledgeAccessSNC().contributorRight(kbGr) && this.isSEOPropertyEnabled(kbGr.kb_knowledge_base)){
  		var tableName = kbGr.sys_class_name;
          var tableColumns = this.getHTMLFieldsDataForTemplate(tableName);

          var seoIssuesInfo = {
              'total_count': 0,
              'fields': {},
          };
          var templateGR = new GlideRecord(tableName);
          if(templateGR.get(kbGr.sys_id)){
              for(var i = 0; i < tableColumns.length; i++){
                  seoIssuesInfo = this.getIssuesInfo(tableColumns[i].column_label, templateGR.getValue(tableColumns[i].column_name), seoIssuesInfo, checkForSingleIssue);	
                  if(checkForSingleIssue && seoIssuesInfo)
                      return true;
              }
              // check if article has knowledge blocks added, if yes get SEO issues for them
              if(tableName != 'kb_knowledge_block' && GlidePluginManager().isActive('com.snc.knowledge_blocks')){
                  var kbbEncodedQuery = 'sys_idIN';
                  var m2mGr = new GlideRecord('m2m_kb_to_block_history');
                  m2mGr.addEncodedQuery("block_state=published^knowledge=" + kbGr.sys_id);
                  m2mGr.query();
                  while (m2mGr.next()){
                      kbbEncodedQuery += m2mGr.getValue('knowledge_block') + ',';
                  }
                  var kbbGr = new GlideRecord('kb_knowledge_block');
                  kbbGr.addEncodedQuery(kbbEncodedQuery);
                  kbbGr.query();
                  while(kbbGr.next()){
                      seoIssuesInfo = this.getIssuesInfo(kbbGr.number + ' - ' + kbbGr.short_description, kbbGr.getValue('text'), seoIssuesInfo, checkForSingleIssue);	
                      if(checkForSingleIssue && seoIssuesInfo)
                          return true;
                  }
              }
              if(checkForSingleIssue)
                  return false;
              return {
                  'can_access': true,
                  'kb_number': kbGr.number,
                  'kb_short_description': kbGr.short_description,
                  'seo_issues_info': seoIssuesInfo,
                  'issue_count_msg': gs.getMessage('{0} SEO suggestions found', [String(seoIssuesInfo['total_count'])]),
              };
          }
  	}
      if(checkForSingleIssue)
          return false;
  	return {
  		'can_access': false,
  	};
  },

  /**
  * Checks if msg should be displayed depending on SEO property, if user has write access on article and if there is atleast one SEO issue found; if yes, what should be the content of the message
  * @param {GlideRecord Object} kbGr - KB glide record object
  * @returns {Object} - Sample: {'show_msg': true, 'msg': 'For SEO suggestions on this article, click here.'}
  */
  getSEONotification: function(kbGr){
      var isIssueFound = this.getSEOIssuesFromArticle(kbGr, true);
      if(isIssueFound){
          var result = {};
          result['show_msg'] = true;
          result['msg'] = gs.getMessage('For SEO suggestions on this article, click {0}here{1}.', ['<a href="kb_semantic_html_suggestions.do?sysparm_article_id=' + kbGr.sys_id + '" target="_blank">', '</a>']);
          return result;
      }
  	return {'show_msg': false};
  },

  type: 'KBSemanticHTMLCheckerUtilSNC'
};

Sys ID

a6cc68ce77a221107b89a0e89e5a99f8

Offical Documentation

Official Docs: