FreeMarker Best Practices

Managed by | Updated .

Escape output wherever possible

In v14, the default form 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.

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

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

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

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:

<#switch s.result.collection>
  <#case "youtube">
    <#include "result_youtube.ftl">
  <#case "web">
    <#include "result_web.ftl">
    <#include "result_default.ftl">

Avoid data cleansing in the template

Consider whether post-process hook scripts or data cleansing of content at the sources is more appropriate before attempting from Funnelback.

Use temporary variables for data cleansing

If data cleansing is unavoidable, this will help keep the HTML as clean as it can be:

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

Specify a MIME type if rendering something other than HTML

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

# Form named 'csv-export' returns CSV
# Form named 'rss' returns RSS


IncludeUrl against a Funnelback server

When using IncludeUrls to request additional search results be mindful of the server you request. In a multi-server environment, you'll generally want to use localhost to make sure that you don't create an unwanted dependency against a remote host.

For example, if you have 1 admin server and 1 query processor, and you set the IncludeURL source to be http://admin-server/s/search.html?..., then when the form will get published the query processor will actually query the admin server to include its content. That will cause problems difficult to track down if the admin server were to go down for any reason.

To sum up:

  • Avoid IncludeUrl to request additional search results (try to use extra searches instead)
  • When you can't avoid it, use a localhost URL.
IncludeUrl caching & 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). Don't hesitate to contact RnD for advice.

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 change and maintaining 2 versions (preview + live profile) adds complexity and confusion.

Defined parameter variables

Instead of using a string parameter to map to reference variables, use the defined variable names:

    • (Instead of question.inputParameterMap["collection"] or other variants)
  • question.profile
    • (Instead of question.inputParameterMap["profile"] or other variants)
  • question.form
    • (Instead of question.inputParameterMap["form"] or other variants)
  • question.query
    • (Instead of question.inputParameterMap["query"] or other variants)

These also don't require an existence test as they are always defined.

Was this artcle helpful?

Type: Keywords: