FreeMarker and templating

Managed by | Updated .

Escape output wherever possible

Since v14, 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. Do so 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/refdirectiveescape.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 try-catch in terms of function.

Consider splitting the template into sub-components.

This approach is particularly useful when dealing with multiple result types in a meta collection:

...
<@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 HTML as clean as it can be:

<#-- 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.

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 <#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. 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

Be aware that the configuration of caching and request timeout 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 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"
  • There should be no reason to have an expiry shorted than 5mn. If that's the case, a different approach might be better (e.g. fetching the content in a hook script, 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.queryinstead of question.inputParameterMap["query"] or other variants.
Was this artcle helpful?

Tags
Type: Keywords:

Comments