You've now seen how Drupal recognizes a module (.info.yml) and how it discovers a menu link. In this lesson we're switching to a new example module — block_example — and a new topic entirely: Blocks. Blocks are the reusable, placeable chunks of content you see all over a Drupal site: the main navigation, a "Who's online" widget, a custom promo box in the sidebar. Over the next four lessons you'll learn exactly how they're built.
Before we can look at an actual block plugin, though, we need the same two pieces of scaffolding every module needs: an .info.yml file, and this time also a .routing.yml file that gives the module its own landing page. Think of this lesson as "setting the stage" — by the end of it you'll be able to visit the Block Example module's own page and confirm everything is wired up correctly before we write our first block.
What you'll learn in this lesson
- How a
.routing.ymlfile connects a URL to a PHP controller method - What each part of a route definition — path, controller, title, permission — actually does
- How a module declares dependencies on other modules it needs (like Drupal core's own Block module)
- Where to go in the admin UI to see the blocks this module will soon provide
The source file
Path: modules/block_example/block_example.routing.yml
block_example.description:
path: '/examples/block-example'
defaults:
_controller: '\Drupal\block_example\Controller\BlockExampleController::description'
_title: 'Block Example'
requirements:
_permission: 'access content'
How it works
The route name: block_example.description
The top-level YAML key is the machine name of the route — Drupal's internal identifier for it. You'll see this name used elsewhere in code, for example when generating a link with Url::fromRoute('block_example.description'). The naming convention is always module_name.route_identifier, which keeps every route in every module uniquely namespaced so two modules can never accidentally define the same route name.
path
path: '/examples/block-example'
This is the actual URL Drupal will match — visit /examples/block-example on your site and this route fires. The leading slash is required. Routes are registered into Drupal's routing table whenever the cache is rebuilt, so a brand-new route only becomes reachable after that happens (Drupal does this automatically when you enable a module).
defaults: _controller and _title
defaults:
_controller: '\Drupal\block_example\Controller\BlockExampleController::description'
_title: 'Block Example'
_controller is a fully-qualified reference to the PHP class and method that will actually handle the request — in the format \Drupal\{module}\Controller\{Class}::{method}. When someone visits the path above, Drupal instantiates BlockExampleController and calls its description() method, which returns a render array for the page content.
_title sets the page's <title> tag and heading. It's a static fallback — a controller can override it dynamically if it needs to, but for a simple informational page like this one, a fixed string is all that's needed.
requirements: gating access
requirements:
_permission: 'access content'
Before Drupal ever calls your controller, it checks requirements. If they aren't met, the visitor gets a 403 Forbidden response and your controller code never runs at all. _permission is the simplest access rule: it names a Drupal permission the current user must hold. 'access content' is granted to nearly every role by default — including anonymous visitors — so this page is public. Other real-world routes might use something like 'administer site configuration' to restrict a route to administrators only.
The other half: block_example.info.yml
You already know most of this file from Lesson 1 — it's the same pattern, just for a different module:
name: Block Example
type: module
description: Demonstrates how a module can define blocks.
package: Example modules
core_version_requirement: ^10.3 || ^11.0
dependencies:
- drupal:block
- drupal:node
- examples:examples
# Information added by Drupal.org packaging script on 2024-09-09
version: '4.0.4'
project: 'examples'
datestamp: 1725887893
The one thing worth pausing on here is the new dependency: drupal:block. This tells Drupal that before block_example can be enabled, the core Block module must already be enabled — because that's exactly where the plugin manager, the BlockBase class, and the block-placement admin UI all live. Every block plugin we write in the coming lessons will build directly on top of what that dependency guarantees is available.
See it for yourself
With block_example enabled, head to Structure → Block layout (/admin/structure/block) and click "Place block" on any region. Search for "Example" — you should see three block plugins available: Example: empty block, Example: uppercase this please, and Example: configurable text. We'll build and place each of these ourselves over the next three lessons.
Quick check: if you saw a 403 error visiting
/examples/block-exampleas a logged-out visitor, which line in the YAML would you look at first? If you said therequirementssection, you're already thinking like a Drupal developer.
Key takeaways
- Every Drupal module requires an
.info.ymlfile declaring itsname,type,core_version_requirement, anddependencies— without it, Drupal won't recognize the module at all. - The
dependenciesarray enforces hard prerequisites — listingdrupal:blockguarantees the Block plugin manager andBlockBaseclass are available before any of this module's block plugins load. - A
.routing.ymlfile maps URL paths to PHP controller methods using aroute_name: path / defaults / requirementsstructure, rebuilt whenever the cache clears. - The
_controllervalue must be a fully-qualified\Drupal\{module}\Controller\{Class}::{method}string — this is how Drupal resolves and instantiates the right class. requirements._permissionis the simplest access control in Drupal routing: it gates the entire route behind one named permission, checked before your controller ever runs.
Coming up next
Scaffolding's done — now for the fun part. In the next lesson we'll open up the two simplest block plugins in this module and see exactly what a block plugin class looks like: the annotation that registers it, the one method it needs to override, and why an empty block quietly disappears from the page instead of rendering as a blank box.