diff --git a/ai/AGENTS.md b/ai/AGENTS.md new file mode 100644 index 0000000..b114812 --- /dev/null +++ b/ai/AGENTS.md @@ -0,0 +1,9 @@ +# AI contribution guide + +These instructions summarize the Drupal 10+ best practices from `README.md` for any AI-assisted changes inside the `ai/` folder. + +- Prioritize the guidance from **2. Site building** and **3. Theming, templates**. Avoid any Drupal 7.x-only rules. +- Use clear machine names and avoid multi-word theme names; keep everything human-readable and concise. +- Keep recommendations aligned with current Drupal core tooling (composer, drush, ddev) and avoid deprecated workflows. +- Ensure scripts and prompts reinforce configuration over content, avoid hardcoded UUIDs, and respect configuration sync. +- Keep the output concise and actionable so it can be applied from CLI tools or code editors. diff --git a/ai/README.md b/ai/README.md new file mode 100644 index 0000000..be648e2 --- /dev/null +++ b/ai/README.md @@ -0,0 +1,15 @@ +# AI Helpers for Drupal Best Practices + +This folder provides AI-friendly outputs derived from `README.md` sections **2. Site building** and **3. Theming, templates** for Drupal 10+ projects. + +- `AGENTS.md` – scope rules for AI-written assets in this folder. +- `claude-code-skills.md` – per-subsection skills for Claude Code. +- `rules.md` – generic rules for any CLI/editor AI. +- `commands.md` – slash commands mapping to the relevant README sections. +- `scripts/*.sh` – drush-based validators for subsections (nodes, blocks, taxonomy, fields, views, text formats, theming). + +Run validators from a bootstrapped Drupal site, e.g. + +```bash +./ai/scripts/validate-nodes.sh +``` diff --git a/ai/claude-code-skills.md b/ai/claude-code-skills.md new file mode 100644 index 0000000..e12078c --- /dev/null +++ b/ai/claude-code-skills.md @@ -0,0 +1,42 @@ +# Claude Code Skills for Drupal Best Practices + +Use these skills to guide Claude Code when assisting Drupal 10+ work. Each skill maps to README sections. + +## 2.1 Nodes +- Ensure content types use singular names and human-friendly machine names without special characters. +- Create bundles only when display or functionality differs; keep revisions only where workflows require them. +- Prefer shared, generic view modes and document each bundle with a description. + +## 2.2 Blocks +- Create custom block types or plugins in code with clean machine names (no region info, no `block_` prefix). +- Treat block fields and view modes like node bundles and avoid hardcoded UUID-driven blocks. + +## 2.3 Taxonomy +- Keep vocabulary names singular; use taxonomy for categorization pages, not simple filtering. +- Consider entity references when authorization, fields, or displays exceed taxonomy needs. + +## 2.4 Other content entities +- Apply node conventions to media, paragraphs, and comments; watch translation handling for paragraphs. +- Use concise, reusable image style names that describe intent rather than dimensions. + +## 2.5 Fields +- Name shared fields generically and specific fields with entity context using `field_[bundle]_[short]`. +- Add descriptions, reuse fields only when invariant, and set meaningful file directories; drop `gif` unless required. + +## 2.6 Views +- Normalize machine names (remove `_1` suffix), titles, tags, and admin descriptions for every display. +- Favor separate Views per display, render entities via view modes, avoid blanket CSS classes and default Ajax. +- Require permission-based access controls and clear "No results" text. + +## 2.7 Forms +- Default to the Webform module for custom forms; reserve core Contact only for simple single-form cases. + +## 2.8 Text formats and editors +- Standardize on a single HTML format using CKEditor; align buttons with allowed tags and limit format switching. +- Keep insecure content out of WYSIWYG and ensure admin formats mirror author access when possible. + +## 3. Theming, templates +- Use one-word theme machine names without `theme` or base-theme prefixes; favor twig and atomic design patterns. +- Add classes via preprocess in `.theme`, cover special templates (404/403/maintenance/login), and keep overrides minimal. +- Prefer Classy as a base; if using contrib themes, decouple machine names. Use SCSS, meaningful breakpoints, and semantic class prefixes (`twig-`, `js-`). +- Avoid styling by path aliases, comment mixins/functions, and split SCSS by responsibility (entities, variables, etc.). diff --git a/ai/commands.md b/ai/commands.md new file mode 100644 index 0000000..1b2a5c4 --- /dev/null +++ b/ai/commands.md @@ -0,0 +1,13 @@ +# AI Commands for Drupal Best Practices + +Use these slash commands in chat-based tools to pull focused guidance from README sections 2 and 3. + +- `/drupal-best-practices-nodes` → Section 2.1 Nodes: singular bundles, minimal revisions, generic view modes, human-readable machine names. +- `/drupal-best-practices-blocks` → Section 2.2 Blocks: code-driven block types/plugins, clean machine names, no UUID-bound blocks. +- `/drupal-best-practices-taxonomy` → Section 2.3 Taxonomy: singular vocabularies, use for categorization pages, prefer lists or references for filtering/authorization needs. +- `/drupal-best-practices-entities` → Section 2.4 Other content entities: reuse node conventions for media/paragraphs/comments; translation awareness. +- `/drupal-best-practices-fields` → Section 2.5 Fields: `field_[bundle]_[short]` naming, descriptions, reuse rules, image directory conventions, avoid `gif`. +- `/drupal-best-practices-views` → Section 2.6 Views: display-per-view, admin metadata, view modes, permission access, no default Ajax, empty-state text. +- `/drupal-best-practices-forms` → Section 2.7 Forms: default to Webform, reserve core Contact for single lightweight forms. +- `/drupal-best-practices-text-formats` → Section 2.8 Text formats and editors: single HTML format, CKEditor, aligned buttons/tags, limited switching. +- `/drupal-best-practices-theming` → Section 3 Theming, templates: one-word theme names, twig-first, preprocess classes, minimal overrides, SCSS with shared breakpoints, semantic class prefixes. diff --git a/ai/rules.md b/ai/rules.md new file mode 100644 index 0000000..75b10cb --- /dev/null +++ b/ai/rules.md @@ -0,0 +1,11 @@ +# AI-Agnostic Rules for Drupal Best Practices + +Apply these Drupal 10+ rules across any CLI/editor tooling. They mirror README sections 2 and 3. + +- **Machine names:** Use single words without prefixes or suffixes tied to regions/themes; avoid collisions and keep them human-readable. +- **Content modeling (2.1–2.5):** Favor fewer bundles, singular labels, documented descriptions, and reusable fields with clear `field_[bundle]_[short]` naming. Treat blocks, taxonomies, media, and paragraphs with the same discipline and avoid hardcoded UUID content. +- **Views (2.6):** One view per display when possible, explicit admin metadata, permission-based access, entity view mode rendering, non-Ajax by default, and clear empty states. +- **Forms (2.7):** Default to Webform; core Contact only for trivial single-form needs. +- **Text formats (2.8):** Standardize on one HTML format using CKEditor; align toolbar buttons with allowed tags and restrict switching formats. +- **Theming (3):** One-word theme machine names without `theme` suffixes or base-theme coupling, atomic/twig-first approach, preprocess for classes, minimal overrides, SCSS with shared breakpoints, semantic class prefixes (`twig-`, `js-`), and avoid styling by path aliases. +- **Tooling:** Prefer composer, drush, and ddev for automation; keep guidance compatible with config synchronization and current Drupal core support. diff --git a/ai/scripts/validate-blocks.sh b/ai/scripts/validate-blocks.sh new file mode 100755 index 0000000..06a838a --- /dev/null +++ b/ai/scripts/validate-blocks.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validate README section 2.2 Blocks rules. + +command -v drush >/dev/null 2>&1 || { echo "drush is required" >&2; exit 1; } + +drush php:eval ' +use Drupal\block_content\Entity\BlockContentType; +$fail = 0; +$storage = \Drupal::entityTypeManager()->getStorage("block_content_type"); +foreach ($storage->loadMultiple() as $type) { + $id = $type->id(); + $desc = trim((string) $type->getDescription()); + if (preg_match("/\s/", $id) || preg_match("/[^a-z0-9_]/", $id) || preg_match("/[A-Z]/", $id)) { + echo "[blocks] Machine name needs cleanup: $id\n"; + $fail = 1; + } + if (strpos($id, 'block_') === 0) { + echo "[blocks] Remove redundant block_ prefix from $id\n"; + $fail = 1; + } + if ($desc === '') { + echo "[blocks] Missing description for $id\n"; + $fail = 1; + } +} +if ($fail) { + exit(1); +} +' diff --git a/ai/scripts/validate-fields.sh b/ai/scripts/validate-fields.sh new file mode 100755 index 0000000..a30d0a9 --- /dev/null +++ b/ai/scripts/validate-fields.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validate README section 2.5 Fields rules. + +command -v drush >/dev/null 2>&1 || { echo "drush is required" >&2; exit 1; } + +drush php:eval ' +use Drupal\field\Entity\FieldStorageConfig; +$fail = 0; +$storage = \Drupal::entityTypeManager()->getStorage("field_storage_config"); +foreach ($storage->loadMultiple() as $field) { + $id = $field->getName(); + $desc = trim((string) $field->getDescription()); + if (strpos($id, "field_") !== 0) { + echo "[fields] Field name should start with field_: $id\n"; + $fail = 1; + } + if (preg_match("/\s/", $id) || preg_match("/[^a-z0-9_]/", $id) || preg_match("/[A-Z]/", $id)) { + echo "[fields] Machine name needs cleanup: $id\n"; + $fail = 1; + } + if ($desc === '') { + echo "[fields] Missing description for $id\n"; + $fail = 1; + } +} +if ($fail) { + exit(1); +} +' diff --git a/ai/scripts/validate-nodes.sh b/ai/scripts/validate-nodes.sh new file mode 100755 index 0000000..206acae --- /dev/null +++ b/ai/scripts/validate-nodes.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validate README section 2.1 Nodes rules against a Drupal 10+ site. +# Requires: drush, Drupal bootstrap. + +command -v drush >/dev/null 2>&1 || { echo "drush is required" >&2; exit 1; } + +drush php:eval ' +use Drupal\node\Entity\NodeType; +$fail = 0; +$storage = \Drupal::entityTypeManager()->getStorage("node_type"); +foreach ($storage->loadMultiple() as $type) { + $id = $type->id(); + $label = $type->label(); + $desc = trim((string) $type->getDescription()); + if (preg_match("/\s/", $id) || preg_match("/[^a-z0-9_]/", $id) || preg_match("/[A-Z]/", $id)) { + echo "[nodes] Machine name needs cleanup: $id\n"; + $fail = 1; + } + if (preg_match("/s$/", $label)) { + echo "[nodes] Label looks plural; prefer singular: $label ($id)\n"; + $fail = 1; + } + if ($desc === '') { + echo "[nodes] Missing description for $id\n"; + $fail = 1; + } + if ($type->isNewRevision()) { + echo "[nodes] Revisions enabled by default; confirm workflow need: $id\n"; + } +} +if ($fail) { + exit(1); +} +' diff --git a/ai/scripts/validate-taxonomy.sh b/ai/scripts/validate-taxonomy.sh new file mode 100755 index 0000000..2296c98 --- /dev/null +++ b/ai/scripts/validate-taxonomy.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validate README section 2.3 Taxonomy rules. + +command -v drush >/dev/null 2>&1 || { echo "drush is required" >&2; exit 1; } + +drush php:eval ' +use Drupal\taxonomy\Entity\Vocabulary; +$fail = 0; +$storage = \Drupal::entityTypeManager()->getStorage("taxonomy_vocabulary"); +foreach ($storage->loadMultiple() as $vocab) { + $id = $vocab->id(); + $label = $vocab->label(); + if (preg_match("/\s/", $id) || preg_match("/[^a-z0-9_]/", $id) || preg_match("/[A-Z]/", $id)) { + echo "[taxonomy] Machine name needs cleanup: $id\n"; + $fail = 1; + } + if (preg_match("/s$/", $label)) { + echo "[taxonomy] Label looks plural; prefer singular: $label ($id)\n"; + $fail = 1; + } +} +if ($fail) { + exit(1); +} +' diff --git a/ai/scripts/validate-text-formats.sh b/ai/scripts/validate-text-formats.sh new file mode 100755 index 0000000..ecf21fb --- /dev/null +++ b/ai/scripts/validate-text-formats.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validate README section 2.8 Text formats and editors rules. + +command -v drush >/dev/null 2>&1 || { echo "drush is required" >&2; exit 1; } + +drush php:eval ' +use Drupal\filter\Entity\FilterFormat; +$formats = FilterFormat::loadMultiple(); +if (count($formats) > 2) { + echo "[text formats] More than one HTML-capable format detected; aim for a single HTML format.\n"; +} +foreach ($formats as $format) { + $name = $format->id(); + $filters = $format->filters(); + if (strpos($name, "html") !== false && empty($format->getRoles())) { + echo "[text formats] Ensure access to $name is scoped to roles, not open to all.\n"; + } + if ($filters->has('filter_html')) { + $allowed = $filters->get('filter_html')->getConfiguration()['allowed_html'] ?? ''; + if ($allowed === '') { + echo "[text formats] Allowed HTML empty for $name; align toolbar buttons with allowed tags.\n"; + } + } +} +' diff --git a/ai/scripts/validate-theming.sh b/ai/scripts/validate-theming.sh new file mode 100755 index 0000000..5ff769e --- /dev/null +++ b/ai/scripts/validate-theming.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validate README section 3 Theming rules. + +command -v drush >/dev/null 2>&1 || { echo "drush is required" >&2; exit 1; } + +drush php:eval ' +use Drupal\system\Entity\Theme; +$fail = 0; +$themes = Theme::loadMultiple(); +foreach ($themes as $theme) { + $id = $theme->id(); + if (preg_match("/\s/", $id) || preg_match("/[^a-z0-9_]/", $id) || preg_match("/[A-Z]/", $id)) { + echo "[theming] Theme machine name should be one word lowercase: $id\n"; + $fail = 1; + } + if (strpos($id, 'theme') !== false) { + echo "[theming] Remove redundant 'theme' substring from $id\n"; + $fail = 1; + } + $base = $theme->getBaseTheme(); + if ($base && strpos($id, $base) !== false) { + echo "[theming] Subtheme machine name should not include base theme ($base): $id\n"; + $fail = 1; + } +} +if ($fail) { + exit(1); +} +' diff --git a/ai/scripts/validate-views.sh b/ai/scripts/validate-views.sh new file mode 100755 index 0000000..098ff8e --- /dev/null +++ b/ai/scripts/validate-views.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Validate README section 2.6 Views rules. + +command -v drush >/dev/null 2>&1 || { echo "drush is required" >&2; exit 1; } + +drush php:eval ' +use Drupal\views\Views; +$fail = 0; +$storage = \Drupal::entityTypeManager()->getStorage("view"); +foreach ($storage->loadMultiple() as $view) { + $id = $view->id(); + $human = $view->label(); + if (preg_match("/\s/", $id) || preg_match("/[^a-z0-9_]/", $id) || preg_match("/[A-Z]/", $id)) { + echo "[views] Machine name needs cleanup: $id\n"; + $fail = 1; + } + if ($human === $id || $human === '') { + echo "[views] Missing descriptive label for $id\n"; + $fail = 1; + } + foreach ($view->get('display') as $display_id => $display) { + if (substr($display_id, -2) === '_1') { + echo "[views] Rename default _1 display id in $id ($display_id)\n"; + $fail = 1; + } + if (empty($display['display_title'])) { + echo "[views] Missing display title for $id::$display_id\n"; + $fail = 1; + } + } +} +if ($fail) { + exit(1); +} +'