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

In Part 3: Building Components we styled the homepage with reusable theme components. This post goes underneath the styling, to the layer that actually makes Drupal worth the trouble: the data model. We promised back in Part 0 that Drupal stores content as typed entities, not as a blob. Now we'll prove it, using the real event content type that drives our community calendar — every field is a file you can read in this repo.

One warning up front: the single biggest reason smart people bounce off Drupal is vocabulary. "Entity," "bundle," "field," "view" — Drupal uses each word a little differently than the rest of the web does. The next fifteen minutes are the vocabulary that makes the admin screens (and the rest of this series) stop feeling like a foreign language.

The blob vs. the structure

Picture how WordPress stores a piece of content. Almost everything is a "post": one big column of HTML, plus a loosely-attached bag of key/value metadata. Wonderful for articles. But an event isn't an article — it has a start time, an end time, a recurrence rule, a place, and a source calendar. In the blob model you either jam that structure into the HTML where no code can reach it, or you bolt on a plugin that invents its own private way to fake structure.

Drupal inverts this. The core idea is the entity: a typed thing made of typed fields. Because the fields are real and typed, Drupal can act on them — sort events by date, filter by source, render a calendar, emit an iCal feed — without any plugin guessing at your intent. The rest of this post is that idea, made concrete.

Entities and bundles

An entity is anything Drupal can store, load, reference, cache, and expose to an API. You'll touch four kinds:

  • Node — content a visitor reads. A blog post, a page, an event.
  • User — an account. Greg, the content editor.
  • Taxonomy term — a category or tag. "Authored," "meetup.com."
  • Media — a file plus its metadata. An uploaded image with alt text.

Every entity has a bundle — a subtype. Node bundles include "blog post," "page," and "event." A bundle is just a named set of fields. So when we say "the Event content type," we mean a node bundle named event. Here is its definition, exactly as it lives in the recipe:

# recipes/chattanooga_digital/config/node.type.event.yml
name: Event
type: event
description: 'Community calendar events — authored or imported from external iCal feeds.'

That file is the content type. No clicking through /admin/structure/types/add — the structure is code, checked into git, the same on your laptop and on the server.

Fields vs. properties

Before the fields themselves, one distinction that clears up half of Drupal's confusion:

  • A property is built into the entity itself — the title, the author, the publish status. Every node has these, uniformly. You can't add or remove a property without writing code.
  • A field is attached to a bundle by a site builder — the event's date, place, and source. You add and remove fields freely, as configuration, no code.

So an event's title is a property (every node has one), but its start time is a field (only events need one). Every field is actually two files in the recipe: a storage definition (the data shape, shared across bundles) and a field definition (this bundle's instance — its label, help text, whether it's required).

The four fields that make an Event an Event

Here are the fields that turn a generic node into something a calendar can use. Each is a real file under recipes/chattanooga_digital/config/.

1. field_when — when it happens (Smart Date). This is the field that justifies the whole entity model. It isn't a plain date string; it's a smartdate field with a start, an end, and an optional recurrence rule:

# field.field.node.event.field_when.yml
label: 'When'
required: true
field_type: smartdate
third_party_settings:
  smart_date_recur:
    allow_recurring: true
    month_limit: 3

The storage for this field has cardinality: -1 (unlimited) for a subtle reason: a recurring event holds one stored value per occurrence. With cardinality 1, Drupal silently truncates a weekly study group to its first meeting. The month_limit: 3 is a deliberate cap — recurrence expands every unlimited rule out to that horizon, and the calendar serializes every occurrence into the page, so a longer limit literally exhausts memory (more on that in Part 5).

2. field_place — where it happens (plain string). A simple text label like "Code Journeymen LLC" or "Online Zoom":

# field.field.node.event.field_place.yml
label: Place
required: false
field_type: string

3. field_source — which calendar it came from (entity reference → taxonomy term). This is a relationship: instead of typing the source name into every event, the event points at a term in the event_source vocabulary. Change the term once and every event that references it updates.

# field.field.node.event.field_source.yml
label: Source
field_type: entity_reference
settings:
  handler: 'default:taxonomy_term'
  handler_settings:
    target_bundles:
      event_source: event_source

4. field_external_url — the upstream link (link field). For events imported from meetup.com or a Google Calendar feed, this carries the "view on the source site" URL; it's empty for events authored directly in Drupal.

Each field has a field type (how the data is stored — smartdate, string, entity_reference, link), a widget (the edit form — a date picker vs. a text box vs. an autocomplete), and a formatter (how it renders — "short date" vs. "full date with timezone"). The same stored field can display differently in different contexts, which is exactly what we lean on next.

Views: turning entities into a calendar

Typed fields are only half the payoff. The other half is the View — Drupal's query builder. You pick a base entity, add filters and sorts, choose fields to show, and pick a format: a list, a grid, a block, a JSON feed, a full calendar. No SQL. Our calendar is one View, views.view.events.yml, with three displays sharing the same query:

  • Default — the canonical "Upcoming events" query.
  • Upcoming (3 nearest) — a block display, the three soonest events, embeddable anywhere (including as a Canvas component, per Part 3).
  • Month — a page display at /calendar rendered by the fullcalendar_view style.

The View's power is that it filters and sorts on the typed fields directly. "Only future events, soonest first" is two lines because field_when is a real date:

# views.view.events.yml — sort + filter on the typed date
sorts:
  field_when_value:
    order: ASC
filters:
  field_when_value:
    plugin_id: date
    operator: '>='
    value:
      value: now
      type: offset

And the calendar's per-source toggle ("show me only the meetup.com events") works because field_source is a real reference — the View filters on it with the taxonomy_index_tid plugin, rendered as checkboxes. None of this is possible if "when" and "source" are buried inside a blob of HTML. That's the whole argument from Part 0, now standing in front of you as runnable config.

Modules and plugins, in one breath

Two last words, because they're everywhere in Drupal docs. A module is a unit of code you enable or disable — Drupal ships ~100 in core, and ~50,000 more live on drupal.org. A plugin is an extension point inside a module: field types, widgets, formatters, and Views filters are all plugins. When we ran composer require drupal/smart_date, the smartdate field type plugin simply appeared in the field list. You don't write PHP to use modules; you only write PHP to create new plugins, which this site barely needs.

Everything in this post — the event bundle, its four fields, the event_source vocabulary, the events View — is configuration. It exports to YAML and lives in git, which is precisely why make rebuild can recreate it identically from nothing.

What's next

You now have the mental model: entities are typed things, bundles group their fields, fields carry the structure, and Views read that structure back out as lists, blocks, and feeds. In Part 5 we take the event bundle and the events View and turn them into the live community calendar — Smart Date recurrence, the fullcalendar_view render, and the memory trap that month_limit: 3 exists to avoid.

← Part 3: Building Components  ·  Part 5: The Calendar →

More insights

Want updates from Chattanooga.Digital?

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