Drupal: Taxonomy
|
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. |
Taxonomy is the part of Drupal that classifies content. A vocabulary is a named set of terms, a term is a small content entity (a name, an optional description and any fields you add), and a term reference field on a content type, a user, a block or any other fieldable entity points at the terms that apply. This page explains the vocabulary and term model, hierarchy, the term reference field and its widgets, the automatic term page, free tagging, translation, and how to choose between taxonomy, an entity reference to content and a plain list field.
It 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 vocabularies topic (flat, with
free tagging) and gallery (hierarchical, a fixed list of rooms and wings), referenced from the exhibition and event
content types and from the custom collection_object entity of the module meridian_collection. 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 taxonomy is and who uses it
The User Guide defines taxonomy as the system for classifying website content, with terms as the individual entities and vocabularies as sets of related terms. Four roles meet it:
-
editors pick or type terms on a content form;
-
site builders create vocabularies, add reference fields and decide whether the list is fixed or open;
-
administrators grant permissions per vocabulary;
-
developers load terms, walk trees, build entity queries and write templates.
Taxonomy is a core module (taxonomy), enabled by the Standard profile. It depends on the field and entity reference
machinery (see Content types and fields and
Field types and entity references). The Standard profile in
Drupal 11.4 no longer creates the Article and Page content types, so do not assume that a Tags vocabulary or a
field_tags field exists on a fresh site: open the Taxonomy overview and create what you need. Tutorials that start from
"the Tags field on Article" predate this change.
The data model
Two entity types cooperate:
-
Vocabulary (
taxonomy_vocabulary) is a configuration entity, exported astaxonomy.vocabulary.<vid>.yml. It holds the machine name (vid), the label, a description and a weight. It is also the bundle of the term entity type, so each vocabulary can have its own fields, form display and view displays. -
Term (
taxonomy_term, classDrupal\taxonomy\Entity\Term) is a content entity stored in the database, with base fieldstid,uuid,revision_id,vid(the bundle),langcode,status(published),name,description(formatted text),weight,parent,changedandpath(URL alias). Terms are revisionable and translatable.
Because vocabularies are configuration, they travel with the code through configuration management (see
Configuration management). Terms are content: they do not travel with
drush config:export, so a fixed list such as the Meridian galleries has to be created by an update hook, a migration or
default content, or by hand on each environment. A reference field that targets a vocabulary stores only term IDs, and
these IDs differ between environments, which is why references to terms are usually resolved by name or UUID in code.
A vocabulary exported by drush config:export looks like this:
# config/sync/taxonomy.vocabulary.gallery.yml
langcode: en
status: true
dependencies: { }
name: Gallery
vid: gallery
description: 'Rooms and wings of the Meridian Museum.'
weight: 0
Creating vocabularies and terms in the administrative interface
The User Guide walk-through (chapter 6) goes through these screens:
-
Structure > Taxonomy (
/admin/structure/taxonomy) lists the vocabularies. Add vocabulary asks for a name (the machine name is derived from it) and a description. -
The vocabulary’s Overview tab (
/admin/structure/taxonomy/manage/gallery/overview) lists the terms with drag handles and Add term. The Manage fields, Manage form display and Manage display tabs behave as for a content type. -
Add term (
/admin/structure/taxonomy/manage/gallery/add) asks for a name, a description and any fields, plus the Relations section (parent terms and weight) and the URL alias. -
The Edit, Delete and Translate operations on each term live on the term itself (
/taxonomy/term/ID/edit).
The overview page pages its list (the terms_per_page_admin setting of taxonomy.settings, 100 by default), so a large
vocabulary is reordered a page at a time. Reset to alphabetical sets every weight back to 0 and sorts by name.
Terms can carry fields. The Meridian gallery vocabulary has an image field field_gallery_photo and a text field
field_floor; the topic vocabulary has an optional field_icon. Those fields are shown on the term page through the
vocabulary’s view displays (see Display modes and image styles).
Hierarchy
A vocabulary is flat or hierarchical, and nothing has to be configured to choose: each term has a parent field that
holds zero, one or several parent term IDs (a root term has parent 0). Since Drupal 8 the hierarchy type of a
vocabulary is computed from the data, and TermStorageInterface::getVocabularyHierarchyType() returns whether it is
disabled (no term has a parent), single (each term has at most one parent) or multiple (at least one term has several).
-
Drag a term under another in the overview page, or choose Parent terms in the Relations section of the term form. The drag interface only produces single parents; multiple parents come from the Relations select list (with several values selected), from code, from migrations or from imports.
-
Multiple parents are legitimate (a "Fossils" term under both "Biology" and "Earth science"), but they make tree walking, breadcrumbs and "include children" filters return the same term in several places, so keep them to the vocabularies that need them.
-
Deleting a term also deletes its children, unless a child has another parent, in which case it is only detached. Check the overview before deleting a branch, because there is no undo besides a backup.
The storage service gives a developer the tree operations. In a class, inject the entity type manager and ask for the
storage; the \Drupal:: static call belongs only in procedural code and tests.
<?php
namespace Drupal\meridian_collection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Builds options for the gallery filter of the collection pages.
*/
final class GalleryTree {
public function __construct(
private readonly EntityTypeManagerInterface $entityTypeManager,
) {}
/**
* Returns term names indented by depth, keyed by term ID.
*
* @return array<int, string>
*/
public function options(): array {
/** @var \Drupal\taxonomy\TermStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('taxonomy_term');
$options = [];
// loadTree(vocabulary, parent, max depth, load entities). Passing FALSE
// returns lightweight objects (tid, name, depth, parents), which is much
// cheaper than loading every term entity.
foreach ($storage->loadTree('gallery', 0, NULL, FALSE) as $item) {
$options[(int) $item->tid] = str_repeat('-', $item->depth) . ' ' . $item->name;
}
return $options;
}
}
Source: API: TermStorageInterface
The same interface offers loadParents(), loadAllParents() (the ancestors, for breadcrumbs), loadChildren(),
nodeCount() and getNodeTerms(). A term that appears under several parents is returned once per position by
loadTree(), so de-duplicate by tid when you build a flat list.
Term reference fields
A term is attached to something through an entity reference field whose target type is Taxonomy term. In the user interface (Structure > Content types > Exhibition > Manage fields), choose Create a new field, pick the Reference group and Taxonomy term, give it a label, and set the allowed number of values. The field settings then offer:
- Reference method
-
Default(the selection plugindefault:taxonomy_term, which can sort by name or ID) or a View that produces the list. Use a View when the choices depend on context, for example only the terms of a branch. - Vocabulary
-
The allowed vocabularies (
target_bundles). Restricting to one vocabulary keeps editors from mixing lists. - Sort by
-
Name, term ID or another property, ascending or descending.
- Create referenced entities if they don’t already exist
-
The
auto_createsetting, which turns the field into a free-tagging field (see below). If several vocabularies are allowed,auto_create_bundlenames the one that receives new terms.
The resulting configuration is two config entities: a field storage (field.storage.node.field_topics, the type and
cardinality) and a field instance (field.field.node.exhibition.field_topics, the label, selection settings and
required flag). The instance depends on the vocabulary, so the vocabulary must be imported first; configuration
management handles that through the dependencies key.
# config/sync/field.storage.node.field_topics.yml
langcode: en
status: true
dependencies:
module:
- node
- taxonomy
id: node.field_topics
field_name: field_topics
entity_type: node
type: entity_reference
settings:
target_type: taxonomy_term
module: core
locked: false
cardinality: -1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
# config/sync/field.field.node.exhibition.field_topics.yml
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_topics
- node.type.exhibition
- taxonomy.vocabulary.topic
id: node.exhibition.field_topics
field_name: field_topics
entity_type: node
bundle: exhibition
label: Topics
description: 'Subjects an exhibition covers, for example Astronomy or Geology.'
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
handler: 'default:taxonomy_term'
handler_settings:
target_bundles:
topic: topic
sort:
field: name
direction: asc
auto_create: true
auto_create_mode: ''
auto_create_bundle: ''
field_type: entity_reference
Cardinality -1 is "unlimited". The same field type can be added to users, custom blocks, media and the
collection_object entity. A reference to collection_object terms is added in code through base field definitions
(see Custom entity types).
Widgets and formatters
The widget decides how editors choose terms. The reference field widgets in core are:
-
Autocomplete (
entity_reference_autocomplete): a text box that suggests matching names. -
Autocomplete (Tags style) (
entity_reference_autocomplete_tags): a single text box with comma-separated names, the usual widget for free tagging. A name that contains a comma must be quoted. -
Select list, Check boxes/radio buttons (
options_select,options_buttons): show every allowed term, suitable for short fixed lists; a hierarchical vocabulary appears with indentation by depth.
The formatters are Label (with or without a link to the term page), Entity ID, and Rendered entity (the term in a
view mode such as teaser). The linked label is what turns a topic into a link to its term page. The User Guide
describes widgets and formatters in its form and display chapters.
Reading and writing term references in code
A reference field is an item list. Reading it, filtering by vocabulary and writing it back:
<?php
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
$exhibition = Node::load(42);
// Entities referenced by the field, translated to the content language.
foreach ($exhibition->get('field_topics')->referencedEntities() as $term) {
\Drupal::logger('meridian_collection')->info('Topic @label (@id)', [
'@label' => $term->label(),
'@id' => $term->id(),
]);
}
// Append an existing term by ID, and a new term created on the fly.
$astronomy = \Drupal::entityTypeManager()
->getStorage('taxonomy_term')
->loadByProperties(['vid' => 'topic', 'name' => 'Astronomy']);
$exhibition->get('field_topics')->appendItem(reset($astronomy)->id());
$new = Term::create(['vid' => 'topic', 'name' => 'Meteorites']);
$new->save();
$exhibition->get('field_topics')->appendItem($new->id());
$exhibition->save();
Source: Entity API
Code that only runs in a Drush script or a test may call \Drupal:: statics, as above. In a service use constructor
injection (see Services and dependency injection).
Term pages
Drupal makes a page for every published term at /taxonomy/term/ID, listing the content tagged with the term, with a
feed at /taxonomy/term/ID/feed. No code is involved: the module ships a View named Taxonomy term (taxonomy_term)
whose page display takes the term ID as a contextual filter (Has taxonomy term ID (with depth)), shows the term name
and description as a header and lists published content that references the term, newest first and sticky first, with a
pager. The route entity.taxonomy_term.canonical serves the page. Because it is an ordinary view, you can:
-
edit it under Structure > Views to change the sort order, items per page, fields or display mode;
-
leave the default and add a more specific view for one vocabulary by overriding the path or the argument validation;
-
turn the "with depth" argument into one that includes descendants, so that the page of "Biology" also lists content tagged with its child terms (the argument supports a depth number and the option to include terms by negative depth for ancestors);
-
give the term a friendly URL alias (for example
/topics/astronomy) either on the term form or with a Pathauto-style pattern (a contributed module; see Menus and path aliases).
The term page also renders the term entity itself, in the full view mode of its vocabulary. Template suggestions
include taxonomy-term.html.twig and taxonomy-term--gallery.html.twig (the vocabulary), and, with Twig debugging on, the
HTML comments show the exact file names (see
Template suggestions and preprocess).
{# templates/taxonomy/taxonomy-term--gallery.html.twig in the meridian theme #}
{%
set classes = [
'gallery',
view_mode ? 'gallery--' ~ view_mode|clean_class,
]
%}
<article{{ attributes.addClass(classes) }}>
{% if not page %}
<h2><a href="{{ url }}">{{ name }}</a></h2>
{% endif %}
<div{{ content_attributes.addClass('gallery__content') }}>
{{ content }}
</div>
</article>
Source: Working with Twig templates
Since Drupal 11.3 the module’s procedural helpers around term pages are being retired:
taxonomy_term_is_page() and template_preprocess_taxonomy_term() are deprecated in 11.3.0 for removal in 13.0.0 (the
preprocess logic moved to a hook class, and the replacement for the page check is to inspect the view mode during
rendering). A theme that relied on them in .theme code should switch to checking the view_mode variable, as the
template above does, and to the OOP hook mechanism (see Hooks and OOP hooks).
<?php
namespace Drupal\meridian_collection\Hook;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Adds a gallery floor class to the gallery term template.
*/
final class TaxonomyHooks {
#[Hook('preprocess_taxonomy_term')]
public function preprocessTerm(array &$variables): void {
$term = $variables['term'];
if ($term->bundle() === 'gallery' && $term->hasField('field_floor') && !$term->get('field_floor')->isEmpty()) {
$variables['attributes']['class'][] = 'floor-' . $term->get('field_floor')->value;
}
}
}
The taxonomy index
For every published node that references terms, the module keeps a row per term in the taxonomy_index table (node ID,
term ID, status, sticky flag, created time). This table is what makes "content tagged with this term" quick, and the
Views argument above and the term page read it. Writing the index is controlled by the maintain_index_table setting of
taxonomy.settings (true by default). If you switch it off for a site that never lists content by term, the table
stays empty and views that use it return nothing. The helper functions taxonomy_build_node_index() and
taxonomy_delete_node_index() were deprecated in 11.4.0 for removal in 13.0.0 with no replacement, so custom code
should not call them; saving or deleting nodes keeps the index in step. The index covers content (nodes) only: a term
referenced from a user, block or custom entity needs its own view.
Free tagging versus a fixed list
The reference-field settings decide the editorial model:
| Model | Setup | Consequences |
|---|---|---|
Free tagging |
Widget Autocomplete (Tags style); Create referenced entities if they don’t already exist on |
Editors invent terms, so spelling variants ("Fossil", "Fossils") multiply. Needs periodic clean-up, merging and a policy on who may create terms. |
Fixed list |
Widget Select list or Check boxes; auto-create off |
Only administrators add terms (permission Create terms in Gallery). Consistent, but editors cannot add a missing term from the content form. |
Mixed |
Autocomplete with auto-create off |
Editors pick existing terms by typing, but cannot create new ones. |
In the Meridian Museum, topic is free tagging (Content Editors add subjects as they write), whereas gallery is a fixed
list that only the Curator role maintains. The permissions page of the module (People > Permissions, group
Taxonomy) offers per-vocabulary entries: Create terms in, Edit terms in, Delete terms in and View term
revisions in a vocabulary, plus Administer vocabularies and terms, which is an administrator permission that also
allows changing fields and displays. See Users, roles and permissions.
Term pages and published terms are visible to users with the Access content permission; an unpublished term
(status 0) is hidden from them. The same entity query rule that applies elsewhere applies to terms: an entity query
must call accessCheck() explicitly.
<?php
use Drupal\taxonomy\Entity\Term;
// Find or create a topic by name, for an import or a queue worker.
function meridian_collection_topic_id(string $name): int {
$storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$ids = $storage->getQuery()
->accessCheck(FALSE)
->condition('vid', 'topic')
->condition('name', $name)
->range(0, 1)
->execute();
if ($ids) {
return (int) reset($ids);
}
$term = Term::create(['vid' => 'topic', 'name' => $name]);
$term->save();
return (int) $term->id();
}
Source: Working with the Entity API
accessCheck(FALSE) is acceptable here because the code runs in a trusted context (an import or a queue worker); use
accessCheck(TRUE) for anything driven by a visitor’s request. Never build the vocabulary or name from request input
without validation, and keep term names out of |raw in templates: term names and descriptions come from editors, and
Twig autoescaping is what protects the page. See
Database API and entity queries.
Translating terms
Taxonomy has two things to translate, and they are handled by different modules:
-
Terms are content. Enable the Content Translation module, then go to Configuration > Regional and language > Content language and translations (
/admin/config/regional/content-language), tick Taxonomy term, tick the vocabulary (for example Topic), and choose which fields are translatable (name, description, custom fields). A Translate operation then appears on each term at/taxonomy/term/ID/translations, where you add the Spanish name, description and URL alias. The term keeps one ID: every translation is a version of the same entity. -
Vocabularies are configuration. Their label and description are translated through the Configuration translation module at
/admin/config/regional/config-translation, as the User Guide describes for site settings and labels. Field labels and help text of the term reference field are translated there too.
A translated exhibition that references the term "Astronomy" points at the same term ID as the English one. When the field is displayed, Drupal picks the translation of the term that matches the content language, falling back to the original when no translation exists. A reference field itself can be set as translatable or not: if it is not, every translation of the exhibition shares one list of terms; if it is, each language has its own choice of terms.
The term page follows the language prefix: /es/taxonomy/term/12 shows the Spanish name and the content that has a
Spanish translation. Autocomplete matches the names in the current interface language, which matters when editors type
Spanish names but the default language is English. For site-wide multilingual setup see
Multilingual.
# Enable the modules and then translate from the user interface.
ddev drush en content_translation config_translation language -y
ddev drush config:export -y
Taxonomy, entity reference to content and lists
Three structures can look similar to an editor but behave differently. A list field (list_string, list_integer)
stores a fixed set of keys and labels inside the field’s configuration; an entity reference to content points at full
nodes; and taxonomy gives the choices their own entities with fields, hierarchy, a page and a view.
| Aspect | List field | Taxonomy term reference | Reference to a content item |
|---|---|---|---|
Choices live in |
Field storage configuration |
Term entities (content) |
Nodes (content) |
Choices carry fields, a description and a page |
No |
Yes, plus an automatic term page |
Yes, they are full content |
Hierarchy |
No |
Yes, one or many parents |
Only by your own parent field |
Editors can add choices |
No, needs a configuration change |
Optional (free tagging) |
By creating a node (or an inline entity form) |
Travels with configuration export |
Yes |
Vocabulary yes, terms no |
No |
Workflows and revisions |
Not applicable |
Terms are revisionable |
Full content moderation (see Content moderation) |
Typical Meridian use |
Display status ( |
|
|
Choose taxonomy when the values are shared labels that classify many items and need a listing page, hierarchy or
per-value fields. Choose a list field for a short, stable, internal set that developers control. Choose a reference to
content when the target is a real item with its own lifecycle, permissions and workflow: the Meridian Museum’s
"related objects" are collection_object entities, not terms, because each has an accession number and a curator who
owns it. The common mistake is to model a growing set of things as terms (for example every artist) and then need
moderation, per-item access or rich pages that terms handle poorly. The related decision for content versus
configuration is covered in Entity API.
Using taxonomy in Views, search and APIs
-
Views: filters and contextual filters on Content: Has taxonomy term ID (the index) or on the reference field itself; relationships to the referenced term let you show term fields in a content list; an exposed filter shows terms as a select list or autocomplete. See Views.
-
Search: index the term names (through a "Taxonomy term" relationship or the reference field) and use them as facets. See Search and Search API.
-
JSON:API: terms are served under
/jsonapi/taxonomy_term/topic(the vocabulary is the bundle), and a content resource lists its terms inrelationships.field_topics; use?include=field_topicsto embed them. See JSON:API. -
Recipes and migrations: terms are content, so a recipe cannot ship them as configuration, while Migrate imports them by name with the
entity:taxonomy_termdestination (see Migrate API).
GET /jsonapi/taxonomy_term/gallery?filter[name]=East+Wing&include=parent HTTP/1.1
Host: meridian.ddev.site
Accept: application/vnd.api+json
Source: JSON:API: Filtering
Common errors and pitfalls
-
Terms missing on the other environment. Vocabularies and fields are in configuration, terms are not. A reference field that is "required" fails validation on production because the term it needs only exists on your laptop.
-
Duplicate terms from free tagging. Typing a name creates a term when no exact match exists. Allow it only where editors are trained, and review the vocabulary overview regularly.
-
Deleting a vocabulary or a branch. Deleting a vocabulary deletes its terms, and deleting a term deletes children without other parents. Deleted terms leave empty references that Drupal cleans up lazily.
-
Slow trees.
loadTree()with entity loading on a vocabulary of tens of thousands of terms is expensive and the term overview is paged; load without entities, cache the result with thetaxonomy_term_listcache tag and use autocomplete rather than a select list for large lists (see Render API and cache metadata). -
Term page lists nothing. Check that the content is published, that
maintain_index_tableis true, that the cache was cleared after changing the field, and that the view’s contextual filter has not been altered. -
Wrong hierarchy expectations. The "Parent terms" relation is data, not a vocabulary setting. Importing terms with several parents makes the vocabulary multiple-parent, and some contributed widgets assume a single parent.
-
Using terms as access control. A term reference does not restrict who sees content. Use roles, permissions or a dedicated access module; see Access control and permissions in code.
Version notes and older material
-
Taxonomy is a core module in Drupal 10, 11 and 12. Drupal 12.0.0 (scheduled for the week of 2026-12-07) keeps it; the deprecated procedural helpers named above are slated for removal in 13.0.0.
-
The Standard install in 11.4 no longer creates Article and Page, and so no sample tag field. Create vocabularies and fields explicitly or apply a recipe (see Recipes).
-
Older books such as Drupal 10 Masterclass (2023) and Drupal 10 Development Cookbook (2023) build their examples on the Article and Tags of the Standard profile and on Drupal 10.0 and 10.1 code. On current versions create the vocabulary yourself, write hooks as methods on a class in
src/Hook(11.1 and later) and entity or plugin definitions with PHP attributes (10.2 and later) instead of annotations. See Where older books and tutorials are outdated.