Creating custom content entities is a cornerstone of advanced Drupal development. It allows you to define data structures that are perfectly tailored to your business logic. However, once you have defined your entity, you often want to provide default styling or specific templates for different view modes—directly from within your module.
A common frustration for developers is when Drupal recognizes a theme suggestion (like offer--full.html.twig) in the debug output, but refuses to load the template file from the module's templates directory, only picking it up if it is placed in a theme. In this guide, you will learn exactly how to register these suggestions so that your module remains self-contained and portable.
Understanding the Theme Registry Challenge
When you define a custom entity, such as an 'offer', you typically register the base template in your .module file using hook_theme(). This tells Drupal that when it encounters an entity of type 'offer', it should look for a file named offer.html.twig.
Here is the standard initial setup for a custom entity named offer:
/**
* Implements hook_theme().
*/
function offer_theme($existing, $type, $theme, $path) {
return [
'offer' => [
'render element' => 'elements',
],
];
}
/**
* Prepares variables for offer templates.
*/
function template_preprocess_offer(array &$variables) {
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
This works perfectly for the base template located at modules/custom/offer/templates/offer.html.twig. However, the moment you add a view mode (like 'Full content' or 'Teaser') and try to create a specific template for it within your module, you might find that Drupal ignores it. This happens because Drupal's theme registry needs to be explicitly told about these variations if they are to be discovered outside of a theme.
The Solution: Registering Suggestions via Base Hooks
The most reliable way to ensure Drupal looks for specific view mode templates within your module folder is to register the suggestion in hook_theme() using the base hook property. This establishes a relationship between the specific template (e.g., offer__full) and the original entity definition.
To solve the discovery issue, update your hook_theme implementation as follows:
function offer_theme($existing, $type, $theme, $path) {
return [
'offer' => [
'render element' => 'elements',
],
// This is the key: register the specific suggestion
'offer__full' => [
'base hook' => 'offer',
],
];
}
By adding 'offer__full', you are telling the Drupal Theme Registry that this specific pattern is a valid implementation of the offer hook. The 'base hook' => 'offer' part ensures that it inherits the preprocessing and logic defined for the primary entity.
Implementing hook_theme_suggestions_HOOK()
While many developers instinctively reach for hook_theme_suggestions_HOOK_alter(), it is often better to use the non-alter version when you are working within the module that actually defines the entity. Using hook_theme_suggestions_offer() allows you to provide suggestions cleanly without overriding other modules' logic unnecessarily.
Here is how you should implement the suggestion logic to support view modes:
/**
* Implements hook_theme_suggestions_HOOK().
*/
function offer_theme_suggestions_offer(array $variables) {
$suggestions = [];
$offer = $variables['elements']['#offer'];
// Sanitize the view mode to ensure it is a valid suggestion string
$sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');
$suggestions[] = 'offer__' . $sanitized_view_mode;
return $suggestions;
}
Why Sanitization Matters
In the code above, we use strtr($view_mode, '.', '_'). This is a critical best practice. In Drupal, view modes can sometimes contain dots (especially in contributed modules or complex configurations). Since Twig template suggestions use double underscores (__) as delimiters, converting dots to underscores ensures that your file naming convention remains consistent and predictable (e.g., offer--full.html.twig).
Common Mistakes to Avoid
- Missing the Base Hook: As noted, if you provide a suggestion but don't register that specific suggestion in
hook_theme(), Drupal will only look for the file in your active theme. It will ignore your module's template directory. - Incorrect File Naming: Remember that in the PHP code, we use underscores (
offer__full), but the physical Twig file must use hyphens (offer--full.html.twig). - Cache Rebuilds: The Theme Registry is heavily cached. Whenever you add a new suggestion to
hook_theme()or create a new.html.twigfile, you must clear the Drupal cache (drush cr). - Template Location: Ensure your templates are inside a
/templatesfolder within your module. Drupal's default discovery mechanism expects this structure.
Best Practices for Module Reusability
If you are building a module intended for distribution or use across multiple projects, providing default templates is a great way to ensure a "plug-and-play" experience. However, always keep these points in mind:
- Keep Logic in Preprocess: Don't put complex logic in your Twig files. Use
template_preprocess_offerto prepare data. - Provide a Base Template: Always provide a default
offer.html.twig. This acts as a fallback if a specific view mode template isn't found. - Namespace your Suggestions: Ensure your entity name is unique to avoid collisions with other modules or Drupal core entities.
Frequently Asked Questions
Why does my template work in the theme but not in the module?
This is due to how Drupal handles template discovery. Themes have a higher priority and are automatically scanned for any suggested file names. Modules, however, must explicitly register their intent to handle specific theme hooks via hook_theme(). Without the base hook registration, Drupal assumes the suggestion is meant for the theme layer only.
Can I use this for entities with bundles?
Yes! If your entity had bundles (like 'Offer Type'), you would simply add another suggestion to your list: $suggestions[] = 'offer__' . $bundle . '__' . $view_mode;. You would then need to register this pattern in hook_theme() as well if you want to ship bundle-specific templates with your module.
Does this apply to Drupal 10 and Drupal 11?
Absolutely. The theme registry and suggestion system have remained consistent from Drupal 8 through Drupal 11. The logic of using hook_theme with a base hook remains the standard way to provide module-based template suggestions.
Wrapping Up
By properly registering your theme suggestions in hook_theme() and using hook_theme_suggestions_HOOK(), you gain full control over the rendering of your custom content entities. This approach keeps your module organized and ensures that your custom view modes look exactly how you intended, right out of the box, without requiring manual template copying into the site's theme folder.
Next time you see your template suggested in the debug console but not being used, remember: check your base hook registration!