Scala: How to use Play Framework “Twirl templates” in a standalone application (tips, examples)

Last week I finished writing some Scala code to convert this website from using Drupal 8 to using static web pages. Technically what happens is that I use Drupal at a different location, and then my Scala code reads the Drupal database and uses to the Twirl template system — that comes with the Play Framework — to convert that data into the static HTML pages you see here.

Along the way I learned a lot about the 2020 version of Twirl templates, so I thought I’d share some tips and examples about how to do things with Twirl. If you ever want to use Twirl in standalone mode like I did, or inside the Play Framework, I hope this is helpful.

Configuring SBT to use Twirl

To use Twirl with SBT in a standalone project, first put this line in project/plugins.sbt:

addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.5.0")

Then include a line like this in build.sbt:

lazy val root = (project in file(".")).enablePlugins(SbtTwirl)

For instance, here’s my complete build.sbt file:

name := "StaticDrupal"
version := "0.1"
scalaVersion := "2.12.11"

lazy val root = (project in file(".")).enablePlugins(SbtTwirl)

libraryDependencies ++= Seq(
    "org.scalikejdbc" %% "scalikejdbc"          % "3.1.0",
    "org.scalikejdbc" %% "scalikejdbc-config"   % "3.1.0",
    "ch.qos.logback"  %  "logback-classic"      % "1.2.3",
    "mysql"           %  "mysql-connector-java" % "8.0.19",
    "com.typesafe"    %  "config" % "1.4.0",
    "org.scalactic"   %% "scalactic" % "3.1.1",
    "org.scalatest"   %% "scalatest" % "3.1.1"  % "test"

scalacOptions += "-deprecation"

Once you have that setup you can write normal Scala applications that use Twirl templates, and run them with sbt run, and create standalone applications with sbt package or sbt assembly.

How to handle Twirl import statements and input parameters

When you first create a Twirl template, you’ll probably want some import statements at the beginning, and then a parameter list after that:

@import static_drupal.Category
@import static_drupal.Tag
@import static_drupal.WhatsNewLink
@import static_drupal.WhatsRelatedLink

    productionMode: Boolean,
    nid: Int, 
    nodeType: String,
    dateChanged: String,
    title: String, 
    description: String, 
    body: play.twirl.api.Html

<!DOCTYPE html>

A Twirl template is just like a function, so you can think of those parameters as being function parameters.


  • Twirl template are named something like nodeBook.scala.html, nodeBlog.scala.html, footer.scala.html, etc.
  • Twirl templates go under a src/main/twirl folder.

How to include a Twirl template in another template

To include one Twirl template inside another, do it like this:

@* include a template file named cssIncludes.scala.html *@

How to write Twirl functions

I’m still not an expert on Twirl functions — I’m not even sure they’re called functions (see Reusable Blocks on this page) — but you can write them like this:

@getNextNode(currentBookNode: BookNode, allNodesForBook: Seq[BookNode]) = @{
    val idx = allNodesForBook.indexOf(currentBookNode)
    if (idx < allNodesForBook.length-2) {
    } else {

@getPreviousNode(currentBookNode: BookNode, allNodesForBook: Seq[BookNode]) = @{
    val idx = allNodesForBook.indexOf(currentBookNode)
    if (idx == 0) {
    } else {

Twirl if/then/else statements

Display some HTML if a condition is true:

@if(description.trim != "") {
    <meta name="description" content="@description" />

Display the contents of a template named divGoogleAnalytics.scala.html inside an if statement:

@if(productionMode) {

Here’s a Twirl if/else block:

@if(bookmarkUrl.trim.isEmpty) {
    <div class="field__label">URL</div>
    <div class="field__item"><a 
          rel="nofollow">(the url was not found)</a></div>
} else {
    <div class="field__label">URL</div>
    <div class="field__item"><a 

Here’s a Twirl if/else-if/else block:

@if(pageNumber == 1) {
    <li class="pager__item pager__item--prev">1</li>
} else if(pageNumber == 2) {
    <li class="pager__item pager__item--prev">
    <a href="/" title="Previous page">«</a>
} else {
    <li class="pager__item pager__item--prev">
    <a href="/{pageNumber-1}" title="Previous page">«</a>

Note that the opening ( needs to be write next to the if as shown, i.e., as if(...), and not if (...). I don’t remember what the error message is, but it won’t compile if you have a blank space after the if.

A Twirl for-loop

Here’s a Twirl for-loop:

<!-- START "content_tags" -->
<div class="content_tags">
<div class="field field--name-tags field--type-entity-reference field--label-hidden field__items">
@for(t <- tags) {
    <div class="field__item"><a href="/taxonomy/term/@t.tid" hreflang="en"></a></div>
<!-- END "content_tags" -->

Here’s another for-loop with UL/LI tags:

@for(link <- whatsNewLinks) {    
<li><div class="views-field views-field-title"><span class="field-content"><a href="@link.urlAlias" hreflang="en">@link.title</a></span></div></li>

Using a counter in a Twirl for-loop

When you need to use a counter in a Twirl for-loop, call the zip method on your sequence. In this code, nodesForPage is a Seq[Node]:

@for((node,currentFileNumber) <- from 1)) {

    @defining(currentFileNumber) { pageNumber =>

        @if(startRow(pageNumber)) {
            <div class="views-row clearfix row-@currentRow(pageNumber)">

        <div class="views-col col-@getColumn(pageNumber)">
            <div class="views-field views-field-field-photo-d8">
            <div class="field-content">
            <a href="@node.photoNodeUri"><img 
            width="160" height="160" 
            alt="@node.altText" typeof="foaf:Image" class="image-style-thumbnail-160x160" />

        @if(endRow(pageNumber, nodesForPage.length)) {
            </div><!-- views-row-->



A Twirl match expression

Here’s a Twirl match expression:

@photoDetailsOption match {
    case None => {
        <p>(Sorry, the photo was not found)</p>
    case Some(p) => {
        <a href="/sites/default/files/@p.uri"><img 
            width="480" alt="@p.altText" title="@p.titleText" typeof="foaf:Image" class="image-style-preview"

Twirl comments

Here’s a basic Twirl comment:

@* this is a single-line twirl comment *@

This is a multiline Twirl comment:

- looking at an example photo file
    - public://2020-03/colorado-moonset-march-10-2019.jpeg
- files found under the html dir for this image:
    - sites/default/files/2020-03/colorado-moonset-march-10-2019.jpeg
    - sites/default/files/styles/preview/public/2020-03/colorado-moonset-march-10-2019.jpeg
    - sites/default/files/styles/thumbnail/public/2020-03/colorado-moonset-march-10-2019.jpeg
    - sites/default/files/styles/thumbnail_160x160/public/2020-03/colorado-moonset-march-10-2019.jpeg

When you want to emit raw HTML in a Twirl template

If you have some HTML in a variable like myHtml and want to emit that HTML without Twirl interpreting it, use this solution:


Conversely, if you just try to display the HTML contents in myHtml like this:


Twirl will convert all of your < and > characters into &lt; and &gt;. (It probably munges other characters as well.)

How to call a Twirl template from your Scala code

When you need to call a Twirl template from your Scala code, call it just like you’d call a normal function. The only trick is being familiar with the Twirl data types, such as play.twirl.api.Html:

// this is in my scala “controller” class
def createHtmlForRootIndexPages(
    productionMode: Boolean,
    pageNumber: Int,
    totalNumberOfPages: Int,
    nodesForPage: Seq[BlogNode],
    photoDetails: Seq[Option[PhotoDetails]]
): String = {
    // this is where i use a twirl template named nodeBlogPage.scala.html
    val content: play.twirl.api.Html = html.nodeBlogPage(


Those examples show the most common needs I had when using Twirl templates. One of the biggest problems was when I needed to use a counter inside a for-loop, and using zip (as shown) or zipWithIndex seems to be the solution there.

If you need to use Twirl templates in a standalone application or inside the Play Framework, I hope these tips and examples are helpful.