Meridian handbook
Content modellingFields

Dates and times

The date field (a calendar day), the datetime field (a wall-clock time in an IANA zone with its UTC instant) and the recurrence field (an event's repeating schedule and its occurrences), with parameters, stored shapes and template values.

All three are held once for every locale. Times are stored with the zone they belong to, so a 19:00 start stays 19:00 on both sides of a clock change.

date

A calendar day with no time and no zone: an opening date, a deadline, a date of birth.

fields:
  - { name: openedOn, type: date, min: "1800-01-01" }
  - { name: closesOn, type: date, index: true }
ParameterTypeDefaultWhat it does
minYYYY-MM-DDnoneThe earliest day accepted.
maxYYYY-MM-DDnoneThe latest day accepted.
CodeMessage
dateEnter a date as YYYY-MM-DD
min_dateMust be on or after 1800-01-01
max_dateMust be on or before …

Stored value: a string, "2026-04-01". With index: true it is projected as that day at midnight UTC, so lists filter and sort by it.

Template value: a date value. Format it with |date, which writes the day in the request locale’s words and order:

<p>Open since {{ entry.openedOn|date:"long" }}</p>
<time datetime="{{ entry.openedOn.date }}">{{ entry.openedOn|date }}</time>
PropertyWhat it is
dateThe day as YYYY-MM-DD.
local, utcThe day at midnight, as a time.
isoRFC 3339 form.
isAllDayAlways true for a date.
zoneUTC.

A date field reads an old datetime value lossily (its local day) and an ISO date in a text value exactly.

datetime

A moment in time in the zone it happens in: a publication time, a start time, a deadline at noon in Paris.

fields:
  - { name: doorsOpen, type: datetime, zone: Europe/London }
  - { name: launchAt, type: datetime, min: "2026-01-01T00:00:00Z", seconds: true }
  - { name: festivalDay, type: datetime, all_day: true }
ParameterTypeDefaultWhat it does
zoneIANA zonethe site’s, then the locale’s, then UTCThe zone the editor offers first. A value may be in any zone.
minRFC 3339 instantnoneThe earliest instant accepted.
maxRFC 3339 instantnoneThe latest instant accepted.
all_daybooleanfalseOffers an all-day switch. An all-day value renders no time. Without this parameter an all-day value is refused.
secondsbooleanfalseEdits to the second instead of the minute.
CodeMessage
zone_emptyChoose a time zone
zone_unknownUnknown time zone “X”
datetimeEnter a date and time
min_date / max_dateMust be on or after / before the bound
all_dayThis field does not accept all-day values

Stored value:

{ "local": "2026-03-29T10:00", "zone": "Europe/Paris", "utc": "2026-03-29T08:00:00Z", "allDay": false }

The wall clock (local) and the zone are what the editor chose; the UTC instant is recomputed from them on every save. In a seed file or an import you may give local and zone alone, or a bare RFC 3339 string, which is read as UTC. With index: true the instant is projected, so lists sort by it.

Template value:

PropertyWhat it is
localThe wall-clock time in its own zone.
utcThe same instant in UTC.
zoneThe IANA zone.
isoThe instant in RFC 3339.
dateThe local day as YYYY-MM-DD.
isAllDayWhether the editor chose all day.
inZone(name)The instant in another zone.
<time datetime="{{ entry.doorsOpen|iso }}">{{ entry.doorsOpen|datetime:"long short" }}</time>
{% if entry.festivalDay.isAllDay %}{{ entry.festivalDay|date:"full" }}{% endif %}

|date, |datetime, |time, |relative and |iso all accept the value directly (Filters). The article type’s core publishedAt is a datetime.

recurrence

An event’s schedule: a first start and duration in a zone, an optional repeat rule (RFC 5545 RRULE), extra dates, removed dates, and changes to single instances. The core event type’s occurrences field is a recurrence, and a custom type may declare one too.

# config/schema/event.yaml
extends: event
fields:
  - { name: occurrences, label: { en-GB: Dates and times } }   # restate the core field to relabel it
# a custom type
fields:
  - { name: sessions, type: recurrence, zone: Europe/London, all_day: false }
ParameterTypeDefaultWhat it does
zoneIANA zonethe site’sThe zone the editor offers first.
all_daybooleantrueOffers the all-day switch.

The editor offers once, daily, weekly on chosen days, monthly on a date or an ordinal weekday, yearly, an end by date or count, and a custom rule for anything else, and shows the next instances as the rule is edited.

Stored value:

{
  "start": "2026-03-13T19:00",
  "duration": "PT2H",
  "zone": "Europe/London",
  "allDay": false,
  "rrule": "FREQ=WEEKLY;BYDAY=FR",
  "rdates": ["2026-07-04T19:00"],
  "exdates": ["2026-04-03"],
  "overrides": {
    "5": { "status": "cancelled", "note": "Bank holiday" },
    "6": { "start": "2026-04-25T20:00" }
  }
}
KeyWhat it is
startThe first start, wall clock YYYY-MM-DDTHH:MM in zone. Required.
durationISO 8601 duration, such as PT2H, PT90M or P1D.
zoneThe IANA zone. Required.
allDayWhether instances are whole days.
rruleThe RRULE value without the RRULE: prefix. Empty for a one-off. A rule with no end is expanded to the end of the window below.
rdatesExtra starts, wall clock.
exdatesDays removed entirely, YYYY-MM-DD.
overridesChanges to single instances, keyed by the instance’s number in the rule’s own sequence (extra dates are numbered from 100000, so adding one renumbers nothing). Each takes start, duration, status (cancelled or moved) and note.

Validation reports each problem at its part: zone (Choose a time zone), start (Enter the first date and time), duration, rrule (The repeat rule is not valid: …), rdates[n], exdates[n] and overrides.<n>.

Occurrences. On save the engine expands the schedule into individual occurrences in local time, then converts each to UTC, for a rolling window around today: 90 days back and 540 days ahead by default (events.occurrence_window in engine.yaml), rolled forward daily. A cancelled instance keeps its row and is marked cancelled; a removed date has none.

Template value. A template never sees the rule’s expansion logic, only the materialised occurrences of the published document:

PropertyWhat it is
next(n)The next n occurrences that have not ended, cancelled ones left out.
upcoming()Every occurrence that has not ended, cancelled ones left out.
past()Every occurrence that has ended, cancelled ones left out.
between(a, b)Occurrences starting between two times, cancelled ones left out.
allEvery occurrence in the window, cancelled ones included.
countHow many there are in all.
start, zone, isAllDay, rrule, duration, repeatsThe schedule’s own facts; repeats is true when there is a rule or extra dates.

Each occurrence has startsAt, endsAt (UTC), local, localEnd (wall clock), zone, date (YYYY-MM-DD), isAllDay, isCancelled, isMoved, status, note and ord (its number in the sequence).

<ul>
  {% for o in entry.occurrences.next(5) %}
  <li><time datetime="{{ o.startsAt|iso }}">{{ o.local|datetime:"full short" }}</time>{% if o.note %} ({{ o.note }}){% endif %}</li>
  {% endfor %}
</ul>

Occurrences are read for a top-level recurrence field, and they are the document’s: give a type one recurrence field. How event pages and lists read occurrences is covered in Templates.