How-to library · Developer docs

How-to library

Task-shaped pages: a title that starts with a verb, the situation in a few lines, the steps, the working code in both engines, and why this way rather than another. Found from a search, done, and left.

Port one template from Smarty to Twig

Keep the file name, change the contents. Carry escaping across one for one, translate calls second, scope assignments third, then diff the rendered page before and after. Here is one template ported end to end.

Context

Context. Porting is mechanical but not automatic. Two facts make it safe: neither engine escapes output on this platform, so escaping carries across one for one, and the layout points at the template by path, so the file name stays and only the contents change. One card partial, ported below, exercises most of what a template set contains.

Steps

  1. Change the syntax construct by construct: output, conditionals, loops, includes, assignments.
  2. Carry every |escape across as |e, and add none because the target is Twig.
  3. Rewrite translation calls to __.translate() and |format to |date with an explicit format.
  4. Replace scope='parent' and scope='global' with the template context, and check partials that relied on Smarty's lack of isolation.
  5. Render the page before and after, strip the markup, and diff the text.

Code

Template code
{* before: templates/includes/post-card.html *}
{$image = $post->getAreaImage('image')}
<article class="card {if $post->isFeatured()}card--featured{/if}">
  {if !$image->isBlank()}{$image->getResized(600, 400, 'fit')->getHtml(['alt' => $post->getName()|escape, 'loading' => 'lazy'])}{/if}
  <h3><a href="{$post->getFullPath()}">{$post->getName()|escape}</a></h3>
  {if $post->getPublished()}<time>{$post->getPublished()|format:'date'}</time>{/if}
  <p>{$post->getAreaText('summary')|escape|truncate:120}</p>
  <a href="{$post->getFullPath()}">{'card.read_more'|_:'site-blog'}</a>
</article>
{# after: templates/includes/post-card.html, same path #}
{% set image = post.getAreaImage('image') %}
<article class="card {% if post.isFeatured() %}card--featured{% endif %}">
  {% if not image.isBlank() %}{{ image.getResized(600, 400, 'fit').getHtml({'alt': post.getName()|e, 'loading': 'lazy'}) }}{% endif %}
  <h3><a href="{{ post.getFullPath() }}">{{ post.getName()|e }}</a></h3>
  {% if post.getPublished() %}<time>{{ post.getPublished()|date('d/m/Y') }}</time>{% endif %}
  <p>{{ post.getAreaText('summary')|e|u.truncate(120, '…') }}</p>
  <a href="{{ post.getFullPath() }}">{{ __.translate('card.read_more', {}, 'site-blog') }}</a>
</article>
Shell
# diff the rendered text before and after
curl -s https://dev.example.com/blogs/news | sed 's/<[^>]*>/ /g' | tr -s ' \n' > before.txt
# deploy the port, then:
curl -s https://dev.example.com/blogs/news | sed 's/<[^>]*>/ /g' | tr -s ' \n' > after.txt
diff before.txt after.txt

Why this and not that

Not renaming to .twig: every layout pointing at the old path would break, and the admin offers .html files. Not adding |e where the Smarty had none: output that was intentionally raw (rich text) becomes visible tags, and the diff will show it. Not converting a set of templates all at once: the engine is a site-wide setting, so the whole set switches together, which is why the diff of every route is the acceptance test.

Go deeper

Was this page helpful?

Your answer goes to the docs team.