Thanks for taking the time to contribute to Yoast SEO! Before filing a bug report, feature request, or pull request, please read the guidelines below.
This file is the canonical contributor guide for this repository. It is written for both humans and AI coding tools. The repo-root AGENTS.md adds a small set of behaviours specific to AI agents on top of the rules here; the PULL_REQUEST_TEMPLATE.md carries the detailed changelog and label rules.
- How to use GitHub
- Security issues
- I have found a bug
- I have a feature request
- I want to create a patch
- Additional resources
We use GitHub exclusively for well-documented bugs, feature requests, and code contributions. Communication is always done in English.
For support with Yoast SEO we have the following channels:
- Yoast Knowledge base
- Support forums on WordPress.org
If you have purchased one of our premium plugins you will receive personal support by email — see your purchase email for details.
Please do not report security issues on GitHub. Follow our security program instead — see yoast.com/security.txt for the canonical contact details — so we can handle them quickly and responsibly.
Before opening a new issue, please:
- update to the latest versions of WordPress and the Yoast SEO plugins.
- search for duplicate issues to avoid filing the same report twice. If an open issue already exists, please comment on it.
- check our knowledge base — many common errors are documented there with possible solutions.
- pick the matching GitHub issue form (Bug report, Feature request, Design implementation, etc.) and fill in every section.
- check for plugin and theme conflicts and include your findings.
- check for JavaScript errors in your browser's console and include any output.
- include everything needed to understand and reproduce the problem — screenshots, clear reproduction steps, plugin and theme versions, and any relevant logs — but stay focused. A tight, reproducible report is easier to triage than a long narrative with unrelated context or commentary.
Before opening a new issue:
- search for duplicate issues to avoid filing the same request twice. If an open request already exists, please add your thoughts there.
- pick the Feature request issue form and explain why you think this feature is worth considering.
Community patches, localizations, bug reports, and contributions are very welcome — they help Yoast SEO remain the #1 SEO plugin for WordPress.
Yoast SEO is licensed under GPL-2.0-or-later. By opening a pull request you confirm that your contribution is offered under the same license. Before contributing code, make sure that:
- You wrote the code yourself, or you have the right to relicense it under GPL-2.0-or-later.
- You have not copied code from sources whose license is incompatible with GPL-2.0-or-later (for example proprietary code, CC-licensed snippets that restrict commercial use, or GPL-3.0-only code).
- If you have reused code from a GPL-2.0-compatible source (MIT, BSD, public domain, Apache-2.0 where compatible, etc.), you have preserved the original copyright notice and license header, and noted the provenance in the commit message.
- You have not included code whose licensing status is unclear — including AI-generated code whose training or output terms you have not verified as compatible.
If you are unsure whether a piece of code is safe to include, ask in the issue or PR before opening it for review.
- PHP 7.4 or newer (8.x is supported too).
- The latest two major WordPress versions are officially supported.
- The plugin is a single PHP plugin plus a Yarn workspaces / Lerna monorepo of JS packages under
packages/*.
The top-level folders you will touch (or explicitly avoid):
| Path | Purpose | Editable? |
|---|---|---|
src/ |
Namespaced PHP. All new backend work lives here. | Yes |
packages/js/ |
The primary JS bundle shipped with the plugin. | Yes |
packages/* |
Shared JS/TS libraries (UI library, analysis engine, etc.). | Yes |
tests/ |
PHPUnit tests (unit + WP integration). | Yes |
config/ |
Grunt, webpack, php-scoper, wp-env, composer actions, build scripts. | Yes, with care |
docs/ |
Repo-local developer notes. | Yes |
admin/, inc/ |
Legacy non-namespaced PHP. | Maintenance only — see below |
lib/ |
Low-level utilities (ORM, migrations). Touch only when needed. | Maintenance |
src/generated/ |
Compiled Symfony DI container. Gitignored, not committed; the release-build pipeline bundles it into the shipped artifact. | Never hand-edit — regenerate via composer compile-di |
vendor/, vendor_prefixed/ |
Composer dependencies (prefixed copies are scoped with YoastSEO_Vendor). |
Never hand-edit |
build/, js/dist/, css/dist/, artifact/, languages/, node_modules/ |
Generated or distribution artifacts. | Never hand-edit |
wp-seo.php, wp-seo-main.php, index.php |
Plugin bootstrap. Change only when the bootstrap itself needs to change. | With care |
Two organisational patterns coexist inside src/:
-
Concept-based (older, still valid for extensions of existing features). Code is grouped by its role in the plugin:
src/integrations/,src/conditionals/,src/generators/,src/actions/,src/presenters/,src/surfaces/,src/helpers/, etc. When you are expanding a feature that already follows this layout — for example adding a new Schema piece, a new admin integration, or a new REST action — keep it in the matching folder and follow the surrounding patterns. Seesrc/README.mdfor a description of each concept folder. -
Feature-folder with onion layers (preferred for new self-contained features). A new feature gets its own directory under
src/with four subfolders:src/<feature>/ ├── domain/ Pure business logic — no WordPress, no I/O. ├── application/ Use cases, orchestration, DTOs, interfaces. ├── infrastructure/ WordPress/DB/HTTP adapters; repository implementations. └── user-interface/ Integrations, REST routes, WP-CLI, presenters.Live examples in the repo:
src/dashboard/,src/introductions/,src/llms-txt/,src/schema-aggregator/,src/ai-*/.
- Domain depends on nothing outside itself (no WP functions, no framework, no infrastructure).
- Application depends on Domain only, and defines interfaces for anything it needs from Infrastructure.
- Infrastructure and User-Interface depend on Application and Domain — never the other way round.
- Cross-layer wiring happens through constructor injection via the Symfony DI container.
These contain pre-namespace, pre-onion code. Treat them as maintenance-only: fix bugs, keep them compatible, but do not add new features there. When a legacy class needs significant changes, consider whether the work justifies moving (or extracting a new service for) the affected responsibility into src/.
The plugin uses a compiled Symfony DI container. Services are auto-wired from src/ based on their type hints.
- Run
composer compile-diwhenever a change would affect the container. In practice this means:- You added a new class under
src/. - You changed a constructor signature of an existing class.
- You added/removed a service definition or tag in
config/dependency-injection/(if applicable).
- You added a new class under
- The regenerated files under
src/generated/(container.php,container.php.meta) are gitignored — do not commit them. Every environment rebuilds its own container:composer installandcomposer updatere-runcompile-divia thepost-autoload-dumphook, so local dev, CI, and the release build each regenerate it fresh. The compiled container is included in the shipped release artifact; it is just never part of the Git history. - If CI fails during DI compilation after your changes, check that your new or modified classes resolve correctly via auto-wiring (type-hinted constructor arguments, correct namespaces, etc.).
All commands are run from the repo root.
| Command | What it does |
|---|---|
composer install |
Install PHP dependencies (also compiles DI and prefixes vendor packages). |
composer lint |
PHP parse-error check across the repo. |
composer lint-branch |
Same, but only for files changed on the current branch. |
composer check-cs |
Run phpcs with the Yoast ruleset (errors only). |
composer check-branch-cs |
Run phpcs against the files changed on the current branch. |
composer fix-cs |
Auto-fix fixable phpcs violations. |
composer test |
Run PHPUnit unit tests (no WP, no coverage). |
composer test-wp-env |
Run WP integration tests inside the wp-env Docker environment (preferred way to run integration tests locally). |
composer coverage / coverage-wp-env |
Same, with coverage. |
composer compile-di |
Rebuild the DI container. |
composer generate-migration |
Scaffold a new DB migration under src/config/migrations/. |
composer generate-unit-test |
Scaffold a unit-test file for a fully-qualified class name. |
Coding standards are enforced by the Yoast Coding Standard (yoast/yoastcs) — a superset of WordPress Coding Standards — plus parallel-lint for syntax. Run composer check-branch-cs before opening a PR.
| Command | What it does |
|---|---|
yarn install |
Install JS dependencies for the whole workspace. |
yarn start |
Webpack dev build with watch for the main JS bundle. |
yarn build |
Run the build script of every workspace package. |
yarn lint |
Lint every workspace package plus the repo-level tooling. |
yarn test |
Run every workspace's test script (Jest). |
grunt build / grunt build:dev |
Full plugin build (assets + images + i18n) via Grunt. |
Each package under packages/* has its own package.json with local scripts; prefer running those directly when iterating on a single package.
- Every PR that changes PHP behaviour should ship unit tests. Place them under
tests/Unit/…mirroring the path of the class under test. - Integration tests (those that boot WordPress) live under
tests/WP/…and run viacomposer test-wp-env, which starts an isolated WP in Docker through@wordpress/env. - Test one method per test class where practical; share setup via abstract base classes or traits (examples already exist under
tests/Unit). - JS tests use Jest and live inside the relevant
packages/*/tests/folder. - If you cannot run the integration tests in your environment, say so explicitly in the PR description rather than skipping them silently.
- Follow the existing code. When two styles look plausible, match the file you are editing.
- PHP: Yoast CS (
yoast/yoastcs). Class files use snake_case with underscores (e.g.Introductions_Collector) — this is Yoast's convention even though it differs from PSR. Namespaces live underYoast\WP\SEO\…. - JavaScript/TypeScript: the shared
@yoast/eslint-config, plus per-package overrides where they exist. - Comments: document why, not what. End every inline comment with a full stop.
- Don't add features, scaffolding, or abstractions the task doesn't need.
- Fork the repository and create your branch from
trunk.trunkis the active development branch and the default branch on GitHub — every PR should target it unless a maintainer asks otherwise. Do not targetmain: that branch tracks the latest released version and is not where new work goes. When the work tracks a GitHub issue, name your branch<issue-number>-<short-description>(e.g.2056-paid-upgrades). - Make your changes on your fork.
- Follow the Yoast Coding Standards (a superset of the WordPress Coding Standards).
- Document any new functions, actions, and filters following the PHP inline-documentation standards.
- Write tests. We expect every PR that changes PHP behaviour to ship unit tests — see the commands in the checklist below.
- Use the Conventional Commits format for commit messages (e.g.
fix: …,feat(dashboard): …). Prefer atomic commits whenever practical — each commit should represent a single logical change that can be reviewed or reverted on its own. If your PR mixes unrelated changes (e.g. a bugfix plus a refactor plus a tooling tweak), split them into separate commits or ideally separate PRs. Combining closely coupled changes into one commit is fine when splitting would be artificial. - Push your branch and open a pull request against
trunk. Use the pull request template at.github/PULL_REQUEST_TEMPLATE.mdand fill in every section — even for small changes. - Keep the PR description focused. Fill every required section of the template — aim for what the reviewer actually needs. For Context and Relevant technical choices, brevity is welcome but not enforced. Test instructions should be concrete steps, not essays. Link to the epic, issue, or design doc rather than restating them.
Run these checks locally and make sure each one is clean. CI will run the same checks — catching issues locally is faster and easier than fixing them afterwards on a review cycle.
composer test— the unit test suite must pass.composer test-wp-env— the WordPress integration tests (Docker via@wordpress/env) must pass if your change touches code that has or needs WP integration coverage.composer check-branch-cs— checks the Yoast coding-standard ruleset against the files you changed on this branch. It must report no new errors or warnings introduced by your branch. Usecomposer fix-csto auto-fix what it can, and address the rest by hand.composer lint-branch— PHP parse-error check on the files changed on this branch.- For changes under
packages/*orjs/: runyarn lintand the relevant package'syarn test. - If your change adds or edits any image or SVG (
.png,.jpg,.gif,.svg— primarily underimages/orsvn-assets/): rungrunt build:images(or plaingrunt build, which includes it) and commit the resulting optimised files. The release pipeline re-runsimageminduring artifact creation, and the build fails if the committed images aren't already optimised.
Coverage: every PR should increase test coverage, or at minimum keep it flat. In practice this means the code you add should come with tests that exercise it. CI reports the coverage delta on the PR — if coverage drops, explain in the PR description why it was not possible to add tests for the new code (for example: pure wiring code that can only be exercised through a full WordPress boot, or a third-party API call that is impractical to mock).
If a check fails or you need to skip one (e.g. you can't run Docker locally for test-wp-env), say so explicitly in the PR description so reviewers know what still needs validating.
Every PR needs a changelog entry in the Summary section of the PR body and a changelog label on the PR itself:
- Write one bullet describing the change in present tense, 3rd person singular, ending with a full stop. For bugfixes, describe the incorrect behaviour followed by the condition that triggered it, in clear past tense, avoiding hypothetical or nested conditionals (e.g.
Fixes a bug where X happened when YorFixes a bug where X was caused by Y). SeePULL_REQUEST_TEMPLATE.mdfor the full grammar and examples. - Keep each bullet to one short sentence. If you need commas, semicolons, or colons to chain clauses together, the extra context belongs in Context or Relevant technical choices — not in the bullet.
- Attach one of:
changelog: bugfix,changelog: enhancement,changelog: other,changelog: non-user-facing. - If the change also affects another Yoast repo or package, add an extra bullet prefixed with
[<repo-or-package>]. The PR template explains this in more detail.
The community-patch label is applied automatically to external contributions — you do not need to add it yourself. Milestones are set by the maintainer who merges your PR.
Issues tagged with the patch welcome label are enhancements we see value in but have not prioritised. If you'd like to take one on, write a patch and we'll review it.
Make sure your problem doesn't already have a ticket by searching the existing issues. If you can't find anything matching, please open a new issue.