Configure pathauto alias pattern
Goal
Provision a Pathauto URL-alias pattern (pathauto.pattern.{id}) for one bundle or vocabulary — a correct token pattern, scoped to the target with a selection-criteria bundle condition carrying the right context_mapping, then aliases generated for existing content — so the target gets clean, deterministic URLs. This is the narrow capability: just the alias pattern, not a full SEO foundation.
Opinion
This recipe is scoped to aliases only. If the task is a whole SEO build (metatags, JSON-LD, sitemap, robots, canonical), route to the drupal_seo_foundation recipe, which owns pathauto as one layer of many behind a broader input contract. Reach here when the need is genuinely "just give this bundle a clean URL pattern." Source: guide drupal/seo-geo/pathauto-patterns.
Scope the pattern with a selection-criteria condition, not a bare pattern. A pathauto.pattern.{id} targets an entity type via type (e.g. canonical_entities:node) and is restricted to specific bundles by an entity_bundle condition under selection_criteria. That condition MUST carry context_mapping (node: node) — omitting it raises a ContextException at generation time. Source: guide drupal/seo-geo/pathauto-patterns.
Build the token pattern from stable tokens. Prefer stable, human-meaningful tokens ([node:title], [term:name], a section prefix) over volatile ones; let Pathauto's transliteration and separator settings normalize the result rather than encoding those choices into the pattern. Source: guide drupal/seo-geo/pathauto-patterns.
Pattern weight resolves precedence. When multiple patterns can match one entity, the lower weight wins; set weight deliberately when a bundle-specific pattern must override a catch-all. Source: guide drupal/seo-geo/pathauto-patterns.
What this recipe refuses
- A pattern with a bundle condition missing
context_mapping(ContextException on generate). - Re-implementing the SEO foundation's metatag/schema/sitemap layers — out of scope; defer to
drupal_seo_foundation. - Silently overwriting an existing differing pattern for the same target.
Preconditions
- Drupal 10.4+ or 11.x; the
pathautomodule (withpath/token). - The target bundle or vocabulary exists.
- Config export is in use, so the pattern is deployable.
- DDEV runs the site; the verifier's commands go through
ddev.
Input contract
Generic, source-agnostic, supplied by the caller (adapter skill, human operator, or orchestrator).
pattern:
id: string # machine name for the pattern
label: string
entity_type: string # node | taxonomy_term | user | media …
bundles: # bundles/vocabularies the pattern applies to
- string
pattern: string # token pattern, e.g. '/articles/[node:title]'
weight: integer # precedence; lower wins (default 0)
generate_existing: boolean # generate aliases for existing content (default true)
Sequence
If invoked in dry-run mode, perform all reads and derivations but emit a preview instead of writing config. Dry-run is required.
-
Validate the target and tokens. Confirm the entity type and bundle(s) exist and the tokens in
patternare available for that entity type. Seedrupal/seo-geo/pathauto-patterns. -
Read existing patterns for the target — determine absent / present-and-matching / present-and-differing before writing (see the state-awareness contract).
-
Compose the pattern config —
pathauto.pattern.{id}withtype: canonical_entities:{entity_type}, thepatternstring, andweight. -
Scope it with a selection-criteria bundle condition — an
entity_bundle:{entity_type}condition listingbundles, withcontext_mappingset (e.g.node: node). Omittingcontext_mappingis the ContextException trap. Seedrupal/seo-geo/pathauto-patterns. -
Import config and generate aliases — apply the config, then generate aliases for existing content of the target when
generate_existingis true (Pathauto's bulk-generate for the target entity type). -
Export configuration, rebuild caches, and emit a summary — the pattern created/no-op/conflict, and how many aliases were generated.
Data flow
input: pattern (id, label, entity_type, bundles[], pattern, weight)
generate_existing
reads project state:
pathauto.pattern.* (existing patterns for the target)
pathauto.settings (separator, transliterate, max_length …)
existing bundles/vocabularies + available tokens
applies opinion (guardrails):
aliases-only-scope · condition-with-context_mapping ·
stable-tokens · weight-resolves-precedence
references atomic detail (guides):
drupal/seo-geo/pathauto-patterns
emits:
pathauto.pattern.<id> (type, pattern, weight,
selection_criteria: entity_bundle + context_mapping)
url_alias entries (generated for existing content)
State-awareness contract
The recipe reads existing state before writing. For the pattern config: absent → create; present and matching the resolved spec → skip, log no-op; present and differing → conflict, do not overwrite, request operator review.
Alias generation respects Pathauto's update_action setting (leave existing aliases vs regenerate) — the recipe does not force-overwrite hand-crafted aliases unless the caller opts in. Generating aliases for existing content is safe to repeat; a second run over unchanged content and settings produces no new aliases.
Idempotent: running the recipe twice on identical input and identical project state produces no changes on the second run.
Verifier
Each entry is one command, run from the project root after the recipe ran. It is split on spaces and never run through a shell. A non-zero exit fails the entry, whatever pass says. stdout empty reads standard output only; a non-interactive ddev drush keeps Drush's messages on standard error. A stopped DDEV project prints its start-up text on standard output, so start the site before the verify run. Entries that call .aida/configure-pathauto-alias-pattern/verify.php run the script in ## Files. It prints one line per violation and exits non-zero when it printed any.
verifier: - id: pattern-config kind: config-assert run: ddev drush php:script /var/www/html/.aida/configure-pathauto-alias-pattern/verify.php -- pattern {pattern.id} {pattern.entity_type} {pattern.pattern} {pattern.weight:json} pass: stdout empty - id: bundle-condition kind: config-assert run: ddev drush php:script /var/www/html/.aida/configure-pathauto-alias-pattern/verify.php -- condition {pattern.id} {pattern.entity_type} {pattern.bundles} pass: stdout empty - id: fixture-gets-this-pattern kind: self-fixture run: ddev drush php:script /var/www/html/.aida/configure-pathauto-alias-pattern/verify.php -- fixture {pattern.id} {pattern.entity_type} {pattern.bundles} pass: stdout empty - id: existing-content-aliased kind: config-assert run: ddev drush php:script /var/www/html/.aida/configure-pathauto-alias-pattern/verify.php -- existing {pattern.id} {pattern.entity_type} {pattern.bundles} {generate_existing:json} pass: stdout empty - id: active-equals-export kind: config-assert run: ddev drush config:status pass: stdout empty
What the entries do not prove, and where the proof is:
bundle-condition,fixture-gets-this-patternandexisting-content-aliasedrun once per bundle inpattern.bundles. An empty list reads unknown, not pass.pattern-configreads an absentweightas 0, its default.fixture-gets-this-patternsaves one entity of the bundle and checks two things: Pathauto picks this pattern for it, and an alias was written. Then it deletes the entity, which deletes the alias. It checks no exact alias string, because the transliteration and separator settings shape it.bundle-conditionchecks nothing for an entity type with no bundle key, such asuser. Core derives anentity_bundlecondition only for entity types that have one, so such a pattern has no condition to hold.- A bundle condition without
context_mappingmakes Pathauto log a ContextException and skip the pattern, and bulk generation still exits 0.bundle-conditionnames the missing mapping, and the fixture gets no alias from this pattern. - The fixture sets the label and bundle and nothing else. A bundle that cannot save that way, such as a media type that needs a source file, fails the entry rather than passing it.
existing-content-aliasedreads content, not config, and names up to 20 entities of the bundle with no alias. It skips an entity whose automatic-alias box is off, because bulk generation skips it too. An absentgenerate_existingcounts as true, its default. When it is false, the entry checks nothing.existing-content-aliasedlooks for aliases under the entity type's canonical prefix, such as/taxonomy/term/. Pathauto aliases forum vocabulary terms under its/forum/prefix instead, so the entry reports aliased forum terms as missing and fails closed. Pathauto's own bulk update for terms has the same limit. A pattern with moreselection_criteriathan the bundle, such as a language, makes the entry report entities the pattern never applies to, and it fails closed too.active-equals-exportproves the export is current after Sequence step 6. The state-awareness contract makes a second apply a no-op on that state.
Files
One script, which the consumer writes before the verifier runs and removes after it. Do not edit it or commit it. Drush runs it with Drupal booted and passes the words after -- in $extra.
```php .aida/configure-pathauto-alias-pattern/verify.php