Drupal Menu Links Explained: Connecting Routes to Navigation with links.menu.ymlfor Drupal 11 , and 10

Last updated :  

A route, a permission, and a controller are all it technically takes to serve a page — but nobody can visit a page they don't know exists. So far the only way to reach these pages has been typing the exact URL by hand. This lesson fixes that: page_example.links.menu.yml, the file that puts a real, clickable link to your page inside Drupal's navigation.

What is a menu link, and why is it its own file?

By now you've probably noticed a pattern in how Drupal modules are organized: one concern, one file. Routing decides which URL maps to which code. Permissions decide who's allowed in. And menu links decide whether — and where — a clickable entry for that route shows up in the site's navigation. All three can point at the exact same route, but each is declared, and can change, independently of the other two.

What you'll learn in this lesson

  • How a menu link connects to a route by machine name, not by hardcoding a URL
  • How to nest menu links into parent/child hierarchies
  • Why one of this module's three routes deliberately has no menu link at all
  • Which menu a link lands in by default, and how to change that

The source file

Path: modules/page_example/page_example.links.menu.yml

page_example.description:
  title: Page Example
  route_name: page_example.description
  expanded: TRUE

page_example.simple:
  title: Simple - no arguments
  route_name: page_example.simple
  parent: page_example.description

# We can't define a menu link for the page_example_arguments route, because it
# requires path arguments.

How it works

The mandatory file name

Just like the other YAML files you've met so far, this one has a fixed, non-negotiable name: [module_name].links.menu.yml, sitting in the module's root folder next to .info.yml. Drupal discovers it automatically the next time the cache rebuilds — no PHP registration required. The module-name prefix in the filename is how Drupal knows which module these links belong to.

The machine name (again)

You've now seen this exact pattern three lessons in a row: a top-level YAML key acting as a globally-unique machine name. For menu links, the convention is <module_name>.<descriptive_id>, and this machine name matters beyond just being an identifier — you'll see below that other menu links can reference it directly as a parent.

title: the visible label

title: Page Example

Exactly what it sounds like: the text a visitor actually sees and clicks on in the menu. Like every other user-facing string you've met in this course, it's passed through Drupal's translation system automatically.

route_name: connecting the link to a route

route_name: page_example.description

This is the key that actually makes the link work — it references a route by its machine name, the exact same machine name you saw defined in page_example.routing.yml. When a visitor clicks this link, Drupal resolves that route name into its current URL and navigates there. Notice what's not here: a hardcoded path. If the route's URL ever changed, this menu link would keep working without a single edit, because it points at the route by name, not by address.

Fail fast: if route_name points at a route that doesn't exist (a typo, or a route that was since removed), Drupal throws an error the moment the cache rebuilds — not silently at runtime when someone clicks the link. That's a deliberately loud failure, and a good one to get used to seeing while you're learning.

expanded: showing children by default

expanded: TRUE

When a menu link has children (more on that next), expanded: TRUE makes those children visible in the menu immediately, without the visitor needing to hover or click to reveal them. This is especially useful for a link that's really acting as a grouping label rather than a destination in its own right. Leave it out, and the default is FALSE — children stay tucked away until interacted with.

parent: building a menu hierarchy

parent: page_example.description

This is where the machine name you saw a moment ago comes back into play. Setting parent to another link's machine name nests this link underneath it in the menu tree — here, "Simple - no arguments" becomes a child of "Page Example." The parent value can point at a link from any module, not just your own, which is how contributed and custom modules can add items into menus that core or other modules originally created. Omit parent entirely, and a link sits at the top level of whatever menu it's assigned to.

Why the arguments route has no menu link

# We can't define a menu link for the page_example_arguments route, because it
# requires path arguments.

This comment is pointing at a real, structural limitation, not just a design choice. A links.menu.yml entry has to resolve to one single, static URL — but the arguments route's path is /examples/page-example/arguments/{first}/{second}, which has no fixed value until someone actually visits it with real numbers filled in. There's no one correct static URL to point a menu link at. For a route like this that genuinely needs to appear in a menu, you'd reach for a PHP MenuLinkDefault plugin, or generate menu links programmatically — techniques well beyond what a beginner needs on day one, which is exactly why the Examples module simply leaves this route out of the menu.

Which menu does this land in?

Neither entry above specifies a target menu, so by default, links defined this way are placed into Drupal's built-in Tools menu (machine name tools). To target a different menu — say, the site's main navigation — you'd add a menu_name key:

menu_name: main

Cache rebuilds, again

Like every YAML discovery file you've met so far, links.menu.yml is read during the cache/container build phase. Add or change an entry, and you need to run drush cr (or clear caches from /admin/config/development/performance) before Drupal notices. If you're ever staring at a menu wondering why your new link isn't showing up, this is the very first thing to check.

See it for yourself

Visit /examples/page-example on your own DDEV site and look in the Tools menu block. You should see "Page Example" and, nested underneath it, "Simple - no arguments" — exactly the parent/child relationship declared in the YAML above.

The Tools menu block showing Page Example and Simple - no arguments links

Quick check: if you wanted the arguments route to show up somewhere clickable for visitors after all, would you add it to links.menu.yml with a placeholder value baked into the path? (Answer: no — a static menu link can't represent a route with dynamic segments at all. You'd need a PHP-based approach, not a YAML entry with a made-up example URL.)

Key takeaways

  • The file is named [module_name].links.menu.yml and needs a cache rebuild to take effect — no PHP registration required.
  • Each entry uses a globally-unique machine name (<module_name>.<id> convention) and connects to a route via route_name, referencing the route by name rather than hardcoding its URL.
  • parent nests one link under another by machine name, and can reference a link from any module — this is how menu hierarchies get built across module boundaries.
  • expanded: TRUE shows a parent link's children by default, without requiring a hover or click.
  • Static menu links only work for routes with no dynamic path arguments — routes with {placeholder} segments need a PHP-based MenuLinkDefault plugin instead.
  • With no menu_name specified, links land in Drupal's built-in Tools menu by default.

Coming up next

Every file you've met so far has been about wiring — routes, permissions, navigation. The very last piece of this module's puzzle is presentation: how the actual HTML that visitors see gets built using Twig, Drupal's templating language. That's the final lesson in this topic, and it closes the loop on everything you've learned about page_example.