Breadcrumb
-
- Blog
- How We Built It, Part 5: The Calendar
In Part 4: The Data Model we built typed content out of fields and taxonomy. The calendar is where that pays off. An event isn't a blob of HTML with a date pasted in — it's an event content type with a real date field, a place, a source, and a recurrence rule. Because the data is structured, Drupal can sort it, filter it, draw a month grid from it, and hand it back out as a subscribable calendar feed. This post walks the whole pipeline: model, import, render, export.
The event content type
Everything starts with one content type, event, that serves both events we author by hand and events we pull in from other people's calendars. Its fields are defined as recipe YAML so the structure rebuilds from git:
titleandbody— name and descriptionfield_when— a Smart Date field: start, end, and an optional recurrence rulefield_place— the location labelfield_source— a taxonomy reference to theevent_sourcevocabulary (which calendar it came from)field_external_url— a "view on the source" link for imported eventsfield_import_uid— the upstream.icsevent ID, marked unique so re-importing the same feed updates events instead of duplicating them
Smart Date and the recurrence trap
Drupal core's date field can't repeat. "Every third Thursday" or "every Wednesday at 7pm" needs the Smart Date module plus its smart_date_recur sub-module — and that sub-module has to be in the recipe's install: list right next to the parent, or the recurrence UI simply never appears.
Two settings on field_when are load-bearing. First, its cardinality is unlimited (-1): a recurring event is stored as one delta per occurrence, not as a single row with a rule attached. Forget that and recurrence silently collapses to a single date. Second, in field.field.node.event.field_when.yml we cap how far a rule expands:
third_party_settings:
smart_date_recur:
allow_recurring: true
month_limit: 3That month_limit: 3 looks tiny, and it is deliberate. The default is 12. Here's why we dropped it — and why the PHP container in Part 1 runs with a 1 GB memory limit. The month-grid renderer serializes every stored occurrence of every event in the result into the page's JavaScript settings. A Views date filter doesn't bound it — the module reads all of each event's date deltas directly, ignoring the filter. At a 12-month limit, a handful of weekly series ballooned to ~178 future occurrences and the calendar page exhausted even a 1 GB limit and returned a 500. Capping at 3 months keeps each series to ~13 occurrences, and a 12-hour re-import keeps rolling the 3-month window forward — so the calendar always shows the near future without ever blowing up.
Importing outside calendars over iCal
The community calendar aggregates other people's calendars — a Google Calendar, a Meetup group — over the open iCal (.ics) standard, using the Feeds module. An administrator adds a source with no deploy: go to /admin/content/feed, add an iCal Calendar feed, paste the public .ics URL, and click Import. (Meetup groups, for instance, publish at https://www.meetup.com/<group>/events/ical.) Events then re-sync automatically every 12 hours.
The feed type lives in feeds.feed_type.ical_calendar.yml and maps the calendar's fields onto ours: summary to title, description to body, dtstart/dtend/rrule to field_when, location to field_place, and the iCal uid to the unique field_import_uid so re-polls dedupe.
The subtle part is recurrence, and it's why we wrote three small custom modules instead of leaning on the stock parser:
cd_feeds_ical_url— extends the iCal parser to expose the VEVENTURLproperty as a mappable source, so imported events can populatefield_external_url. The feed type uses this parser (parser: feeds_ical_url) instead of the stock one.cd_smart_date_import— a Feeds target plus a node-presave hook that turns an iCalRRULEstring into a real Smart Date recurrence and expands it into per-occurrence date deltas, then cleans up the orphaned rule rows a re-import leaves behind.cd_event_ics— the export side (next section).
The two pieces hand off cleanly because of one flag in the feed type. The parser is configured with skip_recurrence: true:
parser: feeds_ical_url
parser_configuration:
filter_days_before: 0
skip_recurrence: trueWith skip_recurrence left off, the underlying iCal library would pre-expand a recurring event into one copy per occurrence — all sharing a single uid — which then collapse into one node on the unique-UID dedupe and fight with our own expander. With it true, the parser hands over one event carrying the raw RRULE string, and cd_smart_date_import performs the single authoritative expansion. It bounds that expansion to a HORIZON_MONTHS = 3 horizon — the same 3-month bound as field_when's month_limit, kept in lockstep on purpose.
Rendering: the events View and the month grid
The display is a single View, views.view.events.yml, filtered to published events whose date is now or later, sorted ascending. It feeds the public calendar at /calendar, which renders a month grid via the fullcalendar_view module. Two filters are easy to get wrong:
- The "upcoming only" filter on the Smart Date field uses the
datefilter plugin, not the genericnumericone — the numeric plugin crashes at render time deep in Drupal's date code. - The exposed "filter by source" dropdown uses the
taxonomy_index_tidplugin pointed at theevent_sourcevocabulary, so visitors can narrow the calendar to one source with a query like/calendar?source=4.
Exporting: subscribe to our calendar
A calendar you can only read in a browser is half a calendar. The cd_event_ics module turns the data back around and serves it as iCal so anyone can subscribe in Apple Calendar, Google Calendar, or Outlook. It exposes two routes:
/events/ics # subscription feed of all current + future events
/events/{node}/ics # download a single eventBoth are anonymous-accessible and emit RFC 5545 VCALENDAR output. Point a calendar app at https://www.chattanooga.digital/events/ics (or the webcal:// equivalent) and it re-fetches on its own schedule — the community calendar lands directly in someone's personal calendar, the same open standard we used to pull events in running in reverse.
What's next
The calendar takes data and shows it. Next we go the other direction — letting people give us data. In Part 6 we build the forms: the membership signup and contact forms, the fields behind them, and the spam protection that keeps the bots out.
More insights
Want updates from Chattanooga.Digital?
Pre-join the co-op to receive new posts, workshop schedules, and member updates.