Calendar Icon

Calendar

Calendar Main Screenshot
Statamic Calendar

Statamic Calendar

Recurring events and cached occurrences for Statamic. Works with Statamic 5 and 6.

Features

  • Define complex recurrence patterns using RFC 5545 (RRULE) via rlanvin/php-rrule
  • Materialize occurrences into Laravel cache for fast listings
  • Antlers tags for listing, current occurrence, next occurrences, and month grid
  • Month calendar view — server-rendered, navigable via query params, no JS required
  • JSON REST API for JS-based calendar components (opt-in)
  • iCalendar (.ics) feed for calendar app subscriptions + per-event "Add to calendar" downloads
  • Cache-build event for adding custom occurrence fields to tag/API output
  • Pagination for REST API responses and Antlers occurrence lists
  • Two URL strategies: query string (default, Statamic-native) or date segments
  • Control Panel Visit URL and Live Preview support for event entries
  • Example templates for index (list, archive, calendar grid) and show pages

Requirements

  • PHP 8.3+
  • Statamic 5 or 6
  • Laravel 11+

Installation

composer require el-schneider/statamic-calendar

Publish the config:

php artisan vendor:publish --tag=statamic-calendar

Blueprint Setup

The addon expects a dates grid field on your event entries. You can publish an example blueprint to get started:

php artisan vendor:publish --tag=statamic-calendar-examples

This publishes to resources/vendor/statamic-calendar/examples/. Copy the blueprint to your collection:

cp resources/vendor/statamic-calendar/examples/blueprints/collections/events/event.yaml \
resources/blueprints/collections/events/event.yaml

Or add the dates grid field to your existing blueprint manually. See the example blueprint for the expected sub-field handles (start_date, start_time, end_date, end_time, is_all_day, is_recurring, frequency, etc.).

Build the Cache

After adding events, rebuild the occurrence cache:

php artisan occurrences:rebuild

The cache rebuilds automatically when entries are saved or deleted in the Control Panel.

URL Strategies

The addon supports two strategies for occurrence URLs. Configure in config/statamic-calendar.php:

Query String (default)

Uses Statamic's native collection routing. The addon doesn't register any routes — your collection config handles everything:

# content/collections/events.yaml
route: '/events/{slug}'

Occurrence URLs look like /events/my-event?date=2025-03-15.

Date Segments (opt-in)

For SEO-friendly date-based URLs like /calendar/2025/03/15/my-event. Enable in config:

'url' => [
'strategy' => 'date_segments',
'date_segments' => [
'prefix' => 'calendar',
],
],

The addon registers a route at /{prefix}/{year}/{month}/{day}/{slug}.

Control Panel URLs and Live Preview

Saved entries in the configured events collection automatically get Visit URL and Live Preview actions in the Control Panel. No collection route is needed when using date_segments.

The action URL uses:

  1. the next occurrence;
  2. or the most recent occurrence when the event has no upcoming dates.

Entries without a valid occurrence show neither action. Live Preview resolves the occurrence from the unsaved form values, so changes to the title and dates are rendered before saving.

For query_string, keep the native collection route shown above because the occurrence URL is built on top of the entry URL. For date_segments, leave the collection route unset unless the collection needs one for another reason.

Existing Custom Entry Classes

The addon does not replace an existing custom Statamic Entry class. Add the calendar URL concern to that class instead:

use ElSchneider\StatamicCalendar\Entries\Concerns\HasCalendarUrls;
 
class Entry extends \Statamic\Entries\Entry
{
use HasCalendarUrls;
}

When using the Eloquent Driver, extend Statamic\Eloquent\Entries\Entry instead. Existing custom Entry-class registration remains unchanged.

iCalendar (.ics) Export

The addon exposes an .ics feed that calendar apps (Apple Calendar, Google Calendar, Outlook) can subscribe to. Enabled by default.

Subscribable Feed

GET /calendar.ics — returns all cached occurrences as a standard iCalendar feed. Calendar apps can subscribe to this URL and will receive updates as events change.

Single-Event Download

GET /calendar.ics/{occurrenceId} — returns a single occurrence as a downloadable .ics file. Use this for "Add to calendar" buttons. The response includes a Content-Disposition: attachment header.

Configuration

// config/statamic-calendar.php
'ics' => [
'enabled' => true, // set to false to disable .ics routes
'feed_url' => '/calendar.ics',
'calendar_name' => env('APP_NAME', 'Calendar'),
],

Template Usage

{{-- Subscribe link --}}
<a href="{{ calendar:ics_url }}">Subscribe to calendar</a>
 
{{-- Per-event download inside a calendar loop --}}
{{ calendar from="now" limit="10" }}
<a href="{{ calendar:ics_download_url }}">
Add to calendar
</a>
{{ /calendar }}

REST API

An opt-in JSON API for occurrences, designed for JS-based calendar components (FullCalendar, Toast UI Calendar, custom Alpine/Vue/React widgets, etc.) that build their view client-side.

Enable

Set the env var or publish the config:

STATAMIC_CALENDAR_API_ENABLED=true
// config/statamic-calendar.php
'api' => [
'enabled' => env('STATAMIC_CALENDAR_API_ENABLED', false),
'route' => env('STATAMIC_CALENDAR_API_ROUTE', 'api/calendar/occurrences'),
'middleware' => env('STATAMIC_CALENDAR_API_MIDDLEWARE', 'api'),
'max_per_page' => 100,
],

Endpoint

GET /api/calendar/occurrences

Parameter Type Description Default
from date Start date (ISO 8601 or Y-m-d) now
to date End date
limit int Max occurrences (ignored when paginating)
page int Page number; enables pagination
per_page int Items per page; also enables pagination 15
sort string asc or desc asc
tags string Comma-separated tag slugs
organizer string Organizer entry ID

Response

{
"data": [
{
"id": "abc-123-2026-03-06-150000",
"entry_id": "abc-123",
"title": "Laracon Online",
"slug": "laracon-online",
"teaser": "The best Laravel conference",
"organizer_id": "org-456",
"organizer_slug": "laravel-org",
"organizer_title": "Laravel",
"organizer_url": "/organizers/laravel-org",
"tags": ["tech", "laravel"],
"start": "2026-03-06T15:00:00+00:00",
"end": "2026-03-06T16:00:00+00:00",
"is_all_day": false,
"is_recurring": true,
"recurrence_description": "every week on Friday",
"url": "/events/laracon-online?date=2026-03-06"
}
]
}

Examples

// Month view
fetch('/api/calendar/occurrences?from=2026-03-01&to=2026-03-31')
 
// Week view
fetch('/api/calendar/occurrences?from=2026-03-02&to=2026-03-08')
 
// Next 5 upcoming
fetch('/api/calendar/occurrences?limit=5')
 
// Paginated archive
fetch('/api/calendar/occurrences?from=2026-01-01&page=2&per_page=25')
 
// Filtered by tag and organizer
fetch('/api/calendar/occurrences?tags=music,art&organizer=org-123')

CORS

The API uses Laravel's api middleware group, so cross-origin requests are handled by your app's config/cors.php. Laravel's default config already allows api/* paths from all origins — adjust as needed.

Custom Occurrence Fields

Need images, categories, or occurrence-specific flags in API/tag output? Listen to ElSchneider\StatamicCalendar\Events\OccurrenceBuilding. It runs once per materialized occurrence during cache rebuild, with access to both the source entry and resolved occurrence. See config/statamic-calendar.php and the event class docblock for the full recipe.

Setting Up Templates

Events Index

Create a page or route that uses the {{ calendar }} tag. For example, add to routes/web.php:

Route::statamic('events', 'events/index', [
'title' => 'Upcoming Events',
]);

Then create resources/views/events/index.antlers.html:

<h1>Upcoming Events</h1>
 
{{ calendar from="now" limit="20" }}
<a href="{{ url }}">
<h2>{{ title }}</h2>
<p>{{ start format="l, M j, Y" }}</p>
{{ if is_recurring }}
<p>{{ recurrence_description }}</p>
{{ /if }}
</a>
{{ /calendar }}

Event Show Page

Set the collection template to events/show, then create resources/views/events/show.antlers.html:

<h1>{{ title }}</h1>
 
{{ calendar:current_occurrence }}
<p>{{ start format="l, F j, Y" }}</p>
{{ if !is_all_day }}
<p>
{{ start format="g:i A" }}
{{ if end }}– {{ end format="g:i A" }}{{ /if }}
</p>
{{ else }}
<p>All day</p>
{{ /if }}
{{ if is_recurring }}
<p>Repeats: {{ recurrence_description }}</p>
{{ /if }}
{{ /calendar:current_occurrence }}
{{ calendar:next_occurrences :entry="id" limit="5" }}
<a href="{{ url }}">{{ start format="M j, Y" }}</a>
{{ /calendar:next_occurrences }}

The {{ calendar:current_occurrence }} tag reads the ?date= query parameter and resolves the matching occurrence for the current entry. When using the date_segments strategy, the date is extracted from the URL instead.

With the date_segments strategy, the occurrence controller also exposes the current occurrence directly to the show template: start, end, is_all_day, is_recurring, recurrence_description, occurrence_url, and occurrence_canonical_url. Compose SEO markup from those values plus your own blueprint fields:

{{ push:head }}
<link rel="canonical" href="{{ occurrence_canonical_url }}">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Event",
"name": {{ title | to_json }},
"startDate": "{{ start format='c' }}",
{{ if end }}"endDate": "{{ end format='c' }}",{{ /if }}
"url": "{{ occurrence_canonical_url }}"
}
</script>
{{ /push:head }}

Antlers Tags

{{ calendar }}

Lists occurrences from the cache (or resolves them live for non-default collections).

Parameter Description Default
from Start date now
to End date
limit Max occurrences (ignored when paginating)
paginate Items per page
page_name Query string page key page
as Results variable name when using pagination or grouped output occurrences
collection Collection handle config value
tags Filter by taxonomy terms

{{ calendar:month }}

Renders a month grid with weeks, days, and occurrences. Navigation via query params, fully server-rendered. See the example index template for usage.

Parameter Description Default
param Query string parameter name (allows multiple calendars per page) month
week_starts_on Day of week (0 = Sunday, 1 = Monday) 1
fixed_rows Always render 6 rows for consistent grid height false
collection Collection handle config value
tags Filter by taxonomy terms

Variables available inside the tag pair: month_label, year, month, prev_url, next_url, today, day_labels (loop with label, full_label), and weeksdaysdate, day, is_current_month, is_today, occurrences.

{{ calendar:current_occurrence }}

Resolves the current occurrence for the entry in context, based on the ?date= query param. Use as a tag pair — variables available inside:

  • start — Carbon date
  • end — Carbon date (nullable)
  • is_all_day — boolean
  • is_recurring — boolean
  • recurrence_description — human-readable recurrence rule
  • occurrence_url — the occurrence URL

{{ calendar:next_occurrences }}

Lists upcoming occurrences for a specific entry.

Parameter Description Default
entry Entry ID current context id
from Start date now
to End date
limit Max occurrences 5

{{ calendar:ics_url }}

Returns the URL to the .ics calendar feed. Use it to offer a "Subscribe" link:

<a href="{{ calendar:ics_url }}">Subscribe to calendar</a>

{{ calendar:ics_download_url }}

Returns the .ics download URL for a single occurrence. Use inside any {{ calendar }} loop to offer an "Add to calendar" button:

{{ calendar from="now" limit="10" }}
<h2>{{ title }}</h2>
<a href="{{ calendar:ics_download_url }}">Add to calendar</a>
{{ /calendar }}
Parameter Description Default
occurrence_id Occurrence ID ({entry_id}-{Y-m-d-His}) context occurrence_id, else id

{{ calendar:for_organizer }}

Lists upcoming occurrences for an organizer (from cache).

Parameter Description Default
organizer Organizer entry ID current context id
limit Max occurrences (ignored when paginating) 5
paginate Items per page
page_name Query string page key page
as Results variable name when using pagination or grouped output occurrences

Configuration

Key options in config/statamic-calendar.php:

Key Description Default
collection Source collection handle events
fields.dates Grid field handle dates
url.strategy query_string or date_segments query_string
url.query_string.param Query parameter name date
url.date_segments.prefix URL prefix for date segments calendar
api.enabled Enable JSON REST API false
api.route API route path api/calendar/occurrences
api.middleware Middleware group api
api.max_per_page Max API pagination page size 100
ics.enabled Enable .ics feed routes true
ics.feed_url Feed URL path /calendar.ics
ics.calendar_name Calendar name in .ics output APP_NAME
cache.key Cache store key statamic_calendar.occurrences
cache.days_ahead Recurrence expansion window 365

Cache

Occurrences are materialized into Laravel's cache for fast listing. The cache rebuilds automatically when entries are saved or deleted.

Manual rebuild:

php artisan occurrences:rebuild

Example Blueprint

Publish the example blueprint:

php artisan vendor:publish --tag=statamic-calendar-examples

Then copy it into your collection's blueprints directory (see Blueprint Setup above).

Testing

composer install
vendor/bin/pest

Contributing

Contributions are welcome! Please open an issue first to discuss what you'd like to change.

License

MIT — see LICENSE.md for details.