Right, let’s talk menus. Not the laminated kind you get at a diner, but the ones that hold your entire application together. This is where we move from building individual pages to building a system that users can actually navigate. Drupal’s menu system is brilliantly powerful, which is a polite way of saying it can be a bit of a labyrinth if you don’t know what you’re doing. We’re going to define them in our module’s .yml files, because that’s how we roll in the land of serious Drupal development.

The first thing to wrap your head around is that in Drupal, a “menu” is just a container for links. The actual navigation structure—what appears in a block in a specific region—is a separate thing. We define the menu itself, then we populate it with links, and then we tell a block to show those links. Don’t worry, it’ll make sense.

The .links.menu.yml file

This is where you define new menu trees. The filename should follow the pattern mymodule.links.menu.yml. Think of this file as you shouting into the void, “Hey Drupal, here is a new menu I invented!” You’re not putting links into it yet; you’re just creating the empty bucket.

# In mymodule.links.menu.yml
main:
  title: 'Main navigation'
  description: 'The primary menu for the site, typically displayed in the header.'
  menu_name: main
  locked: true

Let’s break this down. The top-level key (main) is the machine name for your menu. The title is what a human will see in the admin UI. The description is helpful for your content editors. The menu_name is… also the machine name. Yes, it’s redundant. I don’t know why it’s required either. Just copy the key down here and move on with your life. locked: true is a best practice; it means content editors can’t accidentally delete this menu from the admin UI, which is almost always what you want for a structural menu like this.

The .links.task.yml & .links.action.yml files

These are special siblings to the menu file and they’re where Drupal’s magic (and confusion) really lives. They don’t add items to a menu like “Main navigation”; instead, they add items to contextual menus.

Task links are the local tasks—those tabs you see at the top of a node, like “View” and “Edit”. You define the hierarchy here.

# In mymodule.links.task.yml
entity.node.canonical:
  title: 'View'
  route_name: entity.node.canonical
  base_route: entity.node.canonical

entity.node.edit_form:
  title: 'Edit'
  route_name: entity.node.edit_form
  base_route: entity.node.canonical
  weight: 10

The base_route is the key. It tells Drupal, “Attach these tasks to the page defined by this route.” The weight controls the order.

Action Links are for one-off actions, typically placed in a dropdown under a “Operations” button. Adding a new content type might be an action.

# In mymodule.links.action.yml
mymodule.add_article:
  title: 'Add Article'
  route_name: node.add
  route_parameters:
    node_type: article
  class: '\Drupal\Core\Menu\MenuLinkDefault'

Notice the route_parameters here. This is how you pass specific values to a general route.

The Big One: .links.yml

This is the workhorse. This file (typically mymodule.links.yml) is where you actually place links into the menus you defined. This is you filling the bucket.

# In mymodule.links.yml
mymodule.dashboard:
  title: 'Dashboard'
  description: 'Go to your personal dashboard.'
  menu_name: main
  route_name: view.dashboard.page_1
  weight: -10
  parent: ''

mymodule.reports:
  title: 'Reports'
  menu_name: main
  route_name: <front>
  parent: ''

mymodule.sales_report:
  title: 'Sales Data'
  menu_name: main
  route_name: mymodule.sales_report
  parent: 'mymodule.reports'

The most important concept here is the parent key. An empty string ('') means it’s a top-level item. To nest an item, you set its parent key to the machine name of another link in the same menu from this file. The weight dictates the order, with lower numbers appearing first. I set the dashboard to -10 to force it to the very front.

Common Pitfalls and The “Gotchas”

  1. The Caching Nightmare: The menu system is cached, aggressively. You will write a perfect links.yml file, clear the cache, and see nothing. Then you will clear it again, and again, and then finally it will appear. Then you will make a change and it won’t disappear. Get used to running drush cr more often than you blink. This isn’t a bug; it’s a feature with a very serious, very annoying face.

  2. Parent Machine Name Mismatch: This is the number one cause of “my link didn’t nest!”. The parent key must be the exact machine name of the parent link as you defined it in this same .links.yml file, not the route name. If your parent link is keyed as mymodule.reports, you must parent to mymodule.reports.

  3. Route vs. Internal Path: You are defining these by their route name (e.g., view.dashboard.page_1), not the internal path (e.g., /dashboard). This is a modern, robust practice. If you find yourself trying to use a path, you’re almost certainly doing it wrong.

  4. The Weight Isn’t Absolute: The weight only orders items within their level of the hierarchy. A child item with a weight of -100 will still appear below its parent, no matter how high the parent’s weight is. You’re ordering siblings, not the entire tree.

Defining menus in YAML is the clean, maintainable, deployable way to build your site’s navigation structure. It separates configuration from content, which is a core Drupal philosophy. It might feel abstract at first, but once you get the hang of the parent/weight system, you’ll be structuring complex navigations with grim, silent efficiency. Now go clear your cache. Again.