With a solid understanding of Liquid basics, let's explore advanced concepts that enable sophisticated notification templates capable of handling complex real-world scenarios.

Understanding date, time, and localization

Time-sensitive notifications require careful handling of dates and timezones.

Date formatting patterns

In programming, dates and times are often stored in a standardized format, such as "2024-12-25T15:30:00Z", which computers understand easily but humans find hard to read.

Liquid's date filter uses strftime format strings to transform these machine-friendly timestamps into formats that make sense to your users, like "December 25, 2024" or "Wed, Dec 25 at 3:30 PM."

Format strings are like templates for dates. You use special codes, each starting with %, to represent different parts of a date. For example, %B means "full month name" and %d means "day of month." Liquid replaces these codes with the actual values from your timestamp:

Data
{
  "event": {
    "start_time": "2024-12-25T15:30:00Z"
  }
}
Template
{{ event.start_time | date: "%B %d, %Y" }}
{{ event.start_time | date: "%m/%d/%Y" }}
{{ event.start_time | date: "%Y-%m-%d" }}
{{ event.start_time | date: "%A, %b %d at %I:%M %p" }}
Output
December 25, 2024
12/25/2024
2024-12-25
Wednesday, Dec 25 at 03:30 PM

Common format codes:

  • %Y - 4-digit year (2024)
  • %m - Month number (01-12)
  • %d - Day of month (01-31)
  • %B - Full month name (December)
  • %b - Abbreviated month (Dec)
  • %A - Full day name (Wednesday)
  • %a - Abbreviated day (Wed)
  • %I - 12-hour format (01-12)
  • %H - 24-hour format (00-23)
  • %M - Minutes (00-59)
  • %p - AM/PM

Relative time calculations

For dynamic time-based content, you can calculate relative dates. "Relative time" means expressing a date in relation to today, like "tomorrow," "in 3 days," or "2 weeks ago"—rather than showing the specific date. This is more natural for users since they think about upcoming events relative to right now:

For events in the near future (days):

Data
{
  "event": {
    "date": "2024-12-10T10:00:00Z",
    "name": "Product Launch"
  },
  "now": "2024-12-08T10:00:00Z"
}
Template
{% assign days_until = event.date | date: "%s" | minus: now | date: "%s" | divided_by: 86400 | ceil %}

{% if days_until == 0 %}
{{ event.name }} is today at {{ event.date | date: "%I:%M %p" }}!
{% elsif days_until == 1 %}
{{ event.name }} is tomorrow at {{ event.date | date: "%I:%M %p" }}
{% elsif days_until > 1 and days_until <= 7 %}
{{ event.name }} is in {{ days_until }} days
{% else %}
{{ event.name }} is on {{ event.date | date: "%B %d" }}
{% endif %}
Output
Product Launch is in 2 days

For events in the next few hours:

Data
{
  "meeting": {
    "start_time": "2024-12-08T14:30:00Z",
    "title": "Team Standup"
  },
  "now": "2024-12-08T13:45:00Z"
}
Template
{% assign seconds_until = meeting.start_time | date: "%s" | minus: now | date: "%s" %}
{% assign hours_until = seconds_until | divided_by: 3600 %}
{% assign minutes_until = seconds_until | divided_by: 60 | modulo: 60 %}

{% if hours_until < 1 %}
{{ meeting.title }} starts in {{ minutes_until }} minutes!
{% elsif hours_until < 24 %}
{{ meeting.title }} starts in {{ hours_until }} hours
{% else %}
{{ meeting.title }} starts {{ meeting.start_time | date: "%B %d at %I:%M %p" }}
{% endif %}
Output
Team Standup starts in 45 minutes!

For events weeks or months away:

Data
{
  "conference": {
    "date": "2025-03-15T09:00:00Z",
    "name": "DevCon 2025"
  },
  "now": "2024-12-08T10:00:00Z"
}
Template
{% assign days_until = conference.date | date: "%s" | minus: now | date: "%s" | divided_by: 86400 %}
{% assign weeks_until = days_until | divided_by: 7 %}
{% assign months_until = days_until | divided_by: 30 %}

{% if days_until <= 7 %}
{{ conference.name }} is in {{ days_until }} days
{% elsif days_until <= 30 %}
{{ conference.name }} is in {{ weeks_until }} weeks
{% elsif days_until <= 90 %}
{{ conference.name }} is in about {{ months_until }} months
{% else %}
{{ conference.name }} is on {{ conference.date | date: "%B %d, %Y" }}
{% endif %}
Output
DevCon 2025 is in about 3 months

For past events (how long ago):

Data
{
  "user": {
    "last_login": "2024-12-01T08:00:00Z",
    "name": "Sarah"
  },
  "now": "2024-12-08T10:00:00Z"
}
Template
{% assign seconds_ago = now | date: "%s" | minus: user.last_login | date: "%s" %}
{% assign days_ago = seconds_ago | divided_by: 86400 %}

{% if days_ago == 0 %}
Welcome back, {{ user.name }}! You were just here earlier today.
{% elsif days_ago == 1 %}
Welcome back, {{ user.name }}! You were last here yesterday.
{% elsif days_ago <= 7 %}
Welcome back, {{ user.name }}! You were last here {{ days_ago }} days ago.
{% elsif days_ago <= 30 %}
It's been {{ days_ago | divided_by: 7 }} weeks since your last visit, {{ user.name }}.
{% else %}
It's been over a month since we've seen you, {{ user.name }}!
{% endif %}
Output
Welcome back, Sarah! You were last here 7 days ago.

Timezone handling

When working with users across different timezones, you can format dates in their local time. A timezone is basically the local clock setting for different regions of the world—when it's 3:00 PM in New York, it's 12:00 PM in Los Angeles and 8:00 PM in London.

If you're sending a notification about a webinar starting at "7:00 PM," users need to know if that's 7 PM in their timezone or yours. Liquid can convert timestamps to display in each user's local timezone:

Data
{
  "webinar": {
    "start_time": "2024-11-15T19:00:00Z",
    "title": "Q4 Product Updates"
  },
  "user": {
    "timezone": "America/New_York"
  }
}
Template
{{ webinar.title }}

Starts: {{ webinar.start_time | date: "%B %d at %I:%M %p %Z", user.timezone }}
({{ webinar.start_time | date: "%I:%M %p UTC", "UTC" }})
Output
Q4 Product Updates

Starts: November 15 at 02:00 PM EST
(07:00 PM UTC)

Localization patterns

Adapt your notifications for international users by formatting dates and currency according to their locale. Different countries have different conventions for displaying information—Americans write dates as "12/25/2024" (month/day/year), while Europeans write "25/12/2024" (day/month/year). Currency symbols also differ ($99.99 vs 99,99€). By detecting your user's locale (their language and region settings), you can format data in the way that's familiar to them:

Data
{
  "user": {
    "locale": "de-DE",
    "country": "DE"
  },
  "invoice": {
    "date": "2024-11-15",
    "amount": 99.99
  }
}
Template
Invoice Date: {% case user.locale %}
{% when "en-US" %}
  {{ invoice.date | date: "%m/%d/%Y" }}
{% when "en-GB" %}
  {{ invoice.date | date: "%d/%m/%Y" }}
{% when "de-DE" %}
  {{ invoice.date | date: "%d.%m.%Y" }}
{% when "ja-JP" %}
  {{ invoice.date | date: "%Y年%m月%d日" }}
{% else %}
  {{ invoice.date | date: "%Y-%m-%d" }}
{% endcase %}

Total: {% case user.country %}
{% when "US" %}
${{ invoice.amount | round: 2 }}
{% when "GB" %}
£{{ invoice.amount | round: 2 }}
{% when "DE" %}
{{ invoice.amount | round: 2 }}€
{% when "JP" %}
¥{{ invoice.amount | round: 0 }}
{% else %}
{{ invoice.amount | round: 2 }} {{ user.country }}
{% endcase %}
Output
Invoice Date: 15.11.2024

Total: 99.99€

Understanding whitespace control

Whitespace management in Liquid templates is crucial for generating clean output, especially for plain text emails or structured data formats. "Whitespace" refers to invisible characters like spaces, tabs, and line breaks, e.g. when you press Enter. When you write a template with tags and logic spread across multiple lines for readability, Liquid normally preserves all those spaces and line breaks in the output. This can create awkward extra blank lines or spaces you didn't intend. Whitespace control lets you remove these extra spaces while keeping your template code readable.

Whitespace control syntax

Use hyphens (-) inside tag delimiters to strip whitespace. Adding a hyphen tells Liquid "don't include the whitespace next to this tag":

  • {%- - Strip whitespace before the tag
  • -%} - Strip whitespace after the tag
Data
{
  "user": {
    "name": "Sarah",
    "premium": true
  }
}
Template
{%- if user.premium -%}
Premium Member
{%- endif -%}
Output
Premium Member

Practical examples

Clean list generation:

Data
{
  "order": {
    "items": [
      {
        "name": "Laptop",
        "quantity": 1
      },
      {
        "name": "Mouse",
        "quantity": 2
      }
    ]
  }
}
Template
Your order includes:
{%- for item in order.items %}
- {{ item.name }} (Qty: {{ item.quantity }})
{%- endfor %}
Output
Your order includes:
- Laptop (Qty: 1)
- Mouse (Qty: 2)

Inline conditional text:

Data
{
  "user": {
    "name": "Alex",
    "is_premium": true
  }
}
Template
Hello {{ user.name -}}
{%- if user.is_premium %}, our valued premium member{%- endif -%}
!
Output
Hello Alex, our valued premium member!

Compact data formatting:

Data
{
  "items": [
    {
      "id": "A1",
      "name": "Widget"
    },
    {
      "id": "B2",
      "name": "Gadget"
    }
  ]
}
Template
Items: [
{%- for item in items -%}
"{{ item.name }}"
{%- unless forloop.last -%}, {%- endunless -%}
{%- endfor -%}
]
Output
Items: ["Widget", "Gadget"]

Understanding encoding and escaping

Security and data integrity are paramount in notifications. "Encoding" and "escaping" are ways of making data safe to use in different contexts. Some characters have special meanings in URLs, HTML, or JavaScript, and if you include them directly, they can break your code or create security vulnerabilities. For example, a space in a URL needs to become %20, and < in HTML needs to become &lt; to display correctly. Liquid provides filters that automatically handle these conversions for you.

URL encoding

URLs can't contain certain characters like spaces, plus signs, or special symbols, so when building URLs with dynamic parameters, always use url_encode to safely include user data.

For example, if a user's email is "[email protected]" and you put it directly in a URL, the + and @ characters will cause problems. URL encoding converts these characters into safe alternatives (like %2B for +):

Data
{
  "user": {
    "email": "[email protected]",
    "id": "user_123"
  },
  "campaign": {
    "name": "Summer Sale 2024",
    "code": "SAVE20"
  }
}
Template
Track your order:
https://example.com/dashboard?email={{ user.email | url_encode }}&campaign={{ campaign.name | url_encode }}

Redeem your discount:
https://example.com/checkout?code={{ campaign.code }}&user_id={{ user.id | url_encode }}
Output
Track your order:
https://example.com/dashboard?email=sarah%2Btest%40example.com&campaign=Summer%20Sale%202024

Redeem your discount:
https://example.com/checkout?code=SAVE20&user_id=user_123

HTML escaping

In HTML, characters like <, >, and & have special meanings—they're used to create tags and entities. Therefore, when displaying user-generated content or data that might contain HTML characters within your HTML emails, you should always escape it.

For example, if someone's name is "Sarah & Friends" or a comment includes <script> tags, displaying this directly in HTML could either break your layout or create security risks. HTML escaping converts these special characters into "HTML entities" (safe codes) that display correctly without being interpreted as code:

  • < becomes &lt;
  • > becomes &gt;
  • & becomes &amp;
  • " becomes &quot;
  • ' becomes &#39;
Data
{
  "comment": {
    "author": "Sarah & Friends",
    "text": "<script>alert('test')</script>Great product!"
  }
}
Template
<div class="comment">
<strong>{{ comment.author | escape }}</strong> wrote:
<p>{{ comment.text | escape }}</p>
</div>
Output
<div class="comment">
<strong>Sarah &amp; Friends</strong> wrote:
<p>&lt;script&gt;alert(&#39;test&#39;)&lt;/script&gt;Great product!</p>
</div>

JSON encoding

When embedding data in JavaScript or JSON structures, use the json filter. JSON (JavaScript Object Notation) is a format for representing data that JavaScript can understand. If you're putting Liquid data into a JavaScript variable or passing it to a script, the json filter properly formats it by adding quotes around strings, converting objects to the right structure, and handling special characters so JavaScript can parse it correctly:

Data
{
  "user": {
    "name": "Sarah Chen",
    "preferences": {
      "theme": "dark",
      "notifications": true
    }
  }
}
Template
<script>
const userName = {{ user.name | json }};
const userPreferences = {{ user.preferences | json }};

console.log(`Welcome, ${userName}!`);

</script>
Output
<script>
const userName = "Sarah Chen";
const userPreferences = {"theme":"dark","notifications":true};

console.log(`Welcome, ${userName}!`);

</script>

Unlock powerful personalization with Knock

Throughout this guide, we've explored Liquid's capabilities from basic variable substitution to complex conditional logic and data transformations. These features make Liquid the ideal choice for creating sophisticated notification templates that adapt to your users' data and preferences.

When you're ready to implement these Liquid templates in a production notification system, Knock provides a comprehensive platform that leverages Liquid's full potential. With Knock, you can create multi-channel notification workflows that use the same Liquid templates across email, SMS, push, and in-app notifications, ensuring consistent messaging while adapting content for each channel's constraints.

Knock's template editor provides syntax highlighting, real-time preview with your actual user data, and built-in testing tools that make it easy to verify your Liquid logic handles edge cases correctly. It also has custom Liquid helpers designed specifically for notifications, such as format_date_in_locale for international date formatting, currency for regional price display, pluralize for dynamic text, and many more utilities that make building sophisticated notification templates easier.

Whether you're building simple transactional emails or complex multi-step notification workflows, mastering Liquid templates enables you to create personalized, dynamic content that engages your users effectively. With Liquid and the right messaging platform, you can build a communication system that scales with your product and delights your users.