Oops, something went wrong! Site admins have been notified.

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:

  • title and body — name and description
  • field_when — a Smart Date field: start, end, and an optional recurrence rule
  • field_place — the location label
  • field_source — a taxonomy reference to the event_source vocabulary (which calendar it came from)
  • field_external_url — a "view on the source" link for imported events
  • field_import_uid — the upstream .ics event 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: 3

That 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 VEVENT URL property as a mappable source, so imported events can populate field_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 iCal RRULE string 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: true

With 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 date filter plugin, not the generic numeric one — the numeric plugin crashes at render time deep in Drupal's date code.
  • The exposed "filter by source" dropdown uses the taxonomy_index_tid plugin pointed at the event_source vocabulary, 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 event

Both 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.

← Part 4: The Data Model  ·  Part 6: The Forms →

More insights

Want updates from Chattanooga.Digital?

Pre-join the co-op to receive new posts, workshop schedules, and member updates.