Friendly Slug Custom Field (WordPress Style)


SEO-friendly URLs are a must-have nowadays. They not only help web pages rank better in search engines, but also help site visitors and content managers quickly identify a page by glancing over the URL. Generating a slug, a common type of SEO-friendly URL, is a relatively easy task with JavaScript: just change all non-alphanumeric characters to dashes and remove any double-dashes (for example, "Yes: This is the best blog *ever*!!" becomes "yes-this-is-the-best-blog-ever"). Sometimes, however, a slug may turn out the same as one that is already in use, which would cause a page to have a duplicate URL! For instance, a page titled "Exceptional: New Series" would have the same slug as "Exceptional New Series!" ("exceptional-new-series"). The most common way to solve this problem, as made popular by the WordPress content management system, is to check whether a slug already exists and if so, add a number to the end (e.g., "exceptional-new-series-2"). The question for dotCMS developers then becomes, how can one check for duplicate slugs as new content is created in the administration page? This article explains how to accomplish this task.

Although this version only accepts one field, it can be easily modified to generate slugs using two or more fields (e.g., year-month-title) by adding additional $customVarSource variables and appending them together in the old_slug variable declaration in the updateDisplay() JavaScript function of the custom field code. Other modifications may include support for international characters (e.g., converting an e with acute, é, to an e), using a character other than dashes to separate words, and support for more than 999 duplicates. Thus, this code can be used as is, or as the basis for further enhancements that can satisfy more specific business requirements.

The code to implement an SEO-friendly, unique slug in dotCMS is provided below. Note that this code is for dotCMS 1.9.x series, but it may also work with future versions. It consists of two pieces:

Friendly slug JSON service. This code is entered as a widget in an html page that uses a completely blank template. This widget would check the dotCMS structure for duplicate slugs, and return a unique slug as needed in JSON format. Because this page will be called from JavaScript, it is imperative that the page template is blank and does not return any XML or HTML code other than what the widget produces. No additional modifications are needed in this code. In this example, this page is saved as /global/js/friendly-slug.html.
Friendly slug custom field. This code is entered as a custom field in a content structure. In this example, the field is called ���slug,�۝ but some developers may prefer to use ���urlTitle�۝ or even ���id.�۝ The first two lines of code must be modified. First, $customVarSource must be set to the field to use to generate the slug (e.g., ���title���, ���headline,�۝ or ���name���). Then, $friendlySlugURL must be set to the relative address of the page containing the friendly slug JSON service (from step 1).

This article was published on my blog at:



## Friendly Slug JSON Service
## Checks for duplicate slugs and provides an alternative, unique slug as appropriate

#set($structureInode = $request.getParameter("structureInode"))
#set($structureInode = $structureInode.replaceAll(" ","").replaceAll(":","").replaceAll(",","").trim())
#set($structureInode = "0")

#set($fieldName = $request.getParameter("fieldName"))
#set($fieldName = $fieldName.replaceAll(" ","").replaceAll(":","").replaceAll(",","").trim())
#set($fieldName = "_1NaN1_")

#set($fieldValue = $request.getParameter("fieldValue"))
#set($fieldValue = $fieldValue.trim().replaceAll("\"","").replaceAll("'",""))
#set($fieldValue = "_1NaN1_")

#set($skipInode = $request.getParameter("skipInode"))
#set($skipInode = $skipInode.trim().replaceAll("\"","").replaceAll("'",""))
#set($skipInode = "_1NaN1_")

## Get the structure name

## Build the base slug
#set($slug = $fieldValue.trim().toLowerCase()) ##Remove spaces at the beginning or the end, and convert to lowercase
#set($slug = $slug.replaceAll("\"","")) ##Remove double-quotes (")
#set($slug = $slug.replaceAll("\'s(\b)+","s")) ##Remove apostrophe from words ending with 's
#set($slug = $slug.replaceAll("[^a-z 0-9]+","-")) ##Replace other non-alphanumeric characters with a dash
#set($slug = $slug.replaceAll(" ", "-")) ##Replace spaces with a dash
#set($slug = $slug.replaceAll("(-){2,}","-")) ##Replace repeated slashes with a single slash
#set($slug = $slug.replaceAll("^(-){1,}|(-){1,}$","")) ##Remove dash at the beginnning or the end

#set($fieldValueSlugless=$slug.replace("-[0-9]{1,3}$","").trim()) ##Remove any -nnn string at the end of a slug (4 numbers may match a year, so the limit is 3)

#foreach($i in [2..999])
#set($contentList = $dotcontent.pull("+type:content +deleted:false +live:(true OR false) +working:true +structureInode:${structureInode} +${structureName}.${fieldName}:${quote}${slug}${quote} -inode:${skipInode} -identifier:${skipInode}","1","0"))

#if($fieldValue!="_1NaN1_" && $structureName!="_1NaN1_")
"changed" : #if($slug==$fieldValue) false #else true #end,
"oldvalue" : "${fieldValue}",
"newvalue" : "${slug}"
Friendly slug custom field code:
 ## Friendly Slug Custom Field

## Instructions: Set the name of the field to be used as a guide for generating the value of this custom field
## For example, to generate a friendly slug based on the "name" field, set the variable below to "name"
## Also set the URL of the friendly-slug.html page

#set ($customVarSource = "name")
#set ($friendlySlugURL = "/global/js/friendly-slug.html")

#set($_dummy = $render.eval("${esc.hash}set ($origCustomFieldValue = $${field.velocityVarName})"))
<div id="div_display${field.velocityVarName}" style="height:20px"><span id="display${field.velocityVarName}">$!{origCustomFieldValue}</span>&nbsp;&nbsp;<input type="button" id="btnUpdateDisplay${field.velocityVarName}" name="btnUpdateDisplay${field.velocityVarName}" value="Update" onclick="updateDisplay${field.velocityVarName}()" #if(!$UtilMethods.isSet($origCustomFieldValue)) style="display: none; visibility:hidden;" #end /></div>
<script type="text/javascript">
var timeout${field.velocityVarName} = 0;

function updateDisplay${field.velocityVarName}() {
/* Get the value entered by the user */
var old_slug = dojo.byId("${customVarSource}").value;
var slug = old_slug;

/* Build the friendly slug */
slug = slug.replace(/^\s+|\s+$/g,"").toLowerCase(); //Remove spaces at the beginning or the end, and convert to lowercase
slug = slug.replace(/\"/g,""); //Remove double-quotes (")
slug = slug.replace(/\'s(\b)+/g,"s"); //Remove apostrophe from words ending with 's
slug = slug.replace(/[^a-z 0-9]+/g,' '); //Replace other non-alphanumeric characters with a dash
slug = slug.replace(/\s/g, "-"); //Replace spaces with a dash
slug = slug.replace(/(-){2,}/g, "-"); //Replace repeated slashes with a single slash
slug = slug.replace(/^(-){1,}|(-){1,}$/g, ""); //Remove dash at the beginnning or the end

/* Set the values of the display place holder and the custom field */
if(slug!=old_slug) {
dojo.byId("display${field.velocityVarName}").innerHTML = slug;

/* Call the web service to verify that this slug is unique, and if it is not, get a new one */
url:"$!{friendlySlugURL}?structureInode=" + dojo.byId("selectedStructure").value + "&fieldName=${field.velocityVarName}&fieldValue=" + slug + "&skipInode=" + ((typeof assetId=='undefined')?'':assetId.toString()),
headers: { "Content-Type": "application/json", "Accept" : "application/json"},
handle: function(response){
s = eval('(' + ((response=='')?'""':response) + ')');
if(typeof s=='object')
/* Set the values of the display place holder and the custom field */
dojo.byId("display${field.velocityVarName}").innerHTML = s.newvalue;
s = null;

function delayed_updateDisplay${field.velocityVarName}() {
timeout${field.velocityVarName} = setTimeout("updateDisplay${field.velocityVarName}();", 850);

dojo.byId("display${field.velocityVarName}").innerHTML = "$!{origCustomFieldValue}";
/* Attach to appropriate events */
dojo.connect(dojo.byId("display${field.velocityVarName}"), "onmouseover", null, "delayed_updateDisplay${field.velocityVarName}");
dojo.connect(dijit.byId("${customVarSource}"), "onchange", null, "delayed_updateDisplay${field.velocityVarName}");
dojo.connect(dijit.byId("${customVarSource}"), "onclick", null, "delayed_updateDisplay${field.velocityVarName}");
dojo.connect(dijit.byId("${customVarSource}"), "onfocus", null, "updateDisplay${field.velocityVarName}");
dojo.connect(dijit.byId("${customVarSource}"), "onblur", null, "updateDisplay${field.velocityVarName}");
dojo.connect(dijit.byId("fm"), "onsubmit", null, "updateDisplay${field.velocityVarName}");
dojo.byId("btnUpdateDisplay${field.velocityVarName}").style="display: none; visibility:hidden;";