Best practices - 3.1 Freemarker and templating

Background

This article outlines best practices to follow when implementing Funnelback templates.

Check for missing and null values

Special care must be taken when displaying values in a template that the value isn’t missing or evaluates to null as it will result in a FreeMarker error.

An approach to prevent this is to check that a value exists before displaying it using the ?? construct:

<#-- INCORRECT -->
<p class="description">${s.result.metaData["c"]}</p>

<#-- CORRECT -->
<#if s.result.metaData["c"]??><p class="description">${s.result.metaData["c"]}</p></#if>

Alternatively the ! construct can be used to fall back to a default value. For example ${author!"N/A"} will display the author, or fall back to "N/A" if the author doesn’t exist.

<#-- INCORRECT -->
<p class="description">${s.result.metaData["c"]}</p>

<#-- CORRECT usage -->
<#-- with explicit default value -->
<p class="description">${s.result.metaData["c"]!"My default"}</p></#if>

<#-- with implicit default value (empty string) -->
<p class="description">${s.result.metaData["c"]!}</p></#if>

<#-- When using modifiers, use brackets to explicitly define scope around the base value and the '!' value -->
<p class="description">${(s.result.metaData["c"]!"My default")?lower_case?replace("donotwant", "")}</p></#if>

Escape output wherever possible

Variables printed out in Freemarker templates should always be appropriately escaped for the output format that is being produced. This is important as it prevents injection attacks from occurring.

Since 15.8 the recommended way of escaping templates is to use the ftl output_format property. If you are creating a template from scratch then this is the recommended way of escaping.

The advice below covers the former way of escaping as this is still what is implemented within the default Funnelback template.

Prior to this the recommended way of escaping was to use the Freemarker escape directive, which has been used in the default template since v14 (and is still used in the current 15.20 template). The default template wraps all output in a <#escape> tag to prevent XSS issues. Regions within the form can choose to ignore this directive using the <#noescape> tag. However use this with caution.

The escape macro should be configured to use the appropriate escape for what is being returned.

For templates that return HTML use:

<#escape x as x?html>

For template that return JSON use:

<#escape x as x?json_string>

The code below shows how the <#escape> tag is generally used in a template:

<#ftl encoding="utf-8" />
<#import "/web/templates/modernui/funnelback_classic.ftl" as s/>
<#import "/web/templates/modernui/funnelback.ftl" as fb/>
<#escape x as x?html>
  ...
  <#-- Disable escaping on this region -->
  <#noescape>...</#noescape>
  ...
</#escape>

Note that the escape here applies only to the file it appears in - If you use macros imported from another file, that imported file must handle escaping itself. See details and examples at http://freemarker.org/docs/ref_directive_escape.html

When to use <#attempt>: ?eval and other modifiers

The ?eval modifier allows you to parse a JSON string into a set of Freemarker objects, this can be very powerful but comes with risks: if the eval fails for any reason, it will count as a template error. For that reason you must use the <#attempt> macro:

<#-- INCORRECT -->
<#assign myList = s.result.metaData["L"]?eval />
<#list myList as listItem>
    ...
</#list>

<#-- CORRECT -->
<#attempt>
    <#assign myList = s.result.metaData["L"]?eval />
    <#list myList as listItem>
        ...
    </#list>
<#recover>
    <#-- here you can print alternative output when eval fails -->
</#attempt>

This mechanism is similar to a Java try-catch in terms of functionality.

Consider splitting the template into sub-components.

This approach is particularly useful when dealing with multiple result types in a meta collection. However a balance needs to be found between creating these sub-components which are individually easier to edit and understand, and the number of files that make up a template. As more files are added it becomes difficult to find where a change needs to be made as the template may nest components at multiple levels.

...
<@s.Results>
...
<#switch s.result.collection>
  <#case "youtube">
    <#include "result_youtube.ftl">
  <#break>
  <#case "web">
    <#include "result_web.ftl">
  <#break>
  <#default>
    <#include "result_default.ftl">
  <#break>
</#switch>
...
</@s.Results>
...

Avoid data cleansing in the template

Data cleansing within the template is not recommended as it only affects the output of the html endpoint (search.html), and only when the specific template is applied.

Consider whether post-process hook scripts or data cleansing of content in a filter or at the sources is more appropriate than inside a template.

Use temporary variables for data cleansing

If data cleansing within the template is unavoidable then you should use temporary variables to hold the cleaned values. This will help keep the Freemarker code cleaner and also enable reuse of the cleaned variable:

<#-- NON-PREFERRED -->
<span class="personName">Name: ${s.result.liveUrl?replace('http:..www.+\/person/','','r')?replace('\/.*$','','r')?replace('-', ' ')?capitalize?html}</span>

<#-- PREFERRED -->
<#assign personName = s.result.liveUrl?replace('http:..www.+\/person/','','r')?replace('\/.*$','','r')?replace('-', ' ')?capitalize?html />
<span class="personName">Name: ${personName}</span>

Using a template to return something other than HTML

Returning CSV or JSON

Consider using the all results endpoint if you only require result data (i.e. from response.results).

However, for custom columns and JSON structure you will need to use a normal template and the html endpoint.

Ensure appropriate escaping is applied

Be aware of any special escaping rules that might need to be applied for the format that is being returned.

Make use of the built-in escape functions where possible (e.g. for JSON use an <#ftl output_format="json"> or the <#escape x as x?json_string> tag).

Specify the correct MIME type

Typical non-HTML variants include CSV (query completion files, tabular exports of results), RSS, GeoJSON. See also: ui.modern.form.TEMPLATENAME.contenttype

This should be added to the profile’s profile.cfg.

# Template named 'csv-export' returns CSV
ui.modern.form.csv-export.content_type=text/csv

# Template named 'rss' returns RSS
ui.modern.form.rss.content_type=application/rss+xml

Specify a content-disposition header

When accessing non-html content it is often desirable to force the browser to open a download dialog. This can be done by setting the content-disposition HTTP header for the form that specifes a filename. This should be done in addition to setting the mime type as described above.

This should be added to the profile’s profile.cfg.

# Template named 'csv-export' returns CSV
ui.modern.form.csv.content_type=text/csv
# Template named 'csv' should be downloaded as 'export.csv'
ui.modern.form.csv.headers.1=Content-Disposition: attachment; filename=export.csv

Remote includes

The <@s.IncludeUrl> macro can be used to remotely include content within a template (similar to a server side include)

IncludeUrl against a Funnelback server

  • When including content from the same server always use the localhost address and public http or https port. This will avoid creating a dependency on a specific server (for example the admin server), and also avoid other network infrastructure such as load balancers that may slow down the request.

  • Don’t use <@s.IncludeUrl> to request additional search results - use extra searches instead.

IncludeUrl caching and timeout

The configuration of caching and request timeouts will greatly impact the performance of the search results:

  • If the request timeout is high and the remote site is slow to respond, the search result page will become slow as well as it waits for the remote server to return. Usually, it’s best to not change the default timeout unless there’s a good reason.

  • Similarly, if the cache expiry is set to a low value, the remote server might be requested for each search request. If the form has multiple IncludeUrl cals the impacts are even worse.

Cache expiry is usually set to a low value during development (e.g. 1) to force the content to be re-fetched for each query. Once in production, set it to a sensible value. It will typically depend on how you expect the remote content to change:

  • For header / footers and things that rarely change, set the expiry to 1 day (3600 seconds * 24 hours = 86400) or more

  • For more dynamic content that changes more often, adjust accordingly. e.g. If you know that the remote content is refreshed every hour, set the expiry to 3600.

  • When setting the timeout value note that you need to set an integer value (and not a string) in the IncludeUrl macro. e.g. <@s.IncludeUrl expiry=3600> and not <@s.IncludeUrl expiry="3600">.

  • There should be no reason to have an expiry shorted than 5 min unless the template is still under development. If that’s the case, a different approach might be better (e.g. fetching the content in a hook script and injecting it into the custom data element of the data model or having it in a separate collection).

Custom macro libraries

Grouping custom macros into libraries (separate FTL files) is encouraged for readability and re-usability.

Custom libraries should be stored in the collection configuration folder, not the profile folder. The rationale is that macro rarely changes and maintaining multiple versions adds complexity and confusion.

Defined parameter variables

Instead of using a string parameter to map to reference variables, use the defined variable names as these will always exist regardless of if they appear within the query string:

  • To access the collection ID use question.collection.id instead of question.inputParameterMap["collection"] or other variants.

  • To access the profile ID use question.profile instead of question.inputParameterMap["profile"] or other variants.

  • To access the template ID use question.form instead of question.inputParameterMap["form"] or other variants.

  • To access the query terms use question.query instead of question.inputParameterMap["query"] or other variants.

Use relative URLs

Always prefer using relative URLs when linking separate resources (where appropriate), by order of preference:

  • Completely relative URLs under /s/ (e.g. href="search.html", src="resources/images/image.png")

  • Absolute URLs without a domain name (e.g. href="/s/search.html", src="/s/resources/images/images.png")

  • URLs with a domain name, but without a protocol, for HTTP/HTTPS compatibility (e.g. href="//example.com/s/search.html")

  • collection.cfg options such as ui.modern.search_link.

This applies to:

  • resources linked from search templates

  • resources linked from CSS files (e.g. background: url('/s/resources/…​'); )

This makes the collection more portable as there will be nothing to change when it moves across servers and also makes testing easier by avoiding various problems with cross-domain requests and/or HTTP vs HTTPS requests.