Grav Twig Filters & Functions — Date, Translate, Array and Utility Filters
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
Applying
|datewithout checking if the value is a date: Thedatefilter expects a DateTime object or a timestamp. Applying it to a string field that looks like a date may fail. Convert with|dateonly on actual date values.Forgetting
|rawafter translation: The|tfilter escapes HTML by default. If your translated string contains HTML, apply|raw:{{ 'Contact <b>us</b>'|t|raw }}.Using
|mergeon a non-array value:mergeonly works on arrays. Applying it to a string returns an unexpected result. Verify the value is an array first.Confusing
|nicetimewith|date:nicetimereturns relative time ("3 hours ago").datereturns formatted absolute time ("June 27, 2026"). They serve different purposes.Overusing
|batchwithout 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
What filter converts "hello world" to "Hello World"? Answer: The
|titlefilter capitalizes each word:{{ 'hello world'|title }}outputs "Hello World".How do you display a date as "3 days ago" in Twig? Answer: Use the
|nicetimefilter:{{ page.date|nicetime }}. It returns the relative time from the current moment.What is the difference between
|tand|ta? Answer:|ttranslates a single string.|tatranslates an array of strings. Both support placeholder replacement with a second argument.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.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 usingcycle(['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
Mini Project
Goal: Build a feature-rich blog template using Twig filters and functions.
- Create a blog listing template that uses
|batch(3)for grid layout - Display dates with
|nicetimefor recency and|date('F j, Y')for full dates - Implement a tag display using
|sortand|join(', ') - Add a reading time estimate using string splitting and math
- Create a "Related Posts" section using
|slice(0, 3)to limit results - Use
|cyclefor alternating row styles - Use
|tfor all UI text with placeholder replacement - Use
range()for a numeric pagination display - Add
|safe_emailfor author contact links - Use
|absolute_urlfor 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