Skip to content

Grav Twig Filters & Functions — Date, Translate, Array and Utility Filters

DodaTech Updated 2026-06-27 8 min read

In this tutorial, you'll learn Grav's Twig filters and functions — date formatting, translation filters, array operations, string utilities, and Grav-specific Twig extensions that make template development faster and more powerful.

What You'll Learn

  • Built-in Twig filters: date, upper, lower, replace, slice, split, merge
  • Grav-specific filters: t, ta, nicetime, absolute_url, basename
  • Twig functions: range, batch, cycle, date, dump
  • Translation functions for multilingual sites
  • String manipulation and formatting techniques
  • Array and collection operations

Why It Matters

In WordPress, template logic uses PHP functions mixed with HTML. In Grav, all template logic uses Twig filters and functions. Filters transform data — convert dates to "3 days ago", translate "Submit" into Spanish, merge arrays, truncate text. Functions generate data — create ranges of numbers, batch items into rows, cycle through CSS classes. Mastering filters and functions means you write less code and do more in your templates.

Real-World Use

A multilingual documentation site with 2,000 pages needs to display dates in the reader's local format, translate UI strings into 8 languages, and display related articles in a 3-column grid. Using Twig's date filter for formatting, the t filter for translation, and the batch function for the grid layout, the entire template logic is handled in 15 lines of Twig.

Learning Path

flowchart LR
    A["Markdown & Shortcodes"] --> B["Twig Filters & Functions
← You are here"]:::current B --> C["Twig Macros"] C --> D["Twig Inheritance"] D --> E["Twig Debugging"] E --> F["Custom Twig Extensions"] F --> G["Theme Configuration"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Built-in Twig Filters

String Filters

{{ 'hello world'|upper }}
{# Output: HELLO WORLD #}

{{ 'HELLO WORLD'|lower }}
{# Output: hello world #}

{{ 'hello world'|capitalize }}
{# Output: Hello world #}

{{ 'hello world'|title }}
{# Output: Hello World #}

{{ '  hello  '|trim }}
{# Output: hello #}

{{ 'hello world'|replace({'world': 'grav'}) }}
{# Output: hello grav #}

{{ 'hello world'|slice(0, 5) }}
{# Output: hello #}

{{ 'a,b,c'|split(',') }}
{# Output: ['a', 'b', 'c'] #}

{{ ['a', 'b', 'c']|join(', ') }}
{# Output: a, b, c #}

Date Filters

{# Format a date #}
{{ page.date|date('Y-m-d') }}
{# Output: 2026-06-27 #}

{{ page.date|date('F j, Y') }}
{# Output: June 27, 2026 #}

{{ page.date|date('l, F jS') }}
{# Output: Saturday, June 27th #}

{# Relative time #}
{{ page.date|nicetime }}
{# Output: 2 days ago (relative) #}

{{ page.date|nicetime(false) }}
{# Output: June 27, 2026 (non-relative) #}

Number Filters

{{ 3.14159|number_format(2) }}
{# Output: 3.14 #}

{{ 255|number_format(0) }}
{# Output: 255 #}

{{ 1000000|number_format(0, '.', ',') }}
{# Output: 1,000,000 #}

Array Filters

{% set items = ['c', 'a', 'b'] %}

{{ items|sort|join(', ') }}
{# Output: a, b, c #}

{{ items|reverse|join(', ') }}
{# Output: b, a, c (only the first level) #}

{% set first = ['a', 'b'] %}
{% set second = ['c', 'd'] %}
{{ first|merge(second)|join(', ') }}
{# Output: a, b, c, d #}

{% set nested = [[1, 2], [3, 4]] %}
{% do nested|length %}
{# Output: 2 #}

Grav-Specific Filters

Translate (t and ta)

{{ 'Submit'|t }}
{# Output: Enviar (if language is Spanish) #}

{# With placeholders #}
{{ 'Hello %name%'|t({ '%name%': user.displayname }) }}
{# Output: Hello John #}

{# Array translation #}
{{ ['Submit', 'Cancel']|ta }}
{# Output: ['Enviar', 'Cancelar'] #}

Nicetime

{{ page.date|nicetime }}
{# Output: 3 hours ago #}

{{ page.date|nicetime(false) }}
{# Output: June 27, 2026 2:30pm #}

Absolute URL

<a href="{{ '/about'|absolute_url }}">About</a>
{# Output: <a href="http://example.com/about">About</a> #}

Basename

{{ '/user/pages/01.home/default.md'|basename }}
{# Output: default.md #}

Pathinfo

{% set info = '/user/pages/01.home/default.md'|pathinfo %}
{{ info.dirname }}   {# /user/pages/01.home #}
{{ info.basename }}  {# default.md #}
{{ info.filename }}  {# default #}
{{ info.extension }} {# md #}

Safe Email

<a href="{{ 'user@example.com'|safe_email }}">Email me</a>
{# Output: <a href="mailto:user@example.com">Email me</a> #}

Twig Functions

Range

{% for i in range(1, 5) %}
    {{ i }}
{% endfor %}
{# Output: 1 2 3 4 5 #}

{% for i in range(0, 10, 2) %}
    {{ i }}
{% endfor %}
{# Output: 0 2 4 6 8 10 #}

Batch

The batch function splits an array into groups:

{% set items = ['A', 'B', 'C', 'D', 'E', 'F'] %}

{% for row in items|batch(3) %}
<div class="row">
    {% for item in row %}
    <div class="col">{{ item }}</div>
    {% endfor %}
</div>
{% endfor %}

Output:

<div class="row">
    <div class="col">A</div>
    <div class="col">B</div>
    <div class="col">C</div>
</div>
<div class="row">
    <div class="col">D</div>
    <div class="col">E</div>
    <div class="col">F</div>
</div>

With fill parameter:

{% for row in items|batch(4, 'Fill') %}

Cycle

{% for item in ['one', 'two', 'three'] %}
    <div class="{{ cycle(['odd', 'even'], loop.index0) }}">
        {{ item }}
    </div>
{% endfor %}

Output:

<div class="odd">one</div>
<div class="even">two</div>
<div class="odd">three</div>

Date

{{ date()|date('Y-m-d H:i:s') }}
{# Output: 2026-06-27 14:30:00 (current time) #}

{{ date('2026-12-25')|date('F j, Y') }}
{# Output: December 25, 2026 #}

Dump

{{ dump(page) }}
{# Outputs the full page object structure for debugging #}

{{ dump(page.header) }}
{# Outputs just the frontmatter header #}

Grav-Specific Functions

URL

{{ url('/about') }}
{# Output: http://localhost:8000/about #}

{{ url('theme://images/logo.png') }}
{# Output: http://localhost:8000/user/themes/mytheme/images/logo.png #}

Media

{{ media['user://pages/01.home/hero.jpg'].url }}
{# Output: URL to the hero image #}

Config Access

{{ config.site.title }}
{# Output: My Documentation Site #}

{{ config.plugins.email.from }}
{# Output: noreply@example.com #}

Combining Filters and Functions

{# Recent posts in a 3-column grid with relative dates #}
{% set recent = page.collection({
    'items': '@self.children',
    'order': { 'by': 'date', 'dir': 'desc' },
    'limit': 6
}) %}

{% for row in recent|batch(3) %}
<div class="row">
    {% for post in row %}
    <article class="col-4 card {{ cycle(['first', 'middle', 'last'], loop.index0) }}">
        <h3>{{ post.title }}</h3>
        <time>{{ post.date|nicetime }}</time>
        <p>{{ post.summary }}</p>
        <a href="{{ post.url }}">{{ 'Read more'|t }}</a>
    </article>
    {% endfor %}
</div>
{% endfor %}

Learning Path

flowchart LR
    A["Markdown & Shortcodes"] --> B["Twig Filters & Functions
← You are here"]:::current B --> C["Twig Macros"] C --> D["Twig Inheritance"] D --> E["Twig Debugging"] E --> F["Custom Twig Extensions"] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

Common Mistakes

  1. Applying |date without checking if the value is a date: The date filter expects a DateTime object or a timestamp. Applying it to a string field that looks like a date may fail. Convert with |date only on actual date values.

  2. Forgetting |raw after translation: The |t filter escapes HTML by default. If your translated string contains HTML, apply |raw: {{ 'Contact <b>us</b>'|t|raw }}.

  3. Using |merge on a non-array value: merge only works on arrays. Applying it to a string returns an unexpected result. Verify the value is an array first.

  4. Confusing |nicetime with |date: nicetime returns relative time ("3 hours ago"). date returns formatted absolute time ("June 27, 2026"). They serve different purposes.

  5. Overusing |batch without a fill value: When the array length is not divisible by the batch size, the last row has fewer items. Use the fill parameter: |batch(3, '') to pad incomplete rows.

Practice Questions

  1. What filter converts "hello world" to "Hello World"? Answer: The |title filter capitalizes each word: {{ 'hello world'|title }} outputs "Hello World".

  2. How do you display a date as "3 days ago" in Twig? Answer: Use the |nicetime filter: {{ page.date|nicetime }}. It returns the relative time from the current moment.

  3. What is the difference between |t and |ta? Answer: |t translates a single string. |ta translates an array of strings. Both support placeholder replacement with a second argument.

  4. How would you display 12 items in a 4-column grid layout? Answer: Use the |batch(4) filter: {% for row in items|batch(4) %} creates rows of 4 items each.

  5. Challenge: Build a complete blog post listing template that: shows 9 most recent posts, displays them in a 3x3 grid using |batch(3), alternates card styles using cycle(['dark', 'light'], loop.index0), shows each post's date as relative time using |nicetime, truncates post summaries to 200 characters using |slice(0, 200), translates "Read more" using |t, and displays post tags using |join(', '). The template should handle the case where fewer than 9 posts exist.

FAQ

What does the `ta` filter do?

The ta filter translates each element in an array. If you have ['Submit', 'Cancel'] and the site is in Spanish, |ta returns ['Enviar', 'Cancelar'].

Can I chain filters in Twig?

Yes. Filters chain left to right: {{ value|striptags|upper|slice(0, 100) }} strips HTML, uppercases, then truncates to 100 characters. Order matters.

How do I create a range of letters in Twig?

Twig does not have a native letter range. Use range('a', 'z') works in some versions, or create a string and split it: {% set letters = 'abcdefghijklmnopqrstuvwxyz'|split('') %}.

What is the difference between `page.date` and `page.modified`?

page.date is the creation date set in frontmatter. page.modified is the last modified timestamp from the filesystem. Use page.modified for 'last updated' displays.

How do I check if a variable is an array before using array filters?

Use is iterable test: {% if items is iterable %} or check length: {% if items|length > 0 %}.

Mini Project

Goal: Build a feature-rich blog template using Twig filters and functions.

  1. Create a blog listing template that uses |batch(3) for grid layout
  2. Display dates with |nicetime for recency and |date('F j, Y') for full dates
  3. Implement a tag display using |sort and |join(', ')
  4. Add a reading time estimate using string splitting and math
  5. Create a "Related Posts" section using |slice(0, 3) to limit results
  6. Use |cycle for alternating row styles
  7. Use |t for all UI text with placeholder replacement
  8. Use range() for a numeric pagination display
  9. Add |safe_email for author contact links
  10. Use |absolute_url for canonical links

What's Next

Now you have powerful Twig filters at your disposal. Next, learn to create reusable template chunks:

Continue to Lesson 16: Twig Macros — Reusable template blocks, importing macros, and template composition.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro