Best Practices — camoa
Opinionated playbook rules for Drupal projects with Radix/Bootstrap sub-themes. Each rule is atomic and citable.
| Rule | Guide | Summary |
|---|---|---|
| Bootstrap before custom CSS | Use Bootstrap Before Custom CSS | Always check if Bootstrap provides a class, mixin, variable, or utility before writing custom SCSS. |
| Never replace Bootstrap maps | Never Replace Bootstrap Maps | Extend Bootstrap maps with map-merge() in a map-overrides partial. Never overwrite Bootstrap's $spacers, $theme-colors, or other maps directly. |
| Font sizing via RFS | Font Sizing — Always Use RFS | Never set font-size: directly on headings or body text. Use Bootstrap's @include font-size() mixin which handles responsive scaling via RFS. |
| Mobile-first breakpoints | Mobile-First Breakpoints | Use Bootstrap's media-breakpoint-up() for desktop overrides. Define mobile as the base, layer up. Pick the project's stacking breakpoint intentionally. |
| No direct child selectors with LB | No Direct Child Selectors with Layout Builder | Layout Builder wraps blocks in variable-depth divs. Never use > in selectors that target Layout Builder content. |
| LB styles stay at wrapper level | LB Styles Must Not Override Component Internals | Layout Builder style plugins should apply to the section or block wrapper level. They must not cascade into component-specific elements. |
| Media view mode → responsive image | Media View Mode → Responsive Image Style | Never use the default media view mode for content display. Create purpose-specific view modes with responsive image formatters. |
| Responsive image sizing per context | Responsive Image Sizing Per Context | Match responsive image styles to actual display size. A card must not serve hero-sized sources. |
| Sizes-based vs breakpoint-based images | Sizes-Based vs Breakpoint-Based | Use the sizes attribute when image width depends on layout. Use breakpoint-specific mappings when art-direction differs per breakpoint. |
| No hardcoded content in templates | No Hardcoded Content in Templates | All user-facing text must be CMS-editable. Section headings come from block labels or view titles — never from Twig. |
| Config over code | Config Over Code | Prefer config-only solutions over custom PHP. When unavoidable, use theme preprocess hooks — not controllers or services. |
| Field descriptions via config | Field Descriptions via Base Field Overrides | To add help text to base fields, use core.base_field_override config — not hook_form_alter(). |
| BEF + AJAX over JS toggles | BEF + AJAX Over JS Toggles | For filtered-view switchers, use Better Exposed Filters with AJAX — not client-side JS that hides pre-rendered content. |
| Block → SDC via block template | Block → SDC via Block Template | When a block needs to render as an SDC, create a block template that includes the SDC. Don't hardcode SDCs in page templates. |
| SDC JS scope | SDC Libraries | SDC JS that needs global scope must go in theme-level library overrides, not in the SDC's component JS file. |
| Window width at click time | Window Width at Click Time | Check window.innerWidth at click time, not at Drupal behavior attach time — once() captures a stale snapshot. |
| Per-environment module exclusion | Per-Environment Module Exclusion | Use $settings['config_exclude_modules'] for modules with environment-specific config (GTM, profiling). |
| Metatag per bundle | Metatag Per Bundle | Configure metatag defaults per content type, not globally — each bundle uses different field tokens. |
| Composer module removal order | Composer Module Removal Order | Never remove a module from composer.json before all environments have uninstalled it via drush config:import. |
| Hiding features for launch | Hiding Features for Launch | When temporarily removing a feature, preserve all code — use templates or #access = FALSE and document the re-enable path. |
| Prefer Drush commands over custom PHP | Prefer Drush Commands Over Custom PHP | Check whether a Drush command exists before reaching for php:eval or php:script. Built-in commands are tested, idempotent, flag-aware, and self-documenting; php:eval bypasses all of that and accumulates as brittle copy-paste debt. |
| Avoid unnecessary custom modules | Avoid Unnecessary Custom Modules | Reuse = CSS + Drupal config (zero new code). Extend = contrib modules, Twig template overrides, preprocess hooks, hook implementations (use the framework's extension points). Create = custom module only when both fail. Contrib is shared infrastructure; your template is yours to patch forever. |