Drupal seo foundation
Goal
Deliver a complete on-page SEO foundation for a Drupal site — per-bundle metatag defaults (title, description, og:, twitter:, canonical), per-bundle Schema.org JSON-LD, an XML sitemap tuned per content priority, pathauto URL patterns for nodes and taxonomy terms, redirect-on-alias-change behavior, google_tag installed with its config excluded from sync, robots.txt managed as config, and 403/404/user/taxonomy_term defaults — composed from the target site's own field inventory, deterministically and idempotently, with the operator's policy supplied once via a typed input contract.
The recipe runs fully unattended. Decisions are encoded in the input contract; when the contract doesn't cover a situation, the recipe halts with a typed reason rather than guessing.
Opinion
Adapt to the target's actual fields, never bolt on. Token chains are composed by walking a priority list against the audited per-bundle field set; first present field wins; if none, the recipe omits the override and lets the parent default fall through. The recipe does not assume a reference site's fields exist; it does not reference fields that the bundle doesn't carry. This stance is invariant — it is expressed by the Input-contract priority_chains walk and the State-awareness contract, not by a defaulting fallback.
Per-bundle defaults, not one-size-fits-all. Each bundle gets its own metatag.metatag_defaults.<entity>__<bundle>.yml declaring the chains relevant to its field set plus the Schema.org type that fits its semantic role. Source: play drupal/best-practices/camoa/metatag-per-bundle.
Schema.org type per semantic role, not auto-derived. Article for content pieces, Product for products, Service for solutions, WebPage for pages and landings, LocalBusiness/Organization on home. The mapping is declared by the operator in the input contract; the recipe does not infer it. Source: guide drupal/seo-geo/schema-types-reference.
No hardcoded JSON-LD without explicit policy. Tags like schema_article_publisher, schema_article_author, schema_product_brand carry hardcoded JSON blobs. The recipe never authors them unless the input contract names each blob and supplies (or accepts) its content. Source: guide drupal/seo-geo/schema-metatag-setup.
Sitemap priority and changefreq per editorial role. Uniform 0.9 / daily across every bundle is a smell — it signals poor quality to search engines. Priority and changefreq are derived from the semantic role declared in the input. Source: guide drupal/seo-geo/xml-sitemap.
Pathauto: one pattern per bundle, taxonomy terms are first-class. Every content type reachable on the public site gets a deterministic pattern. taxonomy_term is enabled in pathauto.settings.yml and individual vocabularies get their own patterns when the input contract names them. Source: guide drupal/seo-geo/pathauto-patterns.
robots.txt is content, not scaffold. Use the robotstxt module so robots.txt is editable via config without redeploying core scaffold. The static robots.txt shipped by Drupal core scaffold is removed via the composer drupal-scaffold.file-mapping exclusion. Source: guide drupal/seo-geo/robots-txt.
google_tag is installed, never config-managed. A GA4 / GTM container is environment-specific, so its config must not live in tracked config sync. When google_tag is in scope the recipe installs the module and adds google_tag to $settings['config_exclude_modules'] in settings.php; it authors no container config. The container is set per environment by the operator, outside config management — the recipe never fabricates or exports a GA4 / GTM ID.
The recipe never makes a judgment call. Every situation is either covered by the input contract (proceed) or it isn't (halt with a typed reason). It doesn't infer defaults; it doesn't have a "smart" fallback. This stance is invariant — it is expressed by the escalation policy in the Input contract and the adversarial Verifier, not by a play.
The chain
The layers are configured in this order, and the order is load-bearing:
- Modules present and enabled.
composer require+drush enfor any missing module the input names. - Global metatag defaults. Sitewide patterns that everything inherits. Source: guide
drupal/seo-geo/metatag-architecture(cascading inheritance). - Per-entity defaults.
node,taxonomy_term,user— the entity-level defaults. Source: guidedrupal/seo-geo/core-meta-tags. - Per-bundle defaults.
node__<bundle>files declaring the chains and Schema.org tags specific to each bundle's field set + semantic role. - Page-context defaults.
403,404,front(if not present). - Schema.org / JSON-LD per bundle. Schema_metatag tags inside the per-bundle defaults from step 4. Source: guide
drupal/seo-geo/schema-metatag-setup. - Metatag entity-type groups. Emit
metatag.settings.ymlentity_type_groupspermetatag_groups_by_bundle(or the default derived fromsemantic_role_by_bundle) so only the relevant tag groups (basic, open_graph, schema_article, schema_person, …) surface on each bundle's edit form. Sequenced after the per-bundle defaults exist. Source: guidedrupal/seo-geo/metatag-architecture. - Pathauto patterns. Per-bundle node patterns + per-vocabulary taxonomy patterns. Source: guide
drupal/seo-geo/pathauto-patterns. - Sitemap settings. Per-bundle priority + changefreq (empty
changefreqallowed = no crawl-frequency hint) + image inclusion. Source: guidedrupal/seo-geo/xml-sitemap. - Sitemap index. simple_sitemap 4.x installs the
defaultsitemap with typedefault_hreflang, so hreflang needs no step. It also installs theindexsitemap, disabled. Setstatusinsimple_sitemap.sitemap.index.ymltositemap_index. The recipe writes no sitemap type and does not changedefault_variant. Source: guidedrupal/seo-geo/xml-sitemap. - robots.txt content.
robotstxt.settings.yml+ remove the staticrobots.txtvia composer scaffold exclusion. Source: guidedrupal/seo-geo/robots-txt. - google_tag install + config exclusion. Install the module and add
google_tagto$settings['config_exclude_modules']insettings.phpso its environment-specific container config stays out of config sync. The recipe authors nogoogle_tagconfig; the container is configured per environment by the operator. - Breadcrumb structured data. Verify
easy_breadcrumbhasadd_structured_data_json_ld: true. Source: guidedrupal/seo-geo/breadcrumbs-structured-data. - Breadcrumb customization. When declared, write
easy_breadcrumb.settings.ymlalternative_title_field(custom display-label field) andcustom_paths(path-to-label overrides). Source: guidedrupal/seo-geo/breadcrumbs-structured-data.
The bundle defaults cannot exist until the per-entity defaults exist; the entity-type groups reference the per-bundle defaults; pathauto patterns cannot generate aliases until the patterns are imported; the sitemap cannot reference URLs until aliases exist — hence the order.
Preconditions
- Drupal 11.3+ (required by
drupal_cms_seo_basicanddrupal_cms_seo_tools, which this recipe leverages). - Composer-managed install (the recipe writes to
composer.jsonand runscomposer require). - A writable config sync directory the recipe can author into, and a writable
settings.php(the recipe appendsgoogle_tagto$settings['config_exclude_modules']whengoogle_tagis in scope). - DDEV runs the site, and
ddev drushworks from the project root. The verifier's commands go throughddev. - A typed input contract supplied by the caller (see below).
Input contract
Generic schema, source-agnostic, supplied by the caller. No field in this contract has a runtime default; missing fields fail Phase 0 validation.
mode: dry-run | apply
project_root: string # absolute path to the Drupal site
config_sync_dir: string # relative to project_root
layers_in_scope: # opt-in per layer; absent = false
metatag: true
schema_org: true
sitemap: true
pathauto: true
redirect: true # verify auto-redirect-on-alias-change behavior
robotstxt: true
google_tag: false # install module + exclude its config (settings.php)
non_content_defaults: false # 403/404/taxonomy_term/user metatag defaults
semantic_role_by_bundle: # operator-chosen; no auto-derivation
# role enum: content_article | product | service | web_page | landing_page |
# person | organization | none
# person → author/profile-style bundles (Schema.org Person)
# organization → tenant/agency/brand bundles (Schema.org Organization)
<bundle>: <role>
og_type_by_bundle: # og:type per bundle; bundles omitted inherit
# the node-level default. Source: guide drupal/seo-geo/open-graph
<bundle>: <og_type> # e.g. article | website | product
metatag_groups_by_bundle: # metatag.settings.yml entity_type_groups —
# which tag groups surface on each bundle's edit form. DEFAULT IS DERIVED from
# semantic_role_by_bundle: every role gets basic + open_graph, plus, when
# schema_org is in scope, its role's group: content_article → schema_article;
# product → schema_product; service → schema_service; web_page and
# landing_page → schema_web_page; person → schema_person; organization →
# schema_organization; none → no schema group. Declare a bundle here only to
# OVERRIDE the derived set.
<bundle>: [<group>] # e.g. [basic, open_graph, schema_article]
date_fields_by_bundle: # drives article_published_time /
# article_modified_time tokens. Bundles omitted get no date meta. Each bundle's
# actual date fields differ. Source: guide drupal/seo-geo/open-graph
<bundle>:
published: <field_machine_name> # e.g. field_publication_date | created
modified: <field_machine_name> # e.g. field_updated_date | changed
alternate_links: [] # metatag_custom_tags rel="alternate" links
# (RSS, AMP, …). Empty = none. Requires the metatag_custom_tags module when used.
# - {rel: alternate, type: <mime_type>, href: <url>}
priority_chains: # walked top-down against audited fields;
og_image: # first present field wins; if none, omit
- <field_machine_name>
meta_title:
- <field_machine_name>
- node:title # always-present terminator
meta_description:
- <field_machine_name>
reference_sources: [string] # absolute paths to reference projects;
# Phase 2 reads ONLY these
reference_selections: [string] # explicit menu items pre-approved;
# unselected items are dropped silently
home_node: # single source of truth; recipe does not infer
nid: integer
bundle: string
og_image_strategy: omit | static_url
og_image_static_url: string|null
og_type: string|null # og:type on the front-page default; often
# differs from the node-level default; null = inherit
sitemap_index: bool # status of simple_sitemap.sitemap.index. The
# index lists every other enabled sitemap. simple_sitemap 4.x installs it disabled
# and installs the `default` sitemap as type default_hreflang, so hreflang takes
# no input. Source: guide drupal/seo-geo/xml-sitemap
sitemap_priority_by_role: # role → (priority, changefreq)
# changefreq enum: always | hourly | daily | weekly | monthly | yearly | never | ""
# "" = emit no changefreq hint (a valid, intentional "don't signal crawlers" stance)
content_article: {priority: <0.0-1.0>, changefreq: <enum|"">}
product: {priority: <0.0-1.0>, changefreq: <enum|"">}
service: {priority: <0.0-1.0>, changefreq: <enum|"">}
web_page: {priority: <0.0-1.0>, changefreq: <enum|"">}
landing_page: {priority: <0.0-1.0>, changefreq: <enum|"">}
person: {priority: <0.0-1.0>, changefreq: <enum|"">}
organization: {priority: <0.0-1.0>, changefreq: <enum|"">}
pathauto_patterns_by_bundle: # bundles omitted get no pattern
<bundle>: <pattern_string>
pathauto_patterns_by_vocabulary: # vocabularies omitted get no pattern
<vocabulary>: <pattern_string>
excluded_vocabularies: [] # vocabularies that exist as taxonomies but are
# NOT part of the public SEO surface: no pathauto pattern, no sitemap setting, no
# metatag default is authored for them. Declared opt-out so the operator need not
# name every vocabulary just to silently exclude it.
# - <vocabulary>
redirect: # verified when redirect in layers_in_scope
auto_redirect: bool # expected true
default_status_code: integer # expected 301
suppress_404: bool # redirect_404.suppress_404; expected true
robotstxt_content: string # full robots.txt body
robotstxt_remove_static_scaffold: bool # remove web/robots.txt via composer scaffold
# google_tag has no contract fields — when in scope the recipe installs the module
# and excludes its config via settings.php (config_exclude_modules). The GA4 / GTM
# container is environment-specific and set by the operator per environment.
breadcrumb: # easy_breadcrumb.settings.yml customization
alternative_title_field: string|null # custom field for the breadcrumb display label
# (e.g. field_breadcrumb_title); null = default
custom_paths: {} # path → label overrides, operator-facing as a map;
# the recipe serializes it to easy_breadcrumb's native `PATH::LABEL` newline-delimited
# string form. Empty = none.
# <path>: <label>
escalation_policy: # per ambiguity class; default = halt
no_image_field_for_bundle: halt | omit_og_image | use_site_default
no_description_field_for_bundle: halt | use_fallback | use_static
url_convention_change_on_live_aliases: halt | apply_with_redirects | skip
hardcoded_schema_org_blob: halt | apply | skip
conflict_with_existing_config: halt | overwrite | skip
unknown_bundle_in_groups_map: halt | apply | drop # bundle in metatag_groups_by_bundle doesn't exist
unknown_vocabulary_in_excluded_list: halt | apply | drop # vocab in excluded_vocabularies doesn't exist
date_field_missing_on_bundle: halt | omit_date_meta | use_node_created_changed # date_fields_by_bundle names an absent field
new_entity_field_required: halt # always halts; data-model changes out of scope
content_seed_required: halt # always halts; content authoring out of scope
Sequence
If mode: dry-run, perform all reads and derivations but emit a preview instead of writing.
-
Validate input contract. Halt with
contract_erroron missing/inconsistent fields, on bundles that don't exist, on roles that aren't in the enum, on priority chains that name no terminator. -
Audit target project state. Inventory composer.json modules,
core.extension.yml, per-bundle field lists, existing sync configs matching the in-scope layers, taxonomies, home node. Read-only. Emit a structuredaudit.json. See guidedrupal/seo-geo/overviewfor what's worth inventorying. -
Reference scan (advisory menu). Read only
reference_sources; produce a labelled menu of reference patterns. Filter toreference_selections; drop everything else. The recipe never carries unselected reference patterns into the plan. See guidedrupal/seo-geo/seo-recipe-baselinefor the Drupal CMS recipe shape if it's a selected source. -
Compose the plan. For each in-scope layer, walk the contract's rules over the audit:
- For each bundle in
semantic_role_by_bundle: compose its per-bundle metatag default by walkingpriority_chainsagainst the bundle's audited fields. Cite the rule and the field for every produced line. Addog_type_by_bundleanddate_fields_by_bundleoverrides where declared; halt perdate_field_missing_on_bundleif a named date field is absent. - For
metatag_groups_by_bundle(or the role-derived default): composemetatag.settings.ymlentity_type_groups; halt perunknown_bundle_in_groups_mapon an unknown bundle. - For each role: derive Schema.org type and sitemap priority/changefreq (empty
changefreqallowed) from the input mapping. Plansimple_sitemap.sitemap.index.ymlwithstatusset tositemap_index. - For each bundle in
pathauto_patterns_by_bundle: produce the pattern file. - For each vocabulary in
pathauto_patterns_by_vocabulary: produce the pattern file. Vocabularies inexcluded_vocabulariesget no pathauto/sitemap/metatag surface; halt perunknown_vocabulary_in_excluded_liston an unknown vocabulary. - For
alternate_links(if any): planmetatag_custom_tagsrel="alternate"entries (requires themetatag_custom_tagsmodule). - For
breadcrumb(if declared): planeasy_breadcrumb.settings.ymlalternative_title_field+custom_paths. - For
google_tag(if in scope): plan the module install and thesettings.phpconfig_exclude_modulesentry. No container config is produced. -
For robotstxt, redirect verification, 403/404, taxonomy_term, user, home: produce per the input. Emit a structured
plan.json. -
Resolve escalations. Walk every ambiguity and apply the escalation_policy. If any policy is
halt, emit a structuredescalation.jsonand exit non-zero. The operator updates the contract and re-runs. -
Apply. Walk the plan:
composer require <missing modules>+drush en <missing modules>(includesgoogle_tagwhen in scope).- For each emitted config file: absent → write; present + matching → no-op + log; present + differing → halt with
conflict(already escalated in step 5; reaching this means a TOCTOU change). - For
google_tagin scope: addgoogle_tagto$settings['config_exclude_modules']insettings.php. The module is installed (above); no container config is written or exported. drush cim.drush pathauto:aliases-generate createper added pattern.drush simple-sitemap:generate.drush cr.-
For
robotstxt_remove_static_scaffold: true: updatecomposer.jsonextra.drupal-scaffold.file-mappingto exclude[web-root]/robots.txt; delete the file. -
Verify. Run the verifier (next section). Non-zero exit on any failure.
-
Emit summary. Change log: files written, modules installed, aliases regenerated, verifier results.
Data flow
input: contract (operator-supplied, validated up front)
reads project state:
composer.json + core.extension.yml
field.field.<entity>.<bundle>.<field>.yml (per bundle field inventory)
taxonomy.vocabulary.*.yml
existing metatag.metatag_defaults.* / simple_sitemap.* / pathauto.* /
redirect.* / robotstxt.*
settings.php (config_exclude_modules — for the google_tag exclusion)
system.site.yml (home node)
applies opinion:
play drupal/best-practices/camoa/metatag-per-bundle (per-bundle defaults)
inline invariant stances: adapt-to-project-fields · schema-type-per-role ·
no-hardcoded-json-ld-without-policy · sitemap-priority-per-role ·
one-pathauto-pattern-per-bundle · robotstxt-as-content ·
google-tag-install-only · halt-on-ambiguity ·
headless-via-input-contract · verifier-runs-adversarially
references atomic detail (guides):
drupal/seo-geo/{ overview, seo-recipe-baseline, metatag-architecture,
core-meta-tags, open-graph, twitter-cards, canonical-urls,
metatag-multilingual, structured-data-decision, schema-metatag-setup,
schema-types-reference, pathauto-patterns, redirect-management,
xml-sitemap, robots-txt, breadcrumbs-structured-data, testing-validation }
emits (in chain order):
composer.json updates (modules + scaffold exclusion)
metatag.metatag_defaults.global.yml (update)
metatag.metatag_defaults.node.yml (update; chains pruned to audit)
metatag.metatag_defaults.taxonomy_term.yml (create, if non_content_defaults)
metatag.metatag_defaults.user.yml (create, if non_content_defaults)
metatag.metatag_defaults.403.yml (create, if non_content_defaults)
metatag.metatag_defaults.404.yml (create, if non_content_defaults)
metatag.metatag_defaults.node__<bundle>.yml (per bundle in semantic_role_by_bundle)
metatag.settings.yml (entity_type_groups per metatag_groups_by_bundle / derived)
pathauto.settings.yml (taxonomy_term enabled if needed)
pathauto.pattern.<bundle>.yml (per bundle in input)
pathauto.pattern.taxonomy_<vocabulary>.yml (per vocab in input)
simple_sitemap.bundle_settings.default.node.<bundle>.yml (per role mapping; changefreq may be '')
simple_sitemap.bundle_settings.default.taxonomy_term.<vocab>.yml (per input; excluded_vocabularies omitted)
simple_sitemap.sitemap.index.yml (update; status = sitemap_index)
robotstxt.settings.yml (from input)
easy_breadcrumb.settings.yml (alternative_title_field + custom_paths, if declared)
settings.php (config_exclude_modules += google_tag, if in scope)
core.extension.yml (newly-enabled modules)
State-awareness contract
For every emitted config object: absent → create; present and matching the derived spec → skip with no-op; present and differing → conflict, do not overwrite unless escalation_policy.conflict_with_existing_config: overwrite. A field listed in priority_chains that does not exist on any in-scope bundle → log warning, drop from chain. A bundle in semantic_role_by_bundle whose role implies fields it lacks (e.g. content_article without field_seo_title) → halt with contract_error so the operator either adjusts the role or the chain.
Idempotent: running the recipe twice on identical input and identical project state produces no changes on the second run, including no alias regeneration if no new pattern was added.
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/drupal-seo-foundation/verify.php run the script in ## Files. It prints one line per violation, prints a violation when an input it needs is missing, and exits non-zero when it printed any.
verifier: - id: metatag-defaults-per-bundle kind: config-assert run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- metatag-defaults {layers_in_scope:json} {semantic_role_by_bundle:json} {og_type_by_bundle:json} pass: stdout empty - id: head-per-bundle kind: live-site run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- head {layers_in_scope:json} {semantic_role_by_bundle:json} {og_type_by_bundle:json} {date_fields_by_bundle:json} pass: stdout empty - id: metatag-groups kind: config-assert run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- metatag-groups {layers_in_scope:json} {semantic_role_by_bundle:json} {metatag_groups_by_bundle:json} pass: stdout empty - id: sitemap-settings kind: config-assert run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- sitemap-settings {layers_in_scope:json} {semantic_role_by_bundle:json} {sitemap_priority_by_role:json} pass: stdout empty - id: sitemap-served kind: live-site run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- sitemap-served {layers_in_scope:json} {semantic_role_by_bundle:json} {sitemap_priority_by_role:json} pass: stdout empty - id: sitemap-index kind: config-assert run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- sitemap-index {layers_in_scope:json} {sitemap_index:json} pass: stdout empty - id: robots-served-by-module kind: live-site run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- robots {layers_in_scope:json} {robotstxt_content:json} {robotstxt_remove_static_scaffold:json} pass: stdout empty - id: pathauto-patterns kind: self-fixture run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- pathauto {layers_in_scope:json} {pathauto_patterns_by_bundle:json} {pathauto_patterns_by_vocabulary:json} pass: stdout empty - id: redirect-config kind: config-assert run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- redirect-config {layers_in_scope:json} {redirect:json} pass: stdout empty - id: redirect-on-alias-change kind: self-fixture run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- redirect-fixture {layers_in_scope:json} {redirect:json} {pathauto_patterns_by_bundle:json} pass: stdout empty - id: google-tag-config-excluded kind: config-assert run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- google-tag {layers_in_scope:json} pass: stdout empty - id: excluded-vocabularies kind: config-assert run: ddev drush php:script /var/www/html/.aida/drupal-seo-foundation/verify.php -- excluded {excluded_vocabularies: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 checks went:
- Each script check reads
layers_in_scopeand prints nothing for a layer out of scope.excluded-vocabulariesandactive-equals-exportrun whatever the scope. head-per-bundle,sitemap-servedandrobots-served-by-modulerequest the page in process, through the site's own HTTP kernel, as an anonymous visitor. They need no web server and no base URL, but they read the site's content.head-per-bundlerenders the newest published node of each bundle insemantic_role_by_bundle, and skips a bundle that has none. It fails when no bundle has a published node. A tag in the bundle's default that is absent or empty on that node fails the entry. A tag is skipped when every node field its tokens name exists and is empty on that node, such asog_imageon an article with no image. The same holds for the date tags and the fieldsdate_fields_by_bundlenames.robots-served-by-modulecompares the served body withrobotstxt_content, trimmed. Lines another module adds throughhook_robotstxt()make them differ. The in-process request always reaches the module's route. A web server that still serves a static file is covered by the file check, run whenrobotstxt_remove_static_scaffoldis true.pathauto-patternscreates an unpublished node per bundle and a term per vocabulary, then deletes them. The pattern pathauto picks for the new entity must equal the input string, and the saved entity must get an alias. This replaces the old check on the newest entity, which depended on existing content. It also covers the oldContextExceptioncheck: pathauto logs that exception and does not apply the pattern, so the fixture gets no pattern.redirect-on-alias-changeuses the first bundle whose pattern holds[node:title], because a title change must move the alias. It changes the title and reads the redirect repository. It does not request the old alias.- The fixtures' saves run every module's entity hooks, such as the sitemap queue. The fixture deletes its entities, their aliases and their redirects, and nothing else.
sitemap-indexchecks that thedefaultsitemap exists and is enabled, because the bundle settings name it. It checks that theindexsitemap has typeindexand astatusequal tositemap_index. simple_sitemap 4.x keeps no list of sitemaps in the index's configuration. The index lists every enabled sitemap whose type is notindex.metatag-groupscompares each bundle's groups with the declared override, or with the set the Input contract derives from its role.- The old idempotency check re-ran
apply.active-equals-exportproves the configuration half. Nothing proves "no aliases regenerated", so that half is dropped. - No entry covers the breadcrumb layers (chain steps 13 and 14),
non_content_defaults,home_node,alternate_linksorpriority_chains. The old checks covered none of them either. - The script in
## Filesis written into the project for the verify run and removed after it. Do not commit or edit it.
Files
The verifier's script. It runs through ddev drush php:script, which passes the arguments after -- to the script as $extra.
```php .aida/drupal-seo-foundation/verify.php