Drupal: Views
|
This content was generated with the assistance of AI and should be verified against the official Drupal documentation before being relied on in production. This sub-section’s Drupal bibliography lists the reference material consulted while preparing these pages. |
Views is the part of Drupal that turns "show me a list of these things" into configuration instead of code. A view is a stored query plus the instructions for presenting its results: which table to read, which rows to keep, in what order, how many per page, which columns or rendered entities to output, and whether the output becomes a page at a URL, a block, an RSS feed, a JSON endpoint or an embedded fragment. Almost every list on a Drupal site is a view: the administrative content screen, the front page, the "recent comments" block, a search results page, a taxonomy term page and the JSON that a headless front end consumes.
This page was written against Drupal 11.4.x, with Drush 13 or 14 and PHP 8.3 or later, and the facts were checked
against the official documentation on 2026-10-30. The running example is the Meridian Museum: the view
upcoming_events (the next events, with an exposed filter), the view exhibitions_by_gallery (exhibitions in one room,
driven by a contextual filter) and the view objects_on_display (items of the custom collection_object entity of the
module meridian_collection, served as a page, a block and a JSON endpoint for the gallery kiosk). Drupal 12.0.0 is
scheduled for the week of 2026-12-07 and the end of life of Drupal 10 for 2026-12-09, so both are described as upcoming.
What Views is and who uses it
The core Views module (views) is a general query and display engine: it builds a query for a data source, runs it, and
formats the result. The core Views UI module (views_ui) is the administrative interface that creates and edits views at
admin/structure/views. Four roles meet Views:
-
site builders create and tune views in the Views UI, which is where most of this page lives;
-
editors and visitors use the results, and the exposed filters and pagers a view offers;
-
administrators decide who may use the Views UI, who may see each view and how it is cached;
-
developers expose their own tables and entity types to Views, add plugins, alter queries and embed views in code.
The User Guide describes the same split: Views displays a view, and Views UI creates and edits it. The user guide’s chapter on listings walks through a content list, a duplicated view and a block display, and the developer overview on api.drupal.org lists the four ways a developer works with Views: providing plugins, providing data, implementing hooks and theming. Views UI is a development and site-building tool, so on a production site it is normally enabled only in local and staging environments, and views travel to production as exported configuration (see Exporting a view to configuration).
How a view is built, in order
A view is evaluated as a pipeline. Each stage narrows or reshapes the previous one, and the last stages decide what is cached.
Everything in the diagram is a handler or plugin configured on the view:
-
the base table is chosen once, when the view is created ("Show: Content" in the wizard) and cannot be changed afterwards;
-
relationships add joined tables, such as the author’s user account or the taxonomy term a field references;
-
filters and contextual filters become
WHEREconditions, the first with fixed or visitor-chosen values and the second with values taken from the URL or another context; -
sort criteria become
ORDER BY, the pager becomesLIMITandOFFSET; -
the display and its format and row plugins decide the output, and the cache plugin decides how long the query results and the rendered output may be reused.
The parts of a view in the Views UI
The User Guide lists seven parts of a view in the administrative interface, and they map directly to the screen:
| Part | What it controls |
|---|---|
Display |
The kind of output: Page (content at a URL), Block, Feed, Attachment and others. A view can have several displays. |
Format |
How rows are arranged: unformatted list, HTML list, grid, table, and so on. Some formats output rendered entities or fields, while a table requires fields. |
Fields |
The columns to output when the row style is "Fields". |
Filter criteria |
Which rows are included (publication status, content type, field values). Filters can be exposed to visitors. |
Sort criteria |
The order of the results, by any available field. |
Contextual filters |
Filters whose values come from the context of the display, such as the URL, the current date or time, or another detectable value. |
Relationships |
Connections from the base content to other entities, such as the author’s user account, so their fields become available. |
Beyond these, the Advanced column holds the header and footer, the "No results behavior", the pager, exposed form
settings, query settings, caching and access. A view always has a hidden-by-default Default (also called Master)
display that holds the settings shared by the others. The Views UI hides it unless "Always show the default display" is
enabled at admin/structure/views/settings. Each setting in the UI carries a For selector: "All displays" writes to
the Default display and is inherited, while "This page (override)" or "This block (override)" stores the value on that
one display only. The User Guide’s block-display walk-through uses the override choice for every step so that the
existing page display stays unchanged. Forgetting this selector is the most common reason that a block unexpectedly
changes the page, or the reverse.
Creating a view with the wizard
Open Structure > Views > Add view (admin/structure/views/add). The wizard asks for:
-
View name (the label, from which the machine name such as
upcoming_eventsis derived); -
Show: the base table, for example Content, Taxonomy terms, Users, Comments, Files, or a custom entity type;
-
of type and sorted by for content;
-
whether to create a page (title, path, display format, items to display, pager, menu link) and a block.
The User Guide’s example creates a Vendors page with the display format Table, ten items and a menu link in the main navigation, then uses "Save and edit" to add the fields in a second step. The wizard is only a shortcut for the settings that the full editor exposes, so everything below applies equally to a view created by the wizard and to one built from scratch.
The base table is the one decision the wizard cannot reverse. The Views documentation states that the base table is chosen when the view is created, from options such as Comments, Content and Taxonomy terms, and cannot be changed later. When the wrong one was chosen, duplicate the view only to copy its display settings and rebuild it on the correct base.
Base tables and what "Show" really means
A base table is a table that Views can start a query from. Core describes them through hook_views_data() (or, for
entity types, through the entity’s views data handler), and every base table lists a primary key (base_field), a human
title and a description. Typical base tables:
| "Show" in the wizard | Table in the query | Notes |
|---|---|---|
Content |
|
One row per node translation. A multilingual site can show each translation as its own row, so add a "Content: Translation language" filter or the rendering-language setting where that matters. |
Taxonomy terms |
|
Used for glossary pages and term listings. See Taxonomy. |
Users |
|
Directories of members or staff. |
Media |
|
Galleries of images and documents. See Media and the Media Library. |
Files |
|
File listings, with "File usage" as a relationship. |
Custom entity ( |
|
Available only when the entity type declares a views data handler. |
A view therefore always reads one kind of thing. To show data from a second kind, such as the gallery name next to an exhibition, a view uses a relationship or a rendered-entity row, not a second base table.
Exposing a custom entity type to Views
Content entity types get their Views integration from a views data handler. The api.drupal.org overview recommends
implementing \Drupal\views\EntityViewsDataInterface and referencing it in the entity type’s views_data handler, and
notes that extending \Drupal\views\EntityViewsData generates most of the integration automatically. For the Meridian
Museum’s collection_object entity, the handler is a single line in the entity type attribute:
<?php
declare(strict_types=1);
namespace Drupal\meridian_collection\Entity;
use Drupal\Core\Entity\Attribute\ContentEntityType;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\EntityViewsData;
#[ContentEntityType(
id: 'collection_object',
label: new TranslatableMarkup('Collection object'),
label_collection: new TranslatableMarkup('Collection objects'),
entity_keys: [
'id' => 'id',
'label' => 'title',
'uuid' => 'uuid',
'published' => 'status',
],
handlers: [
// Generates the Views data for the base table and every base field.
'views_data' => EntityViewsData::class,
],
base_table: 'collection_object',
data_table: 'collection_object_field_data',
)]
class CollectionObject extends ContentEntityBase {
// Base field definitions (accession_number, period, material, dimensions,
// on_display) are declared in baseFieldDefinitions().
}
Source: Drupal API: Views overview
Base fields declared with setDisplayConfigurable() and a proper field type become fields, filters and sorts
automatically, with the right handler for the type (a boolean field gets a boolean filter, a string a string filter).
The full entity type is built in Custom entity types. Configurable (bundle)
fields added through the field UI are exposed the same way, each as its own <entity>__<field> table.
For data that is not an entity, hook_views_data() returns an array whose outer keys are Views table names, usually
matching the hook_schema() table names, each describing the table, its joins and its fields. The api.drupal.org
documentation lists hook_views_data() and hook_views_data_alter() as the two hooks for providing and changing data.
Core’s own implementations are now OOP methods in classes under src/Hook/ (for example the dblog and history modules),
so a module can use the same style as described in Hooks and OOP hooks.
Displays
A display is a way of exposing the view’s configuration somewhere. One view holds one Default display plus any number of
additional displays that override parts of it. The core display plugins are listed in the Views API documentation; all
are plugins with a ViewsDisplay attribute in current 11.x core.
| Display | Plugin id | Use |
|---|---|---|
Page |
|
A route at a path, with an optional menu link or tab and a title. Path arguments ( |
Block |
|
A block that appears in Structure > Block layout (see Blocks and block layout), with its own block name and category. A block display can take contextual filters from the page it is placed on. |
Feed |
|
RSS or Atom output at a path; uses the Feed style and a row plugin such as Content or Fields. It can be attached to other displays as a feed icon. |
REST export |
|
A data endpoint from the |
Embed |
|
A display that can be embedded using the Views API (for example from a render array). It allows attachment displays to be attached to it and marks its output as embedded. |
Attachment |
|
Extra output placed before or after another display: a summary table above a list, a "featured" row before the results. Settings include the attachment position, whether to inherit contextual filters, exposed filters and the pager. |
Entity Reference |
|
Supplies the options of an entity reference field that uses the "Views: Filter by an entity reference view" selection method. |
Two practical consequences. First, REST export, Feed and Embed displays return data or fragments, not full pages, so
they are tested with curl or a render array rather than with the browser’s rendered page. Second, JSON:API, not a REST
export, is the zero-configuration way to expose standard content (see JSON:API); a REST
export earns its place when the response must be shaped by a view (a filtered list with exposed filters, or a computed
selection such as "objects currently on display").
REST export authentication is a setting of the display. The auth option is empty by default, in which case the
default (cookie based) authentication applies. The settings form warns that the authentication system falls back to the
anonymous user when authentication fails, so always set the view’s access restrictions separately and never rely on the
authentication provider alone to hide data. Authentication for the Next.js kiosk is covered in
Authentication and OAuth.
Format and row: fields versus rendered entities
The format (style plugin) decides the overall layout, and the row plugin decides what one item looks like. The two common row choices are very different:
| Show: Fields | Show: Content (rendered entity) | |
|---|---|---|
Output |
Exactly the fields you add, in the order and with the formatters you choose in the view. |
The entity rendered in a chosen view mode, such as |
Where you configure it |
In the view (field settings, rewrite results, labels). |
In Manage display of the entity type (see Display modes and image styles). |
Required for |
Table format, aggregation, sortable columns, rewritten output, data export fields. |
Reusing the same teaser on a page, a block and a search result so that it looks the same everywhere. |
Cost |
One query for the base rows. Each field loads the entity unless Views can read the value straight from the joined table. |
Loads each entity and renders it. Entity render caches are used, so repeated lists are cheap once warm. |
For the Exhibitions by gallery view the best row is the rendered entity in the teaser view mode, which already
contains the exhibition-teaser component of the meridian theme. For the Objects on display table, fields are the
better choice because the page is a sortable table of accession numbers, periods and materials. Mixing the two in one
view is possible per display: a block can show rendered teasers while the page shows a table.
Field settings worth knowing:
-
Rewrite results lets a field be output through a small Twig-like template using replacement tokens (for example the title field’s token) of fields placed earlier in the view; the token appears only for fields that come before the one being rewritten, so rearranging the field order changes what is available.
-
Multiple field settings controls how a multi-value field is output. With "Display all values in the same row" enabled (the default), the row stays single; without it, each value produces a duplicate row for the same entity, which is the usual cause of "my events appear three times".
-
Hide if empty, No results text and Link to content (link the field to the entity) are set per field.
-
Each field can carry a label (turn it off with "Create a label" unchecked, as the User Guide does for a title and image list).
The Views documentation page "Rewrite the output of a view field" and "Customize the output style of a view field or list" cover the details of the UI; theming of the resulting HTML follows the normal template suggestion rules (see Template suggestions and preprocess).
Filters, including exposed filters
A filter restricts the rows. Filters always apply: a view with no published-status filter shows unpublished content to anyone who may view the view and who passes access checks on the individual entities, so the "Published (= Yes)" filter that the wizard adds is part of the design, not decoration. Common filters for the Meridian Museum:
-
Content: Published (= Yes), or "Published status or admin user" so that editors can still preview;
-
Content: Content type (= Event);
-
a date filter, for example
field_event_datesend value greater than or equal to the offsetnow; -
a taxonomy term filter on
field_topicwith the "Dropdown" selection type or an autocomplete; -
a boolean filter, such as
on_displayequal to true for objects on display.
Filter groups combine conditions with AND or OR. Grouping several conditions into an OR group is how "exhibitions
that are either current or have the 'permanent' flag" is expressed.
Exposing a filter
Any filter can be exposed to visitors: in the Views UI open the filter, choose More, and enable "Expose this filter to visitors". The documentation distinguishes two modes. A single filter shows one control with a label and description. Grouped filters show a pre-built choice list in which each row defines a label, an operator and a value, with options to remember the visitor’s selection, allow multiple selections and render the widget as radio buttons or a select list.
An exposed filter has an identifier (the query-string key, topic in ?topic=12), a required checkbox, and for text
filters an operator choice. Several exposed filters plus the pager and exposed sorts make up the exposed form. Its
settings are on the Advanced column under "Exposed form style":
-
Basic renders the form with a submit button;
-
Input required shows nothing until the visitor submits at least one value, which is the right choice for a very large
collection_objectsearch page; -
the submit and reset button labels, the "Expose sort order" and "Expose items per page" options, and whether the form appears above or below the results are settings of the form style.
The form submits with GET, so exposed filters are part of the URL and are bookmarkable. That also means the query
string is part of the page cache key: Views adds the url.query_args cache context for exposed input, and the
dynamic page cache stores one variation per distinct combination. A site that exposes a free-text filter and gets
thousands of distinct searches fills its page cache with variations, so exposed free-text forms usually belong on pages
with the "Input required" style and the Search API (see Search and Search API) rather than on a
plain SQL view.
The Upcoming events view
The upcoming_events view lists events whose end date has not passed, soonest first, with an exposed filter on the
topic vocabulary. The event content type has a field_event_dates date range field (see
Content types and fields). Configuration, written step by step in the Views UI, is:
-
Show: Content of type Event, sorted by Title, create a page at
eventsand a block, both with a mini pager; -
Filter criteria: Published (= Yes); Content type (= Event);
field_event_dates(field_event_dates:end_value) greater than or equal to the type "An offset from the current time" with valuenow; -
Exposed filter: Content: Topic (
field_topic), identifiertopic, "Dropdown"; -
Sort criteria:
field_event_dates(field_event_dates:value) ascending; -
Pager: Paged, mini, 10 items;
-
Row: Content, view mode
teaser; -
Caching: Time-based, one hour (see Caching);
-
Access: Permission, "View published content".
The relative filter "now" is evaluated when the query runs, so a cache that never expires would keep showing a finished event for as long as no node is saved. The cache section explains why this view uses time-based caching.
Contextual filters
A contextual filter has the same effect as a filter, but its value is not configured: it is read from the display’s
context. The User Guide describes contextual filters as taking their values from the URL, the date or time, or another
detectable value. In the page display the path exhibitions/gallery/% passes the third URL segment to the first
contextual filter as argument 1. In a block display the arguments come from the page it is placed on, normally the route.
When adding a contextual filter, the "When the filter value is not in the URL" section is the part that matters. The documentation lists the choices:
-
Display all results for the specified field;
-
Provide default value;
-
Hide view;
-
Display a summary;
-
Display contents of "No Results Found";
-
Display "Access Denied".
"Provide default value" offers several sources: Content ID from URL, a fixed value, a query parameter, a raw value from the
URL, Taxonomy term ID from URL, User ID from logged in user and User ID from route context. The "When the filter value is
in the URL or a default is provided" section lets the view override the title (using a placeholder such as %1) and
specify validation criteria. A relationship may be needed when the context value belongs to another entity than the
base table.
Defaults that bite:
-
"Display all results for the specified field" is the default if the argument is missing, which is fine on a page but surprising on a block that is placed on every page: the block then lists everything. Prefer "Hide view" for contextual blocks.
-
Validation should be turned on. Without validation, an argument such as
exhibitions/gallery/999999simply returns an empty list; with "Taxonomy term" validation restricted to thegalleryvocabulary, an unknown or foreign term is rejected, and the "action to take if the filter does not validate" can be a 404 page. -
"Allow multiple values" lets
+and a comma combine values, so1+2means "gallery 1 or gallery 2" and1,2means "and". Do not enable it when the argument comes from a user-controlled path unless a bounded number of values makes sense.
The Exhibitions by gallery view
The view exhibitions_by_gallery lists exhibitions in one gallery. Its settings:
-
Show: Content of type Exhibition, sorted by start date;
-
Relationship: Content: Gallery (the term referenced by
field_gallery), so the gallery name can be displayed; -
Contextual filter: Content: Gallery (
field_gallery), default "Taxonomy term ID from URL" with the gallery vocabulary, validation "Taxonomy term" restricted togallery, and "Hide view" for an unmatched value; -
Title override:
Exhibitions in %1; -
Page display at
exhibitions/gallery/%, Block display placed on the taxonomy term page (the term page route supplies the term ID, so the same view works as the content of the term page without a hand-made path).
The block display lets editors see the list in the sidebar of any gallery term page, while the page display allows a link from the main menu. The same contextual filter feeds both through the shared Default display.
Relationships
A relationship joins a second table to the base table so that its fields, filters and sorts become available. The documentation example adds "Content: Content author" to a content view; the user name field then appears as "User: Name". The "Require this relationship" option turns the join from a left join to an inner join, so that rows without the related entity disappear from the view. The documentation warns that content without the relationship is hidden when this option is enabled.
Typical relationships on the Meridian Museum:
-
Content: Content author (to the user table);
-
Content: Gallery (field_gallery, to the
taxonomy_term_field_datatable); -
Content: Hero image (a media entity reference, then Media: Image to get the file);
-
Content: Related objects (a reference to
collection_object, which allows filtering exhibitions by an object’s period).
Relationships are the main source of unexpected row counts. A relationship to a multi-value reference returns one row per
referenced entity, so a node with three related objects appears three times. Two remedies exist: enable Query settings
> Distinct (adds DISTINCT, which can break sorting and aggregation) or use aggregation with a group column.
When every row is accidentally duplicated, check that no relationship is set to return more rows than the display needs.
Sort criteria
Sorts are applied in the order listed, so a sort list reads from the most to the least significant key. Rearrange the sorts with the drop-down next to the Sort criteria heading. Some sorts have special behaviour:
-
Content: Sticky or Promoted are simple flag sorts that put pinned items first;
-
Global: Random reorders the results on each request and defeats result caching (each request has a different order), so a random display must use a cache maximum age short enough to make the randomness visible, or none;
-
Date granularity on date sorts ("Second", "Minute", "Hour", "Day") controls whether two events on the same day tie.
Sorts can also be exposed: the visitor then chooses the order from the exposed form. Table formats can additionally make column headings clickable to sort.
Pagers
The pager is part of the Advanced column. The core pager plugins are:
-
Display all items (no limit; dangerous on a large table);
-
Display a specified number of items (a plain limit, with an optional offset; no pager links);
-
Paged output, mini (previous and next only; it does not run the count query that the full pager needs);
-
Paged output, full (page numbers; needs a
COUNTquery for the total).
The mini pager is cheaper on big tables. The full pager shows total counts but must count all rows matching the filters,
which for the collection_object table with exposed filters can cost more than the page itself. A view can expose
"Items per page" and the offset. The pager uses a numbered page query parameter, so every page is a separate cache
variation by url.query_args. The User Guide’s block display uses "Display a specified number of items" with a limit of 5
to show the newest recipes in a sidebar.
Aggregation
By default each row of a view is one entity (or one row of a table). Enabling Advanced > Use aggregation changes the
query into a GROUP BY query, and each field, filter, sort and contextual filter gets an "Aggregation settings" link
that selects how its values combine: Group results together, Count, Count DISTINCT, Sum, Average, Minimum, Maximum and
similar. For the Meridian Museum, a statistics page "Objects by period" uses:
-
Show:
Collection object; -
Field: Period, aggregation type "Group results together";
-
Field: ID, aggregation type "Count";
-
Sort: Period, ascending.
The result is one row per period with the number of objects. Aggregation requires the Fields row style (rendered
entities cannot be grouped), is incompatible with several features of the full pager, and interacts with DISTINCT
and with relationships, because every field that is not aggregated must be part of the GROUP BY. Two pitfalls: a field
formatter that needs the entity (an entity reference label, for example) loads one entity for each result row, and an
aggregated view still needs the correct cache tags, because the aggregate changes whenever any object changes.
Caching
Views has two cache layers, both configured under Advanced > Caching: the query results (the raw rows) and the
rendered output (the HTML of the display). Core ships three cache plugins, all attribute-based plugins in the
Drupal\views\Plugin\views\cache namespace and extending CachePluginBase:
| Plugin | Id | Behaviour |
|---|---|---|
None |
|
No Views-specific caching. The render and page caches still apply to the surrounding page. |
Tag based |
|
"Tag based caching of data. Caches will persist until any related cache tags are invalidated." The default maximum age is permanent, so the cache entry is valid until a tag is invalidated. |
Time-based |
|
"Simple time-based caching of data." Two options, |
Tag-based caching
Tag-based caching relies on the cache tags that the view declares. A view that lists nodes carries the tag node_list,
plus the per-entity tags of each node it renders, and a list of entities of a type is invalidated whenever one is added,
changed or deleted. That is exactly right for "Exhibitions by gallery": a new exhibition or an edited one clears the
affected views at once, and nothing else does. Contextual filter values and the pager add the cache contexts
url and url.query_args, so each combination is cached separately. The cache metadata of a display is stored in the
view configuration under cache_metadata and recalculated when the view is saved. For the underlying model see
Render API and cache metadata and
Performance and caching.
Tag-based caching is wrong for data that changes without a tag being invalidated, which is exactly the case for relative
dates. In upcoming_events the filter "end date >= now" changes meaning as time passes, but nobody saves a node when an
event finishes, so a tag-only cache would keep showing the event until another event is edited. Use time-based caching for
such a view, with a lifespan short enough for the editorial requirement (an hour for events that end on the hour).
Time-based caching
Time-based caching sets a maximum age. It is simpler than tags but means editors wait for a change to appear, unless a tag invalidates it earlier: the rendered output cache is also tagged, so a node edit still invalidates it through the dynamic page cache. The practical recommendation is to combine them: time-based for views with a "now" component, tag-based for everything else. A custom cache plugin can combine both; the contributed Views Custom Cache Tag module adds per-view tags and optional time-based invalidation for relative date filters.
The cacheExpire() method in the core tag and time plugins is deprecated in Drupal 11.4.0 and scheduled for removal in
13.0.0 with no replacement, so a custom cache plugin that extends one of them and calls it should be updated.
What not to cache
Views that depend on the current user (for example "My unread news" with the contextual default "User ID from logged in
user") need the user cache context. Views declares it when the user default is used. A custom plugin that varies by
something else must say so in its cache metadata, or one user’s list will be served to another.
Access
The Access setting (Advanced > Access) decides who may see a display:
-
None (unrestricted; use only for views that cannot expose private data);
-
Permission (a single permission such as "View published content");
-
Role (one or more roles).
Access to the display does not replace access to the rows. For a node base table Views adds the node_access query tag
when the query runs, so queries respect the content access system. The query setting Disable SQL rewriting turns this
off and is an administrative bypass that exposes unpublished and restricted content; leave it off except for views that
only administrators can reach. Other points:
-
The "Published (= Yes)" filter is the only thing that hides unpublished content in the view’s own query. The query access system also hides it from users without permission; do not depend on only one of the two.
-
An entity of a custom type (
collection_object) needs its own query access rules in its entity type, because there is no automatic access rewrite for arbitrary tables. See Access control and permissions in code. -
A REST export view needs a permission or role plus an authentication provider. The default authentication is cookie based; for tokens use the OAuth setup in Authentication and OAuth.
-
The permission "Administer views" is a high-privilege permission: a person who can edit views can read anything the site stores by writing a view over it. Grant it only to administrators.
Review the permissions of the Views modules in Users, roles and permissions.
Header, footer and empty results
The header and footer areas hold text, a result summary ("Displaying 1 - 10 of 124"), links, an embedded view or the exposed form. The No results behavior area shows a message when the query has no rows, which for an events listing is a friendly "No events are scheduled" instead of a blank region. Text in these areas uses a text format, so it is subject to the format’s HTML restrictions (see Text formats and CKEditor 5).
Attachments
An attachment display adds extra content to another display. Typical use: an attachment "Featured exhibition" with one item before the list. Its important settings are the attachment position (before or after), the displays to attach to, and the three inheritance checkboxes (contextual filters, exposed filters and the pager). If it inherits exposed filters, the attachment re-runs with the visitor’s choices; if not, it is independent of the form. An attachment never has its own path or block, so it is tested through the display it is attached to.
Block displays in detail
A block display publishes the view as a block plugin. Settings include the block name (what the Block layout UI shows), the block category, "Allow settings" (lets the placer override items per page in the block configuration) and a caching option inherited from the view. After saving the view, place it from Structure > Block layout (see Blocks and block layout). Block displays are the cheapest way to produce "On this day" or "More from this gallery" modules, but they have two traps: a block placed on many pages runs once per page unless the cache is warm, and contextual filters of a block use the route of the page, so a block placed on a page without the expected route parameter falls to its "value not available" action.
The Objects on display view
The view objects_on_display has three displays on the collection_object base table:
| Display | Path or placement | Purpose |
|---|---|---|
Page |
|
A table of accession number, title, period, material, with exposed filters on period and material and a full pager. |
Block |
Region Sidebar second on gallery pages |
Five objects, rendered as the |
REST export |
|
JSON of accession number, title and gallery for the Next.js kiosk, authenticated with OAuth. |
All three share the Default display’s filter "On display (= True)" and Published status. The REST export is the only
display that sets its own style (serializer) and authentication, as in the exported configuration in the next section.
The headless kiosk is described in Decoupled Drupal with Next.js. When the output of a list should be generic content, JSON:API is better; when it should be a specific, filtered projection, a view is the cleaner tool.
Exporting a view to configuration
A view is a configuration entity named views.view.<machine_name>, so it is exported like any other configuration (see
Configuration management and Configuration API and schema). Export with Drush and
commit the file:
# Export all configuration to the sync directory (config/sync)
ddev drush config:export -y
# Look at one view as YAML
ddev drush config:get views.view.upcoming_events
# Import on the target environment as part of a deploy
ddev drush deploy
Source: Drush 13 commands
Drush also ships view-specific commands: views:list (list views), views:enable and views:disable (change a view’s
status), views:execute (run a view and print the results) and views:dev (set several Views settings to
developer-friendly values). On a staging site ddev drush views:execute upcoming_events page_1 is a quick check that
the query returns rows without a browser.
The exported views.view.upcoming_events.yml has the structure below. Only the part relevant to the page display is
shown. Note the dependencies block (Views records the modules, content types and fields it needs, so the view imports
only where these exist) and the cache plugin settings.
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_event_dates
- field.storage.node.field_topic
- node.type.event
module:
- datetime_range
- node
- taxonomy
- user
id: upcoming_events
label: 'Upcoming events'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
id: default
display_title: Default
display_plugin: default
position: 0
display_options:
title: 'Upcoming events'
fields: { }
pager:
type: mini
options:
offset: 0
items_per_page: 10
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: true
access:
type: perm
options:
perm: 'access content'
cache:
type: time
options:
results_lifespan: 3600
results_lifespan_custom: 0
output_lifespan: 3600
output_lifespan_custom: 0
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
style:
type: default
row:
type: 'entity:node'
options:
view_mode: teaser
sorts:
field_event_dates_value:
id: field_event_dates_value
table: node__field_event_dates
field: field_event_dates_value
plugin_id: datetime
order: ASC
filters:
status:
id: status
table: node_field_data
field: status
plugin_id: boolean
value: '1'
group: 1
type:
id: type
table: node_field_data
field: type
plugin_id: bundle
value:
event: event
group: 1
field_event_dates_end_value:
id: field_event_dates_end_value
table: node__field_event_dates
field: field_event_dates_end_value
plugin_id: datetime
operator: '>='
value:
type: offset
value: now
group: 1
field_topic_target_id:
id: field_topic_target_id
table: node__field_topic
field: field_topic_target_id
plugin_id: taxonomy_index_tid
exposed: true
expose:
identifier: topic
label: Topic
group: 1
cache_metadata:
max-age: 3600
contexts:
- 'languages:language_content'
- url.query_args
- user.permissions
tags: { }
page_1:
id: page_1
display_title: Page
display_plugin: page
position: 1
display_options:
path: events
menu:
type: normal
title: Events
menu_name: main
The meridian_events recipe applies this view to a fresh site. It lists the modules to install and imports the exported
configuration shipped inside the recipe directory (see Recipes):
name: 'Meridian events'
description: 'Event content type with an Upcoming events view.'
type: 'Content type'
install:
- node
- datetime_range
- taxonomy
- views
config:
import:
node:
- node.type.event
views:
- views.view.upcoming_events
Views UI is itself a module whose configuration should not reach production unless site builders must edit there. A
common pattern is to enable views_ui locally, export, commit, and keep views_ui out of the production module list
through a configuration split (see Deployment and CI/CD).
Rendering a view from code
A view can be embedded in a render array with the view render element. Its properties are #name (the machine name),
#display_id (default default), #arguments (default empty array), #embed (default TRUE, which calls preview();
FALSE runs executeDisplay() and adds contextual links) and, optionally, #view to pass a loaded view object.
<?php
declare(strict_types=1);
namespace Drupal\meridian_collection\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\views\Views;
/**
* Shows the Objects on display view inside a custom page.
*/
final class OnDisplayController extends ControllerBase {
/**
* Builds a page that embeds the block display with one argument.
*/
public function page(string $gallery): array {
return [
'#type' => 'view',
'#name' => 'objects_on_display',
'#display_id' => 'block_1',
'#arguments' => [$gallery],
'#embed' => TRUE,
];
}
/**
* Loads the view object to read or change it before execution.
*/
public function count(): int {
$view = Views::getView('objects_on_display');
if ($view === NULL) {
return 0;
}
$view->setDisplay('page_1');
$view->execute();
return count($view->result);
}
}
Source: Drupal API: View render element
The old helper views_embed_view() embeds a view from a PHP snippet, but it is deprecated as of Drupal 11.4.0 and will be
removed in Drupal 13.0.0; its documentation recommends '#type' => 'view' render elements instead. Older books and
tutorials that call views_embed_view('name', 'block_1') or views_get_view_result() in a preprocess function should be
updated.
Altering a view
Two hooks cover most needs. hook_views_query_alter(ViewExecutable $view, QueryPluginBase $query) runs just before a
query is executed; core’s own implementations include the workspaces module and the content moderation test module.
hook_views_data_alter() changes the data that other modules provide. In an OOP module the implementation sits in a
class under src/Hook with the #[Hook] attribute, as described in Hooks and OOP hooks:
<?php
declare(strict_types=1);
namespace Drupal\meridian_collection\Hook;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ViewExecutable;
/**
* Views hooks for the Meridian collection.
*/
final class MeridianViewsHooks {
/**
* Hides retired objects from the kiosk endpoint.
*/
#[Hook('views_query_alter')]
public function queryAlter(ViewExecutable $view, QueryPluginBase $query): void {
if ($view->id() !== 'objects_on_display' || $view->current_display !== 'rest_export_1') {
return;
}
// The query is a Sql query; add a condition on the base table.
$query->addWhere(0, 'collection_object_field_data.retired', 0, '=');
}
}
Most customisations should be done with the UI instead. Custom query code hides logic from site builders, bypasses
the exported configuration and has to be kept in step with schema changes. When the same filter is useful everywhere, a
custom filter plugin (a class with the ViewsFilter attribute in src/Plugin/views/filter) is the maintainable form.
Debugging and troubleshooting
-
Show the SQL query. At
admin/structure/views/settings, "Live Preview Settings" has "Show the SQL query", which prints the generated queries in the Preview. Atadmin/structure/views/settings/advanced, "Add Views signature to all SQL queries" appends the view and display names to each SELECT so that they are findable in database server logs; the documentation says to use it only when troubleshooting. -
Preview and autopreview. The preview under the editor runs the display with its exposed filters and arguments (a field for arguments, separated by
/). -
Duplicate rows: check relationships to multi-value references, the field setting "Display all values in the same row", and "Distinct".
-
Missing rows: a "Require this relationship" setting, an inner join with a missing value, translation filters, or a published filter.
-
Stale output: the cache plugin. The documentation notes that Views caches data for tables, modules and available views, and that disabling caching can seriously harm performance, so disable it for debugging only (the "Never cache" choice of the time-based plugin, or
drush views:dev). -
Overridden settings: open every display and read the "For" selector of the setting that looks wrong; the value on the Default display and the display’s override can differ.
-
Broken views after a module is removed: the view’s
dependenciesblock lists the module; uninstalling a module that provides a field used by a view is blocked by the dependency until the view is fixed or deleted.
Version notes and findings
-
The Views and Views UI modules are in core in Drupal 11.4. The Standard install profile no longer creates the Article and Page content types from 11.4, so a tutorial that starts from "the Article content type" or "Frontpage view of Articles" needs you to create the content types first; a view that filters by a type that does not exist imports with a missing dependency.
-
Plugins used by Views (cache, display and others) use PHP attributes in current 11.x core (for example
#[ViewsCache]and#[ViewsDisplay]); annotations are on a deprecation path (see Plugin API and attributes). -
views_embed_view()is deprecated as of 11.4.0, for removal in 13.0.0 (use theviewrender element). -
cacheExpire()in the core tag and time cache plugins is deprecated as of 11.4.0, for removal in 13.0.0. -
Views that rely on modules removed from core lose their data. Statistics ("Content: Total views", "Popular content" blocks) was removed in 11.0, as were Book and Forum, so views built on their fields need the contrib versions installed before the upgrade, not uninstalled. Drupal 12 moves History ("Has new content", "Last viewed" fields) and Search to contrib, so views using their handlers need the contrib module. See Removed core modules and replacements and Preparing for Drupal 12.
-
Drupal 12 removes Claro, Olivero and Stable 9 from core. The Views UI is themed through the admin theme, so check how the Views edit screen looks under Default Admin (based on Gin) when it becomes the admin theme.
-
Entity queries must call
accessCheck()explicitly. A view does the equivalent through its access plugin and query tags, so replacing a view with a hand-written entity query in a custom controller means adding the access check by hand.
Corrections to older books and tutorials
Older books such as Drupal 10 Masterclass (2023) and Drupal 10 Development Cookbook (2023) teach Views from the Drupal 10.0 and 10.1 era. The Views UI concepts (base table, display, fields, filters, contextual filters, relationships, caching) still apply, and the User Guide and the Views documentation pages remain the reference. What changed:
-
Examples that embed a view with
views_embed_view()should use theviewrender element; the function is deprecated in 11.4. -
Custom Views cache, display, filter or field plugins shown with
@ViewsCache,@ViewsDisplayor@ViewsFilterannotations now use the matching attribute; annotations are deprecated. -
Views hooks shown only as
.moduleor.views.incfunctions can be written as OOP hook methods with#[Hook]on autowired classes insrc/Hooksince 11.1; core already does this for its ownhook_views_data()implementations. -
Views built on Statistics, Book, Forum, Tour and Actions UI data refer to modules removed from core in 11.0; pages about "Popular content" blocks need the contrib replacement.
-
Screens that show Classy or Seven markup for the Views UI, or CKEditor 4 in header and footer text, belong to older releases: Views text areas use CKEditor 5, and the administration theme is Claro (Default Admin in Drupal 12).
-
Tutorials that begin with the Article content type and the Tags vocabulary assume a Standard install from before 11.4.
Common errors and pitfalls
-
Choosing the wrong base table; it cannot be changed. Rebuild the view.
-
Editing the wrong scope: using "All displays" when "This page (override)" was intended.
-
A relationship to a multi-value field producing duplicate rows.
-
Forgetting the Published filter; or enabling "Disable SQL rewriting" and exposing unpublished content.
-
A "Hide view" or "Display all results" choice on a contextual block that renders on every page.
-
A tag-based cache on a filter that depends on
now; use time-based caching. -
An exposed free-text filter with the full pager on a large table, which runs a costly count query for each distinct search.
-
Exposed filters that require
GETparameters ignoring the clean-URL path; test with a real query string. -
A rendered-entity row with aggregation (not possible; use fields).
-
A REST export with no permission or authentication restriction; the anonymous fallback reveals what the view returns.
-
Editing a view on production through Views UI; the next configuration import overwrites it.