Create a Drupal preprocess function for node variables
Drupal preprocess functions let a theme change the values available to a template before Twig renders a node. They are useful for adding reading time, labels, classes, or other presentation data without rewriting the node in the database. A correctly named hook can target every node, while conditions inside it keep the behaviour specific to a content type or view mode.
This walkthrough uses Drupal 9, 10, or 11 with a custom theme and Twig. It assumes that field data already exists and that editors understand content types. For Australian editorial teams in Melbourne and Sydney, the example uses a 200-word reading estimate and a local-news label; the same pattern works for government, education, or community sites.
How preprocess hooks shape node rendering
Drupal builds a render array for an entity, runs preprocess functions, and then passes the resulting variables to the relevant Twig template. For nodes, the main inputs include the node object, title, content, submitted information, links, and the current view mode. The preprocess hook receives one argument by reference, so changing $variables changes what the template receives without changing the stored node.
The node entry is the loaded node entity, not a duplicate that should be saved. Read fields from it with the Drupal entity API, such as $node->get('body'), and use $node->bundle() to obtain the content type machine name. Treating the entity as read-only avoids accidental database writes and makes the hook safe to run during anonymous page requests.
Scope is the next decision. A single hook_preprocess_node() implementation can inspect every node bundle, and $variables['view_mode'] can separate teaser from full rendering. Checking the bundle directly is often clearer for a small site, but a larger Drupal operation in Sydney or Melbourne may need shared helper functions, configuration, or separate template suggestions to avoid repeating conditions.
Build the function in your custom theme
Put presentation-specific logic in the active custom theme. Create or edit its .theme file, commonly au_news.theme, and name the function <theme_machine_name>_preprocess_node(). The ampersand before $variables is required because the function must modify the array in place. A module can implement a preprocess hook too, but theme-level logic keeps the markup concern beside the templates and makes the change easier to audit.
The following implementation targets the news_article content type, calculates an approximate reading time from the raw body value, adds an Australian label, and suppresses the submitted line. The body field may be absent on some node bundles, so the code handles a missing field item. Raw text is appropriate for a rough count, although embedded media and markup are not included.
/**
* Implements hook_preprocess_node().
*/
function au_news_preprocess_node(&$variables) {
$node = $variables['node'];
if (!$node instanceof \Drupal\node\NodeInterface) {
return;
}
if ($node->bundle() !== 'news_article') {
return;
}
$body_item = $node->get('body')->first();
$body_text = $body_item ? strip_tags((string) $body_item->value) : '';
$word_count = str_word_count($body_text);
$variables['reading_minutes'] = $word_count
? max(1, (int) ceil($word_count / 200))
: 0;
$variables['local_label'] = 'Australian update';
$variables['display_submitted'] = FALSE;
}
Keep content relationships out of the hook. If a publisher wants a story to point to a device repair guide, store that reference in a field or prepare it as node content; the preprocess function should only add presentation variables. This separation also means that changing the hook cannot accidentally add the same external link to every node.
After saving the file, rebuild the Drupal cache from the site administration area or with drush cr. If the function does not appear, check the custom theme's machine name, confirm that the theme is active, and verify that the file is named after it. Drupal will not discover a function with a mismatched prefix.
Choose the right extension point
A preprocess hook is the right tool when the template needs an extra flag or display value. It is not the right place for access decisions, workflow transitions, or changes that must be persisted. Drupal's entity API and form APIs should own those behaviours, while Twig should own final markup.
If the render array itself needs a structural change, use hook_node_view_alter() in a module. That hook receives the node build and can affect more consumers of the entity. A theme preprocess function has a narrower purpose: it adjusts values immediately before the node template. Views rows, search results, feeds, and API responses may use separate rendering paths and will not necessarily receive the same variables.
| Requirement | Node preprocess hook | Node view alter hook | Twig template |
|---|---|---|---|
| Working shape | Template variables | Entity render array | Rendered values and markup |
| Usual location | Custom theme .theme file |
Custom module | Theme template |
| Strongest use | Derived labels, flags, and classes | Structural render changes | Semantic presentation |
| Scope | Node template rendering | Entity build consumers | One specific template |
| Main caution | Runs during template preparation | Can affect more outputs | Should avoid business logic |
The practical rule is to start with the least invasive hook that can meet the requirement. Keep a node's data in fields, derive temporary presentation values in preprocess, alter a build only when its structure must change, and leave semantic HTML to Twig. This also makes code review clearer for teams supporting Drupal sites across Australian time zones.
Use the new variables in Twig
A variable becomes useful when the template consumes it. Copy the active theme's node.html.twig into the custom theme before changing it, then retain its existing heading, links, media, and accessibility markup. For a dedicated content-type template, Drupal can use a suggestion such as node--news_article.html.twig.
<article{{ attributes.addClass('local-news-node') }}>
{% if local_label %}
<p class="node__local-label">{{ local_label }}</p>
{% endif %}
{{ title }}
{% if reading_minutes %}
<p class="node__reading-time">
{{ reading_minutes }} min read
</p>
{% endif %}
{{ content }}
{% if display_submitted %}
<p class="node__submitted">{{ submitted }}</p>
{% endif %}
</article>
The reading_minutes and local_label values are ordinary template variables, so Twig escapes them by default. Never use the raw filter on editor-supplied values unless a specific format requires trusted HTML. For publication dates, Australian sites commonly use day-first values such as 07/03/2026 for 7 March; the date formatter can apply that pattern while the site's timezone handles AEST and AEDT daylight-saving changes.
Test, cache and document the behaviour
Test the hook in full and teaser view modes, then check a node with no body, a short body, and a long body. View the result as an anonymous visitor and as an editor, because labels or submitted information can differ by access level. Clear render caches after changing the function, and do not expect an ordinary View listing to inherit the variables: Views normally uses its own row templates and preprocess process.
Avoid slow work in the hook. Do not make an external API request, run an expensive search, or query a large content collection once per node render. Cached pages may hide a performance problem until traffic rises, so inspect query logs in a staging environment. Use a Unicode-aware word counter when the site contains languages for which str_word_count() is not sufficiently accurate.
Finally, confirm that the new output exposes no private information. A Canberra public-sector publisher may need to check the Australian Privacy Act and the Australian Privacy Principles as well as Drupal access controls. The published privacy policy should describe any personal information shown by the site, and template output should never bypass the access checks already applied to the node.