From drupal-workflow
Guides field type selection, entity CRUD operations, view modes, and content modeling for Drupal 10/11. Use when designing content types, selecting fields, or using Entity API.
npx claudepluginhub gkastanis/drupal-workflow --plugin drupal-workflowThis skill uses the workspace's default tool permissions.
| Content Need | Field Type | Widget | Notes |
Guides creating Content Elements, Record Types, Page Types, and File Types using TYPO3 Content Blocks extension—the single source of truth for YAML-based content modeling in TYPO3 14.x.
Craft CMS 5 content modeling — sections, entry types, fields, Matrix, relations, project config, and content architecture strategy. Covers everything editors and developers need to structure content in Craft: choosing section types, designing entry types and field layouts, selecting field types for specific needs, configuring Matrix and nested entries, setting up relations and eager loading, and planning multi-site propagation. Triggers on: section types (single, channel, structure), entry types, field types, field layout design, field type selection, Matrix configuration, nested entries, relatedTo, eager loading, .with(), .eagerly(), categories, tags, globals, global sets, preloadSingles, propagation, multi-site content, URI format, project config, YAML, content architecture, content strategy, taxonomy, asset volumes, filesystems, image transforms, user groups, content permissions, entries-as-taxonomy, entrify, entrification, CKEditor vs Matrix, CMS editions, site propagation, multi-language, language groups, localization, translation method, field translation, content migration, reserved handles, field instances. Always use when planning content architecture, creating sections/fields, configuring Matrix, setting up relations, choosing field types, designing field layouts, making content modeling decisions, or planning multi-site content propagation. Do NOT trigger for PHP plugin/module development, custom field type code, front-end Twig templates, or buildchain configuration.
Brainstorms Drupal feature requirements, entity designs, service architecture, and hook/event strategies before coding new modules or changes.
Share bugs, ideas, or general feedback.
| Content Need | Field Type | Widget | Notes |
|---|---|---|---|
| Event date/time | datetime | datetime_default | Single datetime |
| Date range | daterange | daterange_default | Start/end dates |
| Location (simple) | string | string_textfield | Text-based |
| Location (structured) | address (contrib) | address_default | Full address |
| Short text | string | string_textfield | Max 255 chars |
| Long text | text_long | text_textarea | Unlimited plain text |
| Rich content | text_with_summary | text_textarea_with_summary | Formatted with summary |
email | email_default | Validated email | |
| Phone | telephone | telephone_default | Phone number |
| Website | link | link_default | URL with title |
| Document | file | file_generic | Any file type |
| Image | image | image_image | With alt text |
| Content reference | entity_reference (node) | entity_reference_autocomplete | Links to nodes |
| Term reference | entity_reference (taxonomy_term) | entity_reference_autocomplete | Categories |
| Yes/No flag | boolean | boolean_checkbox | True/false |
| Whole number | integer | number | Integer values |
| Decimal number | decimal | number | Precise decimals (financial) |
| Float number | float | number | Approximate decimals |
| Flexible components | entity_reference_revisions (paragraphs) | paragraphs | Contrib: requires entity_reference_revisions storage |
Always choose the right entity type for your data. Use ContentEntityBase for fieldable content, ConfigEntityBase for exportable config. Never create a custom entity when a node type would suffice.
ContentEntityType): Published content (articles, pages, events).ContentEntityBase): Non-published data (rarely needed — prefer nodes).Always implement baseFieldDefinitions() to declare your entity's base fields. Always add an AccessControlHandler class for custom entities.
// In your entity class:
public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
$fields = parent::baseFieldDefinitions($entity_type);
// Add custom base fields here.
return $fields;
}
Use dependency injection — inject EntityTypeManagerInterface via constructor, never use \Drupal:: static calls in classes.
// In a service or controller with injected $entityTypeManager:
// Load single entity.
$node = $this->entityTypeManager->getStorage('node')->load($nid);
// Load multiple entities (avoids N+1).
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
// Create entity.
$node = $this->entityTypeManager->getStorage('node')->create([
'type' => 'article',
'title' => 'My Article',
'field_tags' => [['target_id' => $tid]],
]);
$node->save();
// Query entities (inject EntityTypeManagerInterface, not entityQuery directly).
$nids = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('type', 'article')
->condition('status', 1)
->accessCheck(TRUE)
->range(0, 10)
->execute();
Custom entities need an AccessControlHandler. Without one, all access defaults to denied.
declare(strict_types=1);
namespace Drupal\my_module\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
final class MyEntityAccessControlHandler extends EntityAccessControlHandler {
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResult {
return match ($operation) {
'view' => AccessResult::allowedIfHasPermission($account, 'view my_entity'),
'update' => AccessResult::allowedIfHasPermission($account, 'edit my_entity'),
'delete' => AccessResult::allowedIfHasPermission($account, 'delete my_entity'),
default => AccessResult::neutral(),
};
}
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL): AccessResult {
return AccessResult::allowedIfHasPermission($account, 'create my_entity');
}
}
Register in the entity annotation: handlers = {"access" = "Drupal\my_module\Access\MyEntityAccessControlHandler"}
Share field storage across content types when the same concept applies:
# Good: shared storage
field_location (on event, venue, office)
field_contact_email (on person, organization, department)
# Bad: duplicated storage
field_event_location, field_venue_location, field_office_location
full: Full content display.teaser: Summary/listing display.search_result: Search listing.card, sidebar).