Menus and links
Menu links, the tabs on a page, and the action buttons above it are all plugins your module declares in YAML files. This guide adds each kind, then generates a family of links at runtime with a deriver.
Place a module’s pages into Drupal’s menu system: a link in the administration menu, tabs to switch between related pages, an action button to start a task, and one dynamic link per content type. If you only need an editor-managed link, add it in the UI at Administer → Structure → Menus instead; this guide is for links your module ships.
Prerequisites
Section titled “Prerequisites”- A custom module, per Scaffold a module.
- Routes to link to, per Routes and controllers. The examples assume two routes,
howto_menus.overviewandhowto_menus.settings.
Add a menu link
Section titled “Add a menu link”Menu links live in MODULE.links.menu.yml. Each top-level key is the link’s plugin ID; route_name is where it points and parent is the link it nests under.
# A static menu link. The key is the plugin ID; one entry, one link.howto_menus.overview: title: 'Menus how-to' description: 'Example page for the menus and links guide.' route_name: howto_menus.overview parent: system.admin_reports weight: 50parent names another menu link plugin ID (system.admin_reports is the Reports section), and the link joins its parent’s menu. Without a parent, the link lands at the top level of the Tools menu unless you set menu_name (for example admin). Rebuild caches after editing the file: drush cr.
Add tabs with local tasks
Section titled “Add tabs with local tasks”Tabs are local tasks, declared in MODULE.links.task.yml. The base_route ties a group of tabs together; the task whose route_name equals its base_route is the default (leftmost) tab.
# Local tasks render as the tabs at the top of a page. The default task# must point at the same route as the page it tabs; siblings add more tabs.howto_menus.overview: title: 'Overview' route_name: howto_menus.overview base_route: howto_menus.overview
howto_menus.settings: title: 'Settings' route_name: howto_menus.settings base_route: howto_menus.overview weight: 10Two or more tasks sharing a base_route render as tabs on every page in that group. A single task renders nothing, so tabs always come in sets.
Add an action button with a local action
Section titled “Add an action button with a local action”Action buttons, the “Add” buttons above a listing, are local actions in MODULE.links.action.yml. appears_on lists the routes the button shows on.
# Local actions render as the action buttons (e.g. "Add") above a page.# appears_on lists the routes the button shows up on.howto_menus.add_article: title: 'Add article' route_name: node.add route_parameters: node_type: article appears_on: - howto_menus.overviewGenerate links dynamically with a deriver
Section titled “Generate links dynamically with a deriver”When the set of links is data-driven, one link per content type, say, a deriver turns one base definition into many. Name the deriver class in the base link:
# A derivative-based link: one base entry, many links at runtime. The# deriver returns one definition per content type, merged onto this base.howto_menus.add: title: 'Add content' parent: system.admin_content deriver: \Drupal\howto_menus\Plugin\Derivative\ContentTypeAddLinksThe deriver returns one definition per variant, merged onto the base. The plugin IDs become BASE:DERIVATIVE (here howto_menus.add:article).
<?php
declare(strict_types=1);
namespace Drupal\howto_menus\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;use Drupal\Core\Entity\EntityTypeManagerInterface;use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;use Drupal\Core\StringTranslation\StringTranslationTrait;use Symfony\Component\DependencyInjection\ContainerInterface;
/** * Derives one menu link per content type. * * A deriver turns one base plugin definition into many at runtime. The base * definition in *.links.menu.yml names this class, and * getDerivativeDefinitions() returns one definition per variant. */final class ContentTypeAddLinks extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/** * Constructs a ContentTypeAddLinks deriver. */ public function __construct( private readonly EntityTypeManagerInterface $entityTypeManager, ) {}
/** * {@inheritdoc} */ public static function create(ContainerInterface $container, $base_plugin_id): static { return new static( $container->get('entity_type.manager'), ); }
/** * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition): array { $node_types = $this->entityTypeManager->getStorage('node_type')->loadMultiple(); foreach ($node_types as $id => $node_type) { $this->derivatives[$id] = [ 'title' => $this->t('Add @label', ['@label' => $node_type->label()]), 'route_name' => 'node.add', 'route_parameters' => ['node_type' => $id], ] + $base_plugin_definition; } return $this->derivatives; }
}Verify
Section titled “Verify”-
Rebuild caches and visit the page:
drush cr, then/admin/reports/howto-menus. The tabs and the action button appear, and the link shows under Reports in the administration menu. -
Confirm the deriver from the command line:
Terminal window drush php:eval "var_dump(\Drupal::service('plugin.manager.menu.link')->getDefinition('howto_menus.add:article') !== NULL);"prints
bool(true)once at least one content type exists.
Variations
Section titled “Variations”Editor-managed links
Section titled “Editor-managed links”For links site builders should control, use content menu links (the menu_link_content entity) created in the UI or with the configuration management workflow, not the plugin links above.
Contextual and other link types
Section titled “Contextual and other link types”Contextual links (MODULE.links.contextual.yml) and menu link parameters follow the same YAML shape. See the menu API topic for the full set.
Related
Section titled “Related”- Routes and controllers for the routes these links target.
- Plugins for how derivers and plugin discovery work.
- The
menu API topicon api.drupal.org.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.