Think of a Drupal module the way a woodworker thinks of a finished piece of furniture leaving the workshop: it doesn't matter how solid the joinery is if the piece falls apart the moment someone else tries to move it. A module you built for one site, full of hardcoded assumptions and no documentation, is furniture that only survives in the room it was built in. This lesson is about the structural changes that let a module survive contact with someone else's Drupal site — the first real step toward sharing it on Drupal.org.
What you'll learn in this lesson
- The directory structure Drupal.org's packaging and review process expects
- Why `.info.yml` metadata matters more once other people are installing your module
- The licensing and coding-standard requirements a module needs to clear before it can go public
- How to separate "things that work on my site" from "things that work on any site"
The directory shape reviewers expect
A contributed module's file layout isn't arbitrary — it follows the same convention every Drupal core and contrib module uses, because Drupal's own tooling (the extension system, the coding-standards linter, the packaging script) all assume it:
my_module/
├── my_module.info.yml # Required: identifies the module to Drupal
├── my_module.module # Procedural hooks, if any
├── my_module.install # schema/update hooks, if any
├── src/
│ ├── Controller/
│ ├── Form/
│ └── Plugin/
├── config/
│ ├── install/ # default configuration shipped on install
│ └── schema/ # config schema so CMI can validate/export it
├── tests/
│ └── src/
├── README.md
├── LICENSE.txt # required for drupal.org
└── composer.json # optional but expected for real dependencies
Nothing here is new API you haven't already used across this course — `src/`, `config/`, `tests/` are exactly what you've been building lesson by lesson. What's new is the expectation that they're complete: a `tests/` folder with no actual tests, or a `config/schema/` file that's missing keys you added later, reads as unfinished to a reviewer (and to Drupal itself, which will complain about untyped config).
.info.yml: the metadata that matters once you're not the only user
name: My Module
type: module
description: A one-line, honest description of what this module actually does.
package: Custom
core_version_requirement: ^10.3 || ^11.0
dependencies:
- drupal:node
- drupal:field
You've written dozens of these by now, but two habits change once a module leaves your own site. First, description stops being a note to yourself and becomes the one sentence a stranger reads to decide whether to install your module — write it for them, not for you. Second, dependencies needs to be genuinely complete: on your own site, a missing dependency might already happen to be enabled from something else you installed; on someone else's site, it won't be, and your module will fatal-error the moment it tries to use a class or service that isn't there.
Licensing and coding standards: the gate, not a suggestion
Drupal.org requires every project to ship under the GPL v2 (or later), signaled by a LICENSE.txt file in the module root — copy it verbatim from an existing contrib project rather than writing your own. Before you ever submit for review, run the Coder module's sniffer against your code:
vendor/bin/phpcs --standard=Drupal,DrupalPractice web/modules/custom/my_module
This isn't cosmetic. Drupal.org's automated review bot runs the same check on every submission, and a module full of tabs-vs-spaces violations or missing docblocks gets flagged before a human reviewer even looks at the logic.
See it for yourself
Look at the top-level structure of the Examples project itself (the same module you've been reading source code from all course long) — every one of its ~20 sub-modules follows exactly this shape: its own .info.yml, its own src/, and a shared top-level README.md explaining how the whole collection fits together. That's not a coincidence; it's the same convention you're now applying to your own module.
Quick check: your module works perfectly on your own site but you never wrote a `LICENSE.txt` or ran `phpcs` against it. Will Drupal.org's automated review catch this before a human reviewer sees it? Yes — both the missing license file and coding-standard violations are checked automatically as part of the project application process, before any human review happens.
Key takeaways
- A contributable module's directory structure isn't a style preference — Drupal's own extension system, config schema validation, and Drupal.org's packaging script all assume the standard layout.
descriptionanddependenciesin.info.ymlneed to be written for a stranger's site, not your own — an incompletedependencieslist that "happened to work" locally will fatal-error elsewhere.- Every Drupal.org project needs a
LICENSE.txt(GPL v2 or later) and must pass the Coder module's `phpcs --standard=Drupal,DrupalPractice` check before human review even begins. - Hardcoded IDs, paths, and site-specific machine names are the most common reason a module that "works for me" breaks the moment someone else installs it — audit for these before packaging.
Coming up next
Structure alone doesn't tell anyone how to actually use what you built — that's what a good README does. Next, you'll write the README.md that turns your well-structured module into something a stranger can actually install and understand.