Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ai/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions ai/README.md
Original file line number Diff line number Diff line change
@@ -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
```
42 changes: 42 additions & 0 deletions ai/claude-code-skills.md
Original file line number Diff line number Diff line change
@@ -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.).
13 changes: 13 additions & 0 deletions ai/commands.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions ai/rules.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 31 additions & 0 deletions ai/scripts/validate-blocks.sh
Original file line number Diff line number Diff line change
@@ -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);
}
'
31 changes: 31 additions & 0 deletions ai/scripts/validate-fields.sh
Original file line number Diff line number Diff line change
@@ -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);
}
'
36 changes: 36 additions & 0 deletions ai/scripts/validate-nodes.sh
Original file line number Diff line number Diff line change
@@ -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);
}
'
27 changes: 27 additions & 0 deletions ai/scripts/validate-taxonomy.sh
Original file line number Diff line number Diff line change
@@ -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);
}
'
27 changes: 27 additions & 0 deletions ai/scripts/validate-text-formats.sh
Original file line number Diff line number Diff line change
@@ -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";
}
}
}
'
31 changes: 31 additions & 0 deletions ai/scripts/validate-theming.sh
Original file line number Diff line number Diff line change
@@ -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) {
Comment on lines +8 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Theming validator uses non-existent Theme entity

The theming script imports Drupal\system\Entity\Theme and calls Theme::loadMultiple(), but Drupal themes are extensions exposed through the theme handler, not config entities. On Drupal 10 this use statement causes drush php:eval to fatally error (Class "Drupal\system\Entity\Theme" not found), so none of the validation logic can run. Please switch to the theme handler (e.g., \Drupal::service('theme_handler')->listInfo()) or another supported API so the script can execute.

Useful? React with 👍 / 👎.

$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);
}
'
37 changes: 37 additions & 0 deletions ai/scripts/validate-views.sh
Original file line number Diff line number Diff line change
@@ -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);
}
'