Breadcrumb
-
- Blog
- How We Built It, Part 2: Recipes 101 and Branding
In Part 1 you ran make build and watched it apply "the recipes in order." That command is doing the most important work in the whole project, so this post opens the hood. We'll cover what a Drupal Recipe actually is, how the two recipes layer to build the site, and then walk through the first thing the recipe touches: turning the stock theme into the Gig City brand.
What a Drupal Recipe actually is
A Drupal Recipe is a folder of declarative YAML that describes site structure — content types, fields, views, menus, pages, theme settings — plus an optional cleanup script. Instead of clicking through admin screens to build the site (which leaves no record of what you did or why), you write the structure down as files in git, and Drupal applies them.
Our recipe lives at recipes/chattanooga_digital/. The entry point is one file, recipe.yml, with three sections that do all the heavy lifting:
recipes:— other recipes to apply first. Ours lists exactly one:byte(the upstream SaaS-style base theme and its components). This is how recipes compose — you build on top of someone else's.install:— the modules to enable. Ours lists the things the site genuinely needs:smart_dateandsmart_date_recur(real date fields with recurrence),feedsandfeeds_ical(importing external calendars),fullcalendar_view,honeypot(spam protection),easy_breadcrumbandmenu_breadcrumb, and several small custom modules likecd_event_ics.config:— the settings to change. Each entry names a Drupal config object and patches specific keys. This is where the site identity, page cache, timezone, permissions, and field settings all get pinned.
The single most important thing to understand: this YAML is the source of truth, not the running database. If you change the site by clicking around in the admin UI, that change vanishes the next time the site is rebuilt from the recipe. Every real change lives in a recipe file (or in the theme source). That discipline is what makes the site reproducible from git — the promise we made back in Part 0.
The build order: byte, then chattanooga_digital, then cleanup
When you ran make build, it ran a fixed sequence. The same sequence runs identically on staging and production (via a service called fix-settings), so the site is built the same way everywhere:
composer update # pull module code
drush updatedb # run any pending DB updates
drush recipe byte # 1. upstream base theme + components
drush recipe chattanooga_digital # 2. our rebrand + structure + content
drush php:script recipes/chattanooga_digital/post_install.php # 3. cleanupWhy three steps instead of one? Because recipes are additive by design — they import config and content, but they have no clean way to delete something an upstream recipe shipped. The Byte recipe ships a stock demo homepage, a handful of placeholder marketing pages, some "expense visibility" blog posts, and a default main menu. We don't want any of that, but the recipe API can't remove it.
That's the job of post_install.php. It runs once after the recipe and does the deletions and pointer-setting the recipe can't express:
- Sets the front page to our homepage by looking it up via its stable UUID, then writing
system.site:page.front. - Deletes the stock Byte demo Canvas pages (anything not matching our titles like "Chattanooga", "Programs", "Workshop", "Blog").
- Deletes the stock placeholder blog posts and the stock main-menu links, keeping only the menu links our recipe ships.
The whole script is written to be idempotent — safe to run over and over. It looks entities up by UUID, and short-circuits if the desired state is already in place. That matters because on staging and production it runs on every deploy, not just the first.
What a Recipe can't do (and where that goes instead)
The split is worth stating plainly, because it's the rule for the rest of this series:
- Anything the recipe API can express — a content type, a field, a view, a menu link, a config value — goes in a recipe YAML file under
recipes/chattanooga_digital/. - Ad-hoc cleanup the API can't express — deleting an upstream entity, setting the front page, reparenting a menu — goes in
post_install.php. - Brand and markup — colors, fonts, templates — live in the theme source under
themes/byte_theme/, not in the recipe at all.
The nine old imperative PHP "build scripts" this project used to have were deleted precisely because they blurred that line. Hand-rolled UUIDs and version-pinned component hashes rot fast across upstream upgrades. The declarative recipe doesn't.
Branding: three CSS files turn stock Byte into Gig City
Now the fun part. Byte ships as a dark-mode SaaS template — Roboto type, a generic blue/orange palette. We rebrand it without subtheming (Byte's own docs say don't subtheme) by editing the three CSS source files Byte officially supports for customization, all under themes/byte_theme/src/:
theme.css— color tokens, typography stack, radius, shadows.fonts.css— the@font-face/@importfont declarations.main.css— body-level overrides, heading families, the grain texture, text selection.
The colors are the heart of it. The design direction maps eight named "Gig City" tokens — each with a specific Chattanooga referent — onto Byte's semantic slots inside theme.css:
:root {
--cd-fiber-blue: #0A4D8C; /* Walnut Street Bridge blue, EPB fiber */
--cd-grid-teal: #0D7377; /* smart-grid switch / link accent */
--cd-signal-green: #7AB648; /* fiber light, success, growth */
--cd-bridge-copper: #B87333; /* patina copper — CTAs, warm accent */
--cd-river-slate: #2C3E50; /* body text, ink */
--cd-limestone: #F5F1EB; /* page background — warm off-white */
--cd-deep-current: #1A252F; /* river at night, footer */
--background: var(--cd-limestone);
--foreground: var(--cd-river-slate);
--primary: var(--cd-fiber-blue);
--secondary: var(--cd-grid-teal);
}That's the trick: the page never references a raw hex value. The brand tokens (--cd-*) feed Byte's existing semantic slots (--background, --primary, and so on), so every button, link, and surface across the whole theme picks up the Gig City palette at once. The blue is Walnut Street Bridge blue, the green is fiber light, the copper is the patina on the bridge ironwork — every token has a real referent, not a Pantone fashion pick.
Typography is three open-source faces, loaded from Bunny Fonts (a privacy-respecting Google Fonts mirror — no cookies, no logging) with a single import line in fonts.css:
@import url("https://fonts.bunny.net/css?family=fraunces:400,500,600,700,800,900|inter:400,500,600,700,800|jetbrains-mono:400,500,600&display=swap");Inter carries body and UI, JetBrains Mono handles code, and Fraunces — a variable serif with an optical-sizing axis — does the editorial headings. The variable axis is what lets the same font feel sharp at body size and hand-crafted at billboard size, so the hero headline doesn't read as template-default.
Rebuild and refresh
CSS edits aren't live until Tailwind recompiles the bundle (build/main.min.css) and Drupal clears its CSS aggregation cache. Two Makefile targets do exactly that, or one combined target:
make theme-build # recompile build/main.min.css via a one-off node container
make cr # drush cr — clear Drupal's caches
make theme-refresh # both of the above in one goThen hard-refresh the browser (Ctrl+Shift+R) so it drops the old aggregated bundle. The same edits propagate to staging and production for free — there's nothing extra to run. Because the three CSS files live in git and the fix-settings service recompiles Tailwind on every deploy, a git push to main is all it takes for the next deploy to carry the new brand.
What's next
You now know what a recipe is, how the two recipes layer, what post_install.php mops up, and how the brand lives in three CSS files. In Part 3 we go up a level from CSS to components — building the reusable SDC pieces (hero, billboard, cards) that the Canvas pages are assembled from.
← Part 1: Install It in Docker · Part 3: Building Components →
More insights
Want updates from Chattanooga.Digital?
Pre-join the co-op to receive new posts, workshop schedules, and member updates.