Drupalwoo ☉

Rendering dynamic content with Drupal's Render API

Drupal's Render API sits at the heart of how a page is assembled, turning structured PHP arrays into HTML through a controlled pipeline. For developers working on sites that need to flex with user input, route context, or time-based logic, the API is the cleanest path between data and markup. Agencies in Sydney, Melbourne, and Brisbane rely on it to build government portals, university intranets, and retail platforms that share a common look while serving very different audiences.

The system runs on render arrays, which are nested associative arrays that describe what should appear on a page and how it should behave. By passing data through theme hooks, preprocess functions, and cache metadata, teams can compose interfaces that respond to permissions, language, or session state without scattering HTML across controllers. The same machinery that powers core content listings also powers the smallest custom block, which keeps the learning curve worth the climb.

For Australian site builders juggling AEST-scheduled publishing, multilingual campaigns for the Asia-Pacific market, or integrations with services built on the Australian Government Design System, render arrays offer a stable foundation. The sections below walk through the structure of these arrays, theme hook registration, preprocess logic, caching decisions, and hook-based alterations, with code samples that reflect current Drupal 10 practices.

How render arrays are structured

A render array is a PHP associative array whose keys start with a hash, indicating render-related properties. The most familiar property is #type, which references an element plugin such as link, html_tag, or container and tells the renderer which class to instantiate. When a property like #theme is used instead, the array is handed to the theme system, which selects a Twig template or theme function.

Other properties control content directly. #markup holds a plain string for the simplest cases, while #plain_text sanitises user-supplied text through Html::escape(). #prefix and #suffix wrap output with static fragments, and #attributes accept attribute arrays for the surrounding HTML element. Nesting is what gives the structure its real power: a top-level array can contain child arrays, each rendered recursively before the parent's wrapper is applied.

The renderer also reads metadata from the same array. #cache declares contexts, tags, and a max-age, while #attached loads libraries, settings, or HTTP headers. Mixing data, presentation, and metadata in one structure means the whole payload can be cached, altered, or replaced at any point in the pipeline, which matters for dynamic content that must stay consistent across regions of a page.

Property Type of value Effect on rendering
#type Element plugin name Instantiates a render element such as link
#theme Hook name Renders through a Twig template or theme function
#markup String Outputs sanitised inline HTML
#attached Associative array Adds CSS, JS, settings, or response metadata
#cache Associative array Configures contexts, tags, and max-age
#attributes Associative array Sets HTML attributes on the rendered wrapper

Building render arrays inside custom modules

A controller that returns a render array is the most common entry point in modern Drupal. Instead of calling the renderer directly, the controller returns the array and the HTTP kernel decides when to render it during the response phase. This separation lets modules contribute markup through hooks without each piece knowing about the others, which is why large publishers such as the Australian Broadcasting Corporation can maintain hundreds of contributors without conflict.

public function content() {
  $build['intro'] = [
    '#type' => 'html_tag',
    '#tag' => 'p',
    '#value' => $this->t('Welcome to the Sydney office portal.'),
  ];
  $build['list'] = [
    '#theme' => 'item_list',
    '#items' => $this->loadNotices(),
    '#cache' => [
      'tags' => ['node_list:announcement'],
      'contexts' => ['user.permissions'],
    ],
  ];
  return $build;
}

The same approach works inside a BlockBase subclass, where the build() method returns an array that the block plugin manager will hand to the renderer. Plugins that need configuration can pull values from $this->configuration and pass them into the array, keeping presentation concerns in templates and behaviour concerns in the plugin class. When render arrays are produced by services, returning them rather than printing HTML keeps the code testable, which matters for teams working under the accessibility and security expectations that apply to Australian government sites.

Using #theme and template files for custom output

When a render array needs more than a one-liner, #theme references a hook registered in hook_theme(). The hook declares the hook name, the variables it expects, and an optional path to the Twig template. Drupal then discovers the template through the theme registry, applies active theme overrides, and falls back gracefully if a file is missing.

function mymodule_theme($existing, $type, $theme, $path) {
  return [
    'team_member' => [
      'variables' => [
        'name' => NULL,
        'role' => NULL,
        'photo' => NULL,
      ],
      'template' => 'team-member',
    ],
  ];
}

Twig templates live under templates/ and are named with hyphens instead of underscores. They receive every variable declared in hook_theme() plus a handful of defaults such as attributes. Theme suggestions, registered through hook_theme_suggestions_HOOK_alter(), let a module propose alternative templates based on node type, view mode, or custom logic, which is how a Brisbane-based retailer can ship distinct layouts for product cards without forking the parent theme. For developers maintaining sites hosted across multiple Australian data centres, theme suggestions also offer a clean way to ship region-specific templates, with the same preprocess logic serving NSW, Victorian, and Queensland campaigns.

Preprocessing variables before they reach the template

Preprocess functions are the bridge between raw data and template-friendly variables. Implemented as mymodule_preprocess_team_member(&$variables), they run before the Twig template parses, giving the module a chance to transform input, set defaults, or attach libraries. Because the same array reaches every active theme, preprocess logic stays portable across base themes and client themes.

function mymodule_preprocess_team_member(&$variables) {
  $variables['initials'] = substr($variables['name'], 0, 1);
  $variables['#attached']['library'][] = 'mymodule/team-card';
}

Preprocess functions also read cache metadata from the render array, which means a transformation can be cached alongside the markup. A function that pulls a postcode-aware delivery estimate, for example, can mark the array with a url.query_args:postcode context so the cache varies when the URL changes, keeping responses fresh without invalidating the entire page. The order of preprocess calls matters: module preprocess runs before theme preprocess, which runs before the template engine, mirroring the precedence model used in Australian accessibility audits where core standards must be respected before client-specific tweaks.

Caching render output for performance and freshness

Caching is where dynamic output either becomes a liability or a strength. Drupal's render cache stores the result of rendering an array, keyed by its #cache metadata, and serves the same response on subsequent requests as long as the metadata matches. The metadata itself is split into contexts, which vary by user, role, language, or URL; tags, which act as invalidation points tied to entities; and max-age, which sets a hard expiry in seconds.

A well-marked render array on a local-government site in Perth, for instance, can vary by user.role for staff-only links while still sharing a single cache bin across public-facing pages. The same array might also carry the tag node:42 so the moment a councillor's profile is updated, every page showing their photo regenerates automatically without a manual cache clear.

When a render array depends on external data such as a weather feed for a Melbourne event, a short max-age of sixty or three hundred seconds keeps the response timely without overloading the upstream API. The render cache then becomes a throttling layer, smoothing traffic during peak AEST business hours when most Australian users arrive at work and refresh the page simultaneously.

Altering rendered output with hooks and services

Once a render array exists, multiple hooks can modify it before it reaches the browser. hook_page_attachments_alter() adds assets, hook_block_view_alter() rewrites a block, and hook_entity_view_alter() adjusts a content entity's renderable array after it has been built. Each hook receives the array by reference, so changes propagate to the final response.

function mymodule_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
  if ($entity->bundle() === 'event' && $build['#view_mode'] === 'card') {
    $build['#attributes']['class'][] = 'card--australian-summer';
  }
}

For deeper changes, hook_element_info_alter() and hook_theme_registry_alter() reshape the metadata that other modules depend on, which is useful for retrofitting a feature onto a contributed module. A common pattern in Australian Drupal agencies is to alter the node element info so that an internal audit field appears in render arrays for every content type without each one being edited manually.

Altering always carries a risk: a hook that runs late cannot affect cached output stored from an earlier request. The safe approach is to make every dependency explicit in #cache and then keep alterations focused on metadata, classes, and small structural tweaks. Anything more substantial usually belongs in a preprocess function, where caching is easier to reason about.

Patterns that keep render arrays maintainable

Common pitfalls when working with the API

The Render API rewards a small amount of upfront structure with very predictable output. Once render arrays, theme hooks, preprocess functions, and cache metadata are part of the muscle memory, dynamic content on Australian Drupal sites becomes easier to extend, easier to cache, and far easier to hand over to the next developer on the team.