This is an advanced technique that provides a Patch / Upgrade safe technique to customize / override the markup generated by all Percussion CM1 Templates. Using this technique it’s possible to fully control the JavaScript and CSS references that are loaded by all CM1 published Pages and Templates.
Override System Velocity Macros
Percussion uses an overlay system for the Velocity templates used by the system. A very simple technique can be used to override system defaults to accomplish specific website goals like Page Speed before a built-in solution is available. User Velocity Macros trump System Velocity macros by design, and they are upgrade and Patch friendly. Knowing that System Velocity Macros can be overridden, it makes sense to review the available Macros and assessing how they relate to Page Speed.
System Velocity Macros
System Velocity Macros are considered core product and will always be overwritten on upgrade or patching. For this reason, the contents of the sys_assembly.vm file should not be customized or overwritten. In contrast, User Velocity Macros are located in rx_resources/vm and are never overwritten on upgrade / patching (with the exception of rx_assembly.vm - we recommend leaving that file to itself).
Location: sys_resources/vm/sys_assembly.vm
There are a number of macros defined in the sys_assmbly.vm file, however a subset of those macros are responsible for rendering CM1 Page Templates and Widget display templates. We’ll go through each macro, including the macro source as of this writing in the section below.
See the System Velocity Macro reference post for details on each available system macro.
How Velocity Macro Overrides Work In Percussion
When the Percussion Server starts, the Velocity template engine is initialized. The engine first loads all .vm files from the sys_resources/vm folder. After the System Velocity macros are loaded, the system loads all User Velocity macros from the rx_resources/vm folder. Macro names in Velocity are global by design, and when the system detects a User Velocity Macro with the same name as a System Velocity macro, the User Velocity Macro will Overlay or Override the System Macro of the same name.
The mechanism is upgrade and patch safe as the installer / patch tools are not designed to overwrite user created resources in the rx_resources/vm folder.
So what does this mean for Page Speed?
It means that we can override any of the default Percussion System Velocity Macros with fine tuned versions of the macros that are tailored for your websites design and usage with a fully supported and supportable technique. In the next section we’ll break down the page header macro and go over creating an override version of it.
Breaking Down the System Header Macro
The system header macro is our biggest Page speed problem. In some cases it includes JavaScript that we don’t need or use, it pushes everything into the header, and it may or may not be including tags or other content that we don’t need on our site.
Macro Directive:
#macro(perc_templateHeader)##
The macro directive just says this is a macro and indicates any parameters (separated by spaces). Macros are called using #macro_name(param1 param2).
DocType Declaration:
#set($selectedDoctype = $perc.template.docType.selected)## #foreach($option in $perc.template.docType.options)## #if($option.option.equals($selectedDoctype))## #perc_displayText("$!option.value")## #end## #end##
This block of code sets a variable named $selectedDoctype to the Document Type Declaration selected in the Metadata of the Template in the user interface. We probably want to keep this block when overriding the macro. Otherwise if someone changed the declaration in the UI, it wouldn’t show up on published pages.
Head:
<head>
This defines the start of the head on the page.
XUA Compatible Block:
##See IE10 Required for drag drop CMS-79 and CMS-3403 #if($perc.isEditMode()) <meta http-equiv="X-UA-Compatible" content="IE=10"/> #else <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> #end **Charset Directive:** <meta content="text/html; charset=UTF-8" http-equiv="content-type"/>
Canonical Link Block:
## #set($percPageID = $perc.getPage().getId()) #if($perc.linkContext.getSite().isCanonical() && "$!{percPageID}" != "")## #set($renderCanonical = true)## #if($perc.linkContext.getSite().isCanonicalReplace())## #set($stripCustomCanonical = true)## #elseif($rx.pageutils.checkLinkCanonical($!perc.page.additionalHeadContent) || $rx.pageutils.checkLinkCanonical($!perc.template.additionalHeadContent))## #set($renderCanonical = false)## #end## #if($renderCanonical)## #set($canLink = $rx.pageutils.itemLink($perc.linkContext, $perc.getPage()).toString())## #if($perc.linkContext.getMode().name() == "PREVIEW")## #set($linkPref = "/Sites/${perc.linkContext.getSite().getName()}")## #set($canLink = $canLink.substring($linkPref.length()))## #end## <link rel="canonical" href="${perc.linkContext.getSite().getSiteProtocol()}://${perc.linkContext.getSite().getName()}${canLink}" /> #end## #end##
Browser Title:
<title>$tools.esc.html($!perc.page.title)</title>
Global Error Handler:
<script type="text/javascript"> var percGlobalErrors = []; window.onerror=function(msg, url, linenumber){ var jsErrorMsg = 'JavaScript error -- ' + msg; percGlobalErrors.push(jsErrorMsg); return false; } </script>
CSS Resource Loading Block:
#foreach($css_link in $rx.pageutils.cssLinks($perc.linkContext, $sys.assemblyItem))## <link rel="stylesheet" href="$css_link" type="text/css" media="all" /> #end##
Main Theme CSS Loading Block:
Region Override CSS Block:
#set($__perc_region_css = $rx.pageutils.themeRegionCssLink($perc.linkContext, $perc.template.theme, $perc.editMode, $perc.editType).toString().trim())## #if("$!__perc_region_css" != "")## <link rel="stylesheet" href="${__perc_region_css}" type="text/css" media="all" /> #end##
Region Style Overrides Block:
#set($__style = $!perc.template.cssRegion.replaceAll("$tools.esc.q", "").trim())## #if("$!__style" != "")## <style type="text/css"> $__style </style> #end## #set($__style = $!perc.template.cssOverride.trim())## #if("$!{__style}" != "") <style type="text/css"> $__style </style> #end##
JavaScript Resource Loading Block:
#foreach($js_link in $rx.pageutils.javascriptLinks($perc.linkContext, $sys.assemblyItem))## <script type="text/javascript" src="$js_link"></script> #end##
Exclude Page From Search Engines Block:
#if("$!perc.page.noindex" == "true")## <meta name="robots" content="noindex" /> #end##
Page Description Block:
#if("$!perc.page.description" != "")## <meta name="description" content="$tools.esc.html($!perc.page.description)" /> #end##
Dublin Core Metadata Block:
#if($sys.item.hasProperty('rx:page_authorname') && $sys.item.getProperty('rx:page_authorname').getString() != "")## <meta property="dcterms:author" content="$tools.esc.html($sys.item.getProperty('rx:page_authorname').getString())" /> ## Use the correct dcterms property <meta property="dcterms:creator" content="$tools.esc.html($sys.item.getProperty('rx:page_authorname').getString())" /> #end## <meta property="dcterms:type" content="page"/> <meta property="dcterms:source" content="$tools.esc.html($!perc.template.name)"/> <meta property="dcterms:created" content="#perc_displayXdsDateTime('rx:sys_contentpostdate')"/> <meta property="dcterms:modified" content="#perc_displayXdsDateTime('rx:sys_contentlastmodifieddate')"/> <meta property="dcterms:alternative" content="$tools.esc.html($!perc.page.linkTitle)"/>
Percussion Tags & Categories Block:
#if($sys.item.hasProperty("page_tags"))## #set( $tagValues = $sys.item.getProperty('page_tags').getValues() )## #foreach($tag in $tagValues)## <meta property="perc:tags" content="$tools.esc.html($tag.String)"/> #end## #end## #if($sys.item.hasProperty("page_categories_tree"))## #set( $categoryValues = $sys.item.getProperty('page_categories_tree').getValues() )## #foreach($category in $categoryValues)## #set($categoryLabel = $rx.pageutils.getCategoryLabel($category.String))## <meta property="perc:category" content="$tools.esc.html($!{categoryLabel})"/> #end## #end##
Percussion Calendar Block:
#if($sys.item.hasProperty("page_calendar"))## #set( $calendarValues = $sys.item.getProperty('page_calendar').getValues())## #foreach($calendar in $calendarValues)## <meta property="perc:calendar" content="$tools.esc.html($calendar.String)"/> #end## #end##
Page Start & End Date Metadata Block:
#if($sys.item.hasProperty("page_start_date"))## <meta property="perc:start_date" content="#perc_displayXdsDateTime('page_start_date')"/> #end## #if($sys.item.hasProperty("page_end_date"))## <meta property="perc:end_date" content="#perc_displayXdsDateTime('page_end_date')"/> #end## ##
Canonical Duplicate Scrub of Head Content Block:
#if ($stripCustomCanonical)## #set($tmplAdditionalHeadContent = $rx.pageutils.stripLinkCanonical($!perc.template.additionalHeadContent))## #set($pageAdditionalHeadContent = $rx.pageutils.stripLinkCanonical($!perc.page.additionalHeadContent))##
Render Template Head Content Block:
#perc_displayText("$!tmplAdditionalHeadContent")##
Render Page Head Content Block:
#perc_displayText("$!pageAdditionalHeadContent")## #else##
Render Template Head Content Block:
#perc_displayText("$!perc.template.additionalHeadContent")##
Render Page Head Content Block:
#perc_displayText("$!perc.page.additionalHeadContent")## #end## ##
Render Percussion DTS Delivery Base Block:
#perc_addDeliveryJSFunctions()##
Render Google Analytics Block:
## The analytics script additions must be the last thing added to the header ##perc_addGoogleAnalyticsScript()##
Render LiveFirst Pages not done importing on Preview Block:
#suppressCatalogedLinks()##
Render the Preview Mobile Toolbar Block:
#addMobilePreviewToolbar()##
Render Foundation JavaScript if this is a Responsive Template Block:
#addResponsiveResources()##
Close the Head!
</head>
Start the Body!
<body>
Render Linkback Block
<input type="hidden" id="perc_linkback_id" value="$perc.page.id"/>
Render Hummingbird Editor Block (deprecated)
#if ($perc.isPreviewMode() && !$perc.isEditMode() && $sys.isHummingbirdEnabled)## <div data-perc-title="$sys.item.getProperty('sys_title').String" data-perc-contentid="$sys.assemblyItem.getId().getUUID()"></div>## #end##
Render “Protected Region” Block
#if($perc.linkContext.mode == "PUBLISH" && $perc.template.protectedRegion && $perc.template.protectedRegion.String != "")## #set($loginPage = $rx.pageutils.getSiteLoginPage($perc.linkContext.site.name))## <div id="protectedRegionInformation" data="{"protectedRegion":"$!perc.template.protectedRegion","protectedRegionText":"$!perc.template.protectedRegionText","siteLoginPage":"$!loginPage"}" style="display:none"></div> #if("" != "$!{perc.template.protectedRegion}"))## <style> #$!perc.template.protectedRegion{ display:none; } </style> #end## #end##
Render Hidden Page Summary Block:
#if($perc.page.name && $sys.item.getProperty("rx:page_summary").String != "")## #set($summaryField="#displayfield('rx:page_summary')")## #set($summaryField2=$summaryField.replace("<!-- morelink -->", ' <a class="perc-more-link"></a>'))## <div property="dcterms:abstract" style="display:none" datatype="rdf:XMLLiteral">$summaryField2</div> #end## #perc_displayText("$!perc.template.afterBodyStartContent")## #perc_displayText("$!perc.page.afterBodyStartContent")## #end##
The End of the Header
With the blocks broken down in the Header, we need to do the same to the Footer. Then we can pick the blocks we need to keep, the blocks we need to move, and the blocks we want to exclude for our override macro.
Macro Directive:
#macro(perc_templateFooter)##
Render Page Before Body Close:
#perc_displayText("$!perc.page.beforeBodyCloseContent")##
Render Template Before Body Close:
#perc_displayText("$!perc.template.beforeBodyCloseContent")##
Hummingbird (Deprecated):
#if ($perc.isPreviewMode() && !$perc.isEditMode() && $sys.isHummingbirdEnabled)##
#end##
</body>
End Body!
</html>
End HTML!
#end##
Checkpoint
This is a long post but necessary to understand how the Template macro’s work before we optimized them for a given site. We have broken down the Percussion stock Velocity Macros. We know we want to update these to get better speed scores. So now what? We will now go through the process of creating the overrides and optimizing existing Templates and Pages. Before continuing make sure that you have updated any inline JavaScript to be Async or Deferred on your Pages and Templates.
JQuery to Defer or Not To Defer
Note: Edited.
jQuery is used by many Percussion widgets and by many customer custom scripts and Widgets as well. When jQuery is deferred, all scripts that rely on jQuery must also be deferred. Otherwise you could end up with “Servicebase Not Defined” errors when non-deferred scripts reference jQuery. The rest of this example assumes that you do not want to defer jQuery or can’t because of Widgets in use.
The following widgets currently inline some JavaScript (resolved in the 5.4 release)
- Navigation Widget
- Form Widget
- Comment Form Widget
- Image Widget
- Poll Widget
- Directory Widget
- Social Buttons Widget
- Calendar Widgets
- Image Slider Widget
Create the User Velocity Macro Override File and Start Testing
- Pick a few target Pages on the WebSite and test Page Speed recording the results.
- Create a file named customer_override.vm
- Copy the following macros from /sys_resources/vm/sys_assembly.vm
** perc_templateHeader
** perc_templateFooter
** perc_addDeliveryJSFunctions
** addResponsiveResources - Update the macros in the customer_override.vm file. (see example below)
- Copy the customer_override.vm file to the /rx_resources/vm/customer_overrides.vm
- Restart the Percussion service
- Publish Pages and test PageSpeed compare against previous Page Speed, there should be an increase in score.
Example customer_override.vm
The perc_addDeliveryJSFunction includes inline script, so it can’t be deferred. It also needs registered before any inline scripts in the page. This is needed on any Templates that use Percussion DTS Widgets.
## Add the delivery service base function to the page #macro(perc_addDeliveryJSFunctions)## #set($dServiceBase = $rx.pageutils.getDeliveryServer($sys.assemblyItem.PubServerId))## #set($cm1Version = $rx.pageutils.productVersion())## #if(! $cm1Version )## #set($cm1Version = "")## #end #set($cm1LicenseStatus = $rx.pageutils.licenseStatus())## #if(! $cm1LicenseStatus)## #set($cm1License = "")## #else## #set($cm1License = $cm1LicenseStatus.licenseId)## #end## <script type="text/javascript"> (function($) {(function() { jQuery.getDeliveryServiceBase = function(){ return '$dServiceBase';};jQuery.getCm1License = function(){ return '$!{cm1License}';};jQuery.getCm1Version = function(){ $
#end##
The addResponsiveResources macro is intended to be called from the head. We need to update this macro to pull out the JavaScript references and move them to the footer macro. Here is the updated macro.
#macro(addResponsiveResources)## #if ($perc.template.sourceTemplateName.startsWith("perc.resp"))## <meta name="viewport" content="width=device-width, initial-scale=1.0" /> #if($perc.linkContext.mode == "PUBLISH")## <link rel="stylesheet" href="/web_resources/cm/foundation/css/foundation.min.css" type="text/css" media="all" /> <link rel="stylesheet" href="/web_resources/cm/foundation/css/normalize.css" type="text/css" media="all" /> #else## <link rel="stylesheet" href="/Rhythmyx/web_resources/cm/foundation/css/foundation.min.css" type="text/css" media="all" /> <link rel="stylesheet" href="/Rhythmyx/web_resources/cm/foundation/css/normalize.css" type="text/css" media="all" /> #end## #end## #end##
The perc_templateFooter macro get’s a few more changes, as this is where we are shifting our JavaScript resources. The updated macro loads dependent scripts first, followed by the responsive framework script, followed by Template Before Body Close scripts and Page Before Body Close scripts. Deferred scripts are guaranteed to be loaded in order. An important note is the #if($perc.editMode)section.
If you are providing your own version of JQuery you could use this location in the macro to load your version of JQuery on Published and Preview views but Percussion’s version of jQuery when in the editor where that version is required. The default code below will load Percussion’s jquery in either case. This will also remove the need for 2 Jqueries to be loaded in the Page. If you take this approach you would remove your version of JQuery from all Templates and Pages and use the macro to inject the script reference.
#macro(perc_templateFooter)## #foreach($js_link in $rx.pageutils.javascriptLinks($perc.linkContext, $sys.assemblyItem))## #if( $js_link != "/Rhythmyx/web_resources/cm/jslib/jquery.js" && $js_link != "/web_resources/cm/jslib/jquery.js")## #if(!$perc.isEditMode())## <script defer type="text/javascript" src="$js_link"></script> #else## <script type="text/javascript" src="$js_link"></script> #end## #end## #end## #if ($perc.template.sourceTemplateName.startsWith("perc.resp"))## #if($perc.linkContext.mode == "PUBLISH")## <script defer type="text/javascript" src="/web_resources/cm/foundation/js/foundation.min.js"></script> #else## <script defer type="text/javascript" src="/Rhythmyx/web_resources/cm/foundation/js/foundation.min.js"></script> #end## #end## #perc_displayText("$!perc.page.beforeBodyCloseContent")## #perc_displayText("$!perc.template.beforeBodyCloseContent")## <script defer type="text/javascript"> var percGlobalErrors = []; window.onerror=function(msg, url, linenumber){ var jsErrorMsg = 'JavaScript error -- ' + msg; percGlobalErrors.push(jsErrorMsg); return false; } </script> </body> </html> #end
The final macro is the perc_templateHeader macro. The suggested override is below. All JavaScript references have been removed and pushed into the Footer macro where they are deferred.
#macro(perc_templateHeader)## #set($selectedDoctype = $perc.template.docType.selected)## #foreach($option in $perc.template.docType.options)## #if($option.option.equals($selectedDoctype))## #perc_displayText("$!option.value")## #end## #end## <head> ##See IE10 Required for drag drop CMS-79 and CMS-3403 #if($perc.isEditMode()) <meta http-equiv="X-UA-Compatible" content="IE=10"/> #else <meta http-equiv="X-UA-Compatible" content="IE=Edge"/> #end <meta content="text/html; charset=UTF-8" http-equiv="content-type"/> ## #set($percPageID = $perc.getPage().getId()) #if($perc.linkContext.getSite().isCanonical() && "$!{percPageID}" != "")## #set($renderCanonical = true)## #if($perc.linkContext.getSite().isCanonicalReplace())## #set($stripCustomCanonical = true)## #elseif($rx.pageutils.checkLinkCanonical($!perc.page.additionalHeadContent) || $rx.pageutils.checkLinkCanonical($!perc.template.additionalHeadContent))## #set($renderCanonical = false)## #end## #if($renderCanonical)## #set($canLink = $rx.pageutils.itemLink($perc.linkContext, $perc.getPage()).toString())## #if($perc.linkContext.getMode().name() == "PREVIEW")## #set($linkPref = "/Sites/${perc.linkContext.getSite().getName()}")## #set($canLink = $canLink.substring($linkPref.length()))## #end## <link rel="canonical" href="${perc.linkContext.getSite().getSiteProtocol()}://${perc.linkContext.getSite().getName()}${canLink}" /> #end## #end## <title>$tools.esc.html($!perc.page.title)</title> #foreach($css_link in $rx.pageutils.cssLinks($perc.linkContext, $sys.assemblyItem))## <link rel="stylesheet" href="$css_link" type="text/css" media="all" /> #end## <link rel="stylesheet" href="$rx.pageutils.themeLink($perc.linkContext, $perc.template.theme)" type="text/css" media="all" /> #set($__perc_region_css = $rx.pageutils.themeRegionCssLink($perc.linkContext, $perc.template.theme, $perc.editMode, $perc.editType).toString().trim())## #if("$!__perc_region_css" != "")## <link rel="stylesheet" href="${__perc_region_css}" type="text/css" media="all" /> #end## #set($__style = $!perc.template.cssRegion.replaceAll("$tools.esc.q", "").trim())## #if("$!__style" != "")## <style type="text/css"> $__style </style> #end## #if("$!perc.page.noindex" == "true")## <meta name="robots" content="noindex" /> #end## #if("$!perc.page.description" != "")## <meta name="description" content="$tools.esc.html($!perc.page.description)" /> #end## #if($sys.item.hasProperty('rx:page_authorname') && $sys.item.getProperty('rx:page_authorname').getString() != "")## <meta property="dcterms:author" content="$tools.esc.html($sys.item.getProperty('rx:page_authorname').getString())" /> ## Use the correct dcterms property <meta property="dcterms:creator" content="$tools.esc.html($sys.item.getProperty('rx:page_authorname').getString())" /> #end## <meta property="dcterms:type" content="page"/> <meta property="dcterms:source" content="$tools.esc.html($!perc.template.name)"/> <meta property="dcterms:created" content="#perc_displayXdsDateTime('rx:sys_contentpostdate')"/> <meta property="dcterms:modified" content="#perc_displayXdsDateTime('rx:sys_contentlastmodifieddate')"/> <meta property="dcterms:alternative" content="$tools.esc.html($!perc.page.linkTitle)"/> #if($sys.item.hasProperty("page_tags"))## #set( $tagValues = $sys.item.getProperty('page_tags').getValues() )## #foreach($tag in $tagValues)## <meta property="perc:tags" content="$tools.esc.html($tag.String)"/> #end## #end## #if($sys.item.hasProperty("page_categories_tree"))## #set( $categoryValues = $sys.item.getProperty('page_categories_tree').getValues() )## #foreach($category in $categoryValues)## #set($categoryLabel = $rx.pageutils.getCategoryLabel($category.String))## <meta property="perc:category" content="$tools.esc.html($!{categoryLabel})"/> #end## #end## #if($sys.item.hasProperty("page_calendar"))## #set( $calendarValues = $sys.item.getProperty('page_calendar').getValues())## #foreach($calendar in $calendarValues)## <meta property="perc:calendar" content="$tools.esc.html($calendar.String)"/> #end## #end## #if($sys.item.hasProperty("page_start_date"))## <meta property="perc:start_date" content="#perc_displayXdsDateTime('page_start_date')"/> #end## #if($sys.item.hasProperty("page_end_date"))## <meta property="perc:end_date" content="#perc_displayXdsDateTime('page_end_date')"/> #end## ## ## #if ($stripCustomCanonical)## #set($tmplAdditionalHeadContent = $rx.pageutils.stripLinkCanonical($!perc.template.additionalHeadContent))## #set($pageAdditionalHeadContent = $rx.pageutils.stripLinkCanonical($!perc.page.additionalHeadContent))## #perc_displayText("$!tmplAdditionalHeadContent")## #perc_displayText("$!pageAdditionalHeadContent")## #else## #perc_displayText("$!perc.template.additionalHeadContent")## #perc_displayText("$!perc.page.additionalHeadContent")## #end## ## ##suppressCatalogedLinks()## ##addMobilePreviewToolbar()## #addResponsiveResources()## #foreach($js_link in $rx.pageutils.javascriptLinks($perc.linkContext, $sys.assemblyItem))## #if($js_link =="/Rhythmyx/web_resources/cm/jslib/jquery.js" || $js_link =="/web_resources/cm/jslib/jquery.js")## <script type="text/javascript" src="$js_link"></script> #perc_addDeliveryJSFunctions()## #end## #end## </head> <body> <input type="hidden" id="perc_linkback_id" value="$perc.page.id"/> #if($perc.linkContext.mode == "PUBLISH" && $perc.template.protectedRegion && $perc.template.protectedRegion.String != "")## #set($loginPage = $rx.pageutils.getSiteLoginPage($perc.linkContext.site.name))## <div id="protectedRegionInformation" data="{"protectedRegion":"$!perc.template.protectedRegion","protectedRegionText":"$!perc.template.protectedRegionText","$ #if("" != "$!{perc.template.protectedRegion}"))## <style> #$!perc.template.protectedRegion{ display:none; } </style> #end## #end## #if($perc.page.name && $sys.item.getProperty("rx:page_summary").String != "")## #set($summaryField="#displayfield('rx:page_summary')")## #set($summaryField2=$summaryField.replace("<!-- morelink -->", ' <a class="perc-more-link"></a>'))## <div property="dcterms:abstract" style="display:none" datatype="rdf:XMLLiteral">$summaryField2</div> #end## #perc_displayText("$!perc.template.afterBodyStartContent")## #perc_displayText("$!perc.page.afterBodyStartContent")## ## #end
That concludes the technique for Part 3. Some of these optimizations will ship with the 5.4 release of CM1 at which time you could simply comment out or remove youUser Velocity Macro overrides. Or you can continue to use your overrides to more fully control the markup that CM1 templates generate.
Comments / Feedback welcome!