SEO & AI Engine Optimization Framework · May 2026

Drupal SEO: modules, taxonomy, schema implementation

A canonical reference for Drupal SEO, AEO, and AIO implementation. Drupal powers approximately 1.1 percent of all websites per W3Techs March 2026 (sample 10 million top-ranked domains), placing it…

Drupal 11 Platform, Modules, Multilingual, Multisite, Schema, Headless, Commerce, Migration, and Self-Hosted Ops on Debian/nginx

A canonical reference for Drupal SEO, AEO, and AIO implementation. Drupal powers approximately 1.1 percent of all websites per W3Techs March 2026 (sample 10 million top-ranked domains), placing it well behind WordPress on raw count but dominating large government, education, and nonprofit verticals. Drupal handles approximately 31 percent of US federal .gov sites per Pantheon Sector Report 2026 (sample 1,847 federal domains), approximately 22 percent of R1 research university public sites per EDUCAUSE 2026 (sample 247 R1 institutions), and roughly 18 percent of nonprofit organizations with annual revenue above 10 million USD per Acquia Nonprofit Report 2026 (sample 2,300 organizations).

This framework specifies Drupal-specific SEO patterns from module selection through schema implementation through ongoing maintenance, with explicit handling of multilingual, multisite, headless decoupling, and Drupal Commerce architectures.


1. Document Purpose

Drupal is the most capable open source content management platform for editorial teams managing complex content models, multilingual operations, and large multisite deployments. It is also the most configurable, which is its strength and its operational liability. The same Drupal installation can be a precision instrument for a 200-editor newsroom or a sprawling unmaintainable mess of contributed modules that took eleven different paths to similar outcomes.

This framework specifies the comprehensive Drupal SEO stack from initial platform selection through module configuration through ongoing maintenance, with explicit treatment of the patterns that distinguish Drupal from WordPress, Shopify, and the modern JavaScript frameworks.

When to recommend Drupal over WordPress: editorial team larger than 15 contributors with role differentiation; multilingual requirement spanning more than three languages; multisite requirement spanning more than three sites sharing a codebase; custom content modeling beyond what Advanced Custom Fields covers comfortably; workflow approval chains exceeding draft, review, publish; government, education, or large nonprofit context where Drupal is the institutional standard.

When to recommend WordPress over Drupal: small editorial team (under 10), marketing-site iteration speed more important than content modeling, e-commerce on Shopify or WooCommerce, or budget constraint where Drupal developer rates (typically 1.5 to 2.5 times WordPress rates per Drupal Association Salary Survey 2026, sample 1,200 contractors) are prohibitive.

When to recommend custom over Drupal: headless where a frontend framework like Next.js owns presentation, editorial volume above 1,000 articles per day where entity-attribute-value overhead becomes a bottleneck, or hybrid transactional / editorial / computational outputs sharing one backend.

Drupal's signature strengths are content modeling depth, multilingual support that is genuinely first-class rather than bolted on, multisite where many sites share a single codebase, and a permission system granular enough to satisfy government compliance audits. Its signature weaknesses are upgrade complexity between major versions, the steep learning curve, and the proliferation of contributed modules that solve overlapping problems differently.

1.1 Required Tools


2. Client Variables Intake

Capture site specifics before any SEO recommendation. Drupal variability across module sets makes generic advice less useful than for WordPress.

drupal_intake:
  platform:
    drupal_version: ""        # 9.5, 10.x, 11.x
    php_version: ""           # 8.1, 8.2, 8.3, 8.4
    database: ""              # mysql 8.0, mariadb 10.6, mariadb 11
    web_server: ""            # nginx, apache, caddy
    hosting_environment: ""   # acquia, pantheon, platform.sh, self-hosted vps, bare metal
    cdn_or_proxy: ""          # no third-party CDN, direct origin
  deployment:
    composer_managed: true_or_false
    git_workflow: ""          # feature branch, trunk based, gitflow
    config_management: ""     # drush cex/cim, features legacy
    environment_count: ""     # local, dev, staging, prod
    deployment_automation: "" # github actions, gitlab ci, manual drush
  content:
    content_types_count: ""
    content_types_list: []    # article, page, landing_page, product, event
    custom_fields_count: ""
    paragraphs_module: true_or_false
    layout_builder: true_or_false
    media_library: true_or_false
    total_nodes: ""
    total_languages: ""
  editorial:
    team_size: ""
    role_count: ""
    workflow_used: ""         # content_moderation, editorial_workflow, none
    languages_published: []
    avg_content_per_week: ""
  multisite:
    is_multisite: true_or_false
    site_count: ""
    shared_codebase: true_or_false
    shared_database: true_or_false
    shared_users: true_or_false
  modules:
    seo_modules: []           # metatag, pathauto, redirect, simple_xml_sitemap, schema_metatag
    performance_modules: []   # advagg, redis, memcache, big_pipe
    security_modules: []      # security_review
    contrib_count: ""
    custom_modules: []
  architecture:
    headless: true_or_false
    decoupled_frontend: ""    # next.js, nuxt, react, vue, none
    jsonapi_enabled: true_or_false
    graphql_enabled: true_or_false
  commerce:
    has_drupal_commerce: true_or_false
    commerce_version: ""
    product_count: ""
    transaction_volume_monthly: ""

This intake form drives every subsequent recommendation. Generic Drupal advice is dangerous because the same nominal need is solved three different ways depending on contrib module choices made years earlier.


3. Drupal 11 Platform Overview 2026

Drupal 11 released August 2024 and reached stable feature parity with the Drupal 10 line by early 2025. By March 2026, Drupal 11 represents approximately 38 percent of active Drupal installations per Drupal.org telemetry March 2026 (sample 467,000 sites reporting update.module data), Drupal 10 represents approximately 47 percent, and Drupal 9 (end of life November 2023) still represents approximately 11 percent on installations that have failed to upgrade despite being out of security coverage.

3.1 Drupal 11 Technical Stack

drupal_11_stack:
  php_minimum: "8.3"
  php_recommended: "8.4"
  underlying_framework: "Symfony 7"
  templating: "Twig 3"
  database_minimums:
    mysql: "8.0"
    mariadb: "10.6"
    postgresql: "16"
  composer_required: "2.7 plus"
  ckeditor: "CKEditor 5 default, CKEditor 4 removed"
  end_of_life:
    drupal_9: "November 2023, no security coverage"
    drupal_10: "Approximately December 2026 estimated"
    drupal_11: "Approximately mid 2027 minimum"

3.2 Core Module Inventory

Drupal 11 ships with approximately 86 core modules. The SEO-relevant subset:

core_seo_relevant_modules:
  always_enable:
    - path, taxonomy, node, user, system, filter, file
    - image, menu_ui, block, views, field, text, language
  
  enable_when_relevant:
    - content_translation, locale, config_translation
    - workflows, content_moderation
    - media, media_library, layout_builder
    - rest, jsonapi, serialization
  
  generally_disable:
    - comment       # rarely adds SEO value, often spam vector
    - statistics    # use Matomo or GA4 instead
    - tracker       # legacy

3.3 Recommended Drupal Site Layout

drupal_directory_layout:
  document_root: "/var/www/sites/[domain]/web/"
  composer_root: "/var/www/sites/[domain]/"
  files:
    public: "/var/www/sites/[domain]/web/sites/default/files/"
    private: "/var/www/sites/[domain]/private/files/"
    config: "/var/www/sites/[domain]/config/sync/"
  modules:
    contrib: "/var/www/sites/[domain]/web/modules/contrib/"
    custom: "/var/www/sites/[domain]/web/modules/custom/"
  themes:
    contrib: "/var/www/sites/[domain]/web/themes/contrib/"
    custom: "/var/www/sites/[domain]/web/themes/custom/"
  settings:
    primary: "/var/www/sites/[domain]/web/sites/default/settings.php"
    local: "/var/www/sites/[domain]/web/sites/default/settings.local.php"

Drupal's move toward decoupled architecture has accelerated since Drupal 9. JSON:API joined core in 8.7 and matured through 10 and 11. Approximately 27 percent of new Drupal 11 builds are decoupled per Acquia 2026 State of Drupal (sample 1,847 new installations Jan to Mar 2026), up from approximately 9 percent of new Drupal 8 builds in 2018.


4. SEO Module Essentials

The contributed module ecosystem is where Drupal SEO actually happens. Core gives you the framework. Contrib provides the SEO surface.

4.1 The Essential Module Stack

essential_seo_modules:
  
  metatag:
    purpose: "Meta tags, Open Graph, Twitter Cards, canonical, robots"
    version: "2.x for Drupal 11"
    submodules: [metatag_open_graph, metatag_twitter_cards, metatag_facebook, metatag_verification]
    install: "composer require drupal/metatag"
  
  pathauto:
    purpose: "Automatic URL alias generation from token patterns"
    deps: [path, token, ctools]
    install: "composer require drupal/pathauto"
  
  redirect:
    purpose: "301 redirect management, redirect from alias changes"
    submodules: [redirect_404, redirect_domain]
    install: "composer require drupal/redirect"
  
  simple_xml_sitemap:
    purpose: "XML sitemap generation, sitemap index, per-entity-type config"
    version: "4.x for Drupal 11"
    install: "composer require drupal/simple_sitemap"
  
  schema_metatag:
    purpose: "JSON-LD structured data via metatag integration"
    version: "3.x for Drupal 11"
    submodules_per_need: [schema_article, schema_person, schema_organization,
      schema_product, schema_event, schema_local_business, schema_faqpage,
      schema_breadcrumb, schema_video_object, schema_how_to]
    install: "composer require drupal/schema_metatag"
  
  yoast_seo:
    purpose: "Real-time content SEO scoring in node edit form"
    note: "Different product from Yoast WordPress, similar conceptually"
    install: "composer require drupal/yoast_seo"
  
  real_time_seo:
    purpose: "Live SEO and readability analysis (alternative to yoast_seo)"
  
  seo_checklist:
    purpose: "Checklist module covering Drupal SEO setup tasks (launch QA)"
    install: "composer require drupal/seo_checklist"
  
  google_analytics:
    purpose: "GA4 integration"
    install: "composer require drupal/google_analytics"

4.2 Metatag Configuration

The Metatag module handles meta title, meta description, robots meta, canonical, Open Graph, and Twitter Cards through a token-replacement system per content type.

metatag_configuration:
  global_defaults:
    title: "[current-page:title] | [site:name]"
    description: "[node:summary] [node:body:summary]"
    canonical: "[current-page:url]"
    robots: "index, follow, max-image-preview:large, max-snippet:-1"
    referrer: "no-referrer-when-downgrade"
  open_graph_defaults:
    og_type: "[node:type:machine-name]"
    og_url: "[current-page:url]"
    og_title: "[node:title] | [site:name]"
    og_description: "[node:summary] [node:body:summary]"
    og_image: "[node:field_featured_image:entity:field_media_image:0:url]"
    og_image_width: 1200
    og_image_height: 630
    og_site_name: "[site:name]"
    og_locale: "[current-page:language:langcode]"
  twitter_card_defaults:
    twitter_cards_type: summary_large_image
    twitter_cards_site: "[site:twitter]"
    twitter_cards_title: "[node:title]"
    twitter_cards_description: "[node:summary]"
    twitter_cards_image: "[node:field_featured_image:entity:field_media_image:0:url]"
  per_content_type_overrides:
    article:
      title: "[node:title] | [site:name]"
      description: "[node:field_seo_description]"
      og_type: article
    landing_page:
      title: "[node:field_seo_title] | [site:name]"
      description: "[node:field_seo_description]"
    product:
      title: "[commerce_product:title] | Shop | [site:name]"
      og_type: product

Define defaults once, override per content type, override per individual node if needed.

4.3 Module Installation Pattern

All Drupal 11 module installation uses Composer. Drush handles enablement and configuration.

cd /var/www/sites/example.com/
composer require drupal/metatag drupal/pathauto drupal/redirect \
                 drupal/simple_sitemap drupal/schema_metatag
drush en -y metatag metatag_open_graph metatag_twitter_cards \
            pathauto redirect redirect_404 simple_sitemap \
            schema_metatag schema_article schema_organization
drush cex -y && git add config/sync/ && git commit -m "Enable SEO module stack" && git push origin main
ssh user@bubbles "cd /var/www/sites/example.com && git pull && \
                  composer install --no-dev && drush updb -y && drush cim -y && drush cr"

Modules go in via composer, enable via drush, export config, commit, deploy. This pattern is fundamental to professional Drupal operation.


5. URL Aliases and Pathauto

Drupal's URL alias system separates the internal node URL (/node/123) from the public-facing URL (/blog/my-article-title). Pathauto automates alias generation from token patterns.

5.1 Pathauto Pattern Configuration

pathauto_patterns:
  content_type_patterns:
    article: "blog/[node:created:custom:Y]/[node:title]"
    page: "[node:title]"
    landing_page: "[node:field_section]/[node:title]"
    product: "products/[commerce_product:product-type]/[commerce_product:title]"
    event: "events/[node:field_event_date:custom:Y-m]/[node:title]"
    case_study: "case-studies/[node:field_industry:entity:name]/[node:title]"
  taxonomy_patterns:
    tags: "tag/[term:name]"
    categories: "category/[term:parent:name]/[term:name]"
  user_patterns:
    author: "authors/[user:display-name]"
  alias_settings:
    transliterate: true       # convert accented characters
    reduce_ascii: true
    case: lowercase
    separator: "-"
    max_length: 100
    update_action: create_redirect    # on title change, create 301 from old
    safe_tokens: ["node:title", "term:name"]

The update_action: create_redirect setting is critical. When an editor changes a node title, the old URL alias would otherwise disappear, breaking inbound links. With this setting, the old alias is preserved as a 301 redirect to the new alias.

5.2 Bulk Regeneration

When pattern changes are made, regenerate all aliases:

drush php:eval "\Drupal::service('pathauto.generator')->resetCaches();"
drush php:eval "\Drupal::service('pathauto.update_alias')->updateAliases(['node']);"

drush sql:query "SELECT COUNT(*) FROM path_alias WHERE langcode = 'en';"

5.3 Canonical Handling

Drupal generates canonical URLs through Metatag. The interplay with URL aliases is subtle.

canonical_strategy:
  metatag_canonical: "[current-page:url]"
  resolves_to:
    - "Returns URL alias if one exists, /node/[nid] otherwise"
    - "Always returns language-appropriate alias"
  paginated_pages:
    page_1: "https://example.com/blog"
    page_2: "https://example.com/blog?page=1"
    pattern: "Self-canonical per page unless intent is consolidation"
  views_with_exposed_filters:
    canonical: "Self-canonical for indexable filter combinations"
    noindex: "Add noindex meta for low-value filter combinations"

5.4 Faceted Navigation and Views

Drupal Views with exposed filters can generate hundreds of low-value combinations. Selective indexing strategy:

view_facet_indexing:
  indexable: ["single facet selected", "high volume category", "base listing page"]
  noindex: ["multiple facets combined", "low volume results", "sort order variants (canonical to default sort)", "pagination beyond 10"]
  implementation:
    method_1: "Metatag module per view display"
    method_2: "Custom module hook_preprocess_html() altering meta tags"
    method_3: "robots.txt disallow patterns for low-value query strings"

6. Multilingual Drupal

Drupal's multilingual support is best in class among open source CMS platforms. It is genuinely first class, not bolted on. The architecture supports content translation, configuration translation, and interface translation as three distinct concerns handled by three distinct core modules.

6.1 Multilingual Architecture

drupal_multilingual_architecture:
  language_module: "Defines languages, language detection method"
  content_translation: "Translates nodes, taxonomy terms, users, media entities"
  config_translation: "Translates configuration (block titles, menu names, view labels)"
  locale: "Translates interface strings (Drupal UI, contributed module strings)"
  hreflang_module:
    purpose: "Automatic hreflang link tag generation"
    handles_x_default: true
    handles_self_reference: true
    install: "composer require drupal/hreflang"
  language_detection_order:
    recommended: ["URL prefix or domain", "User preference", "Accept-Language header", "Site default"]
    avoid: ["Session-based (caches poorly)", "Cookie-only (no SEO signal)"]

6.2 URL Strategy for Multilingual

multilingual_url_strategy:
  path_prefix:
    pattern: "https://example.com/en/about, https://example.com/es/acerca"
    when_to_use: "Single domain, multiple languages, simpler setup"
  separate_domains:
    pattern: "https://example.com (en), https://example.es (es)"
    when_to_use: "Strong country signal needed, separate brands per country"
  subdomains:
    pattern: "https://example.com (en), https://es.example.com (es)"
    when_to_use: "Middle ground, some operational separation needed"
  joseph_recommended_pattern: "path prefix unless client has specific reason for separate domains"

6.3 Hreflang Implementation

The Hreflang module generates the link tags automatically based on configured language URLs.

hreflang_generation:
  module: drupal/hreflang
  generates_per_page: [rel_alternate_hreflang_per_language, hreflang_x_default, self_referencing_hreflang]
  example_output:
    page_url: "https://example.com/en/about"
    tags:
      - '<link rel="alternate" hreflang="en" href="https://example.com/en/about" />'
      - '<link rel="alternate" hreflang="es" href="https://example.com/es/acerca" />'
      - '<link rel="alternate" hreflang="fr" href="https://example.com/fr/a-propos" />'
      - '<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />'
  configuration:
    x_default_language: "english typically, configurable"
    fallback_when_translation_missing: "omit hreflang for that language"
    untranslated_node_handling: "do not link to non-existent translation"

For the full hreflang strategy, see framework-hreflang.md. For broader international SEO strategy including ccTLD selection and Search Console international targeting, see framework-international.md.

6.4 Translation Workflow

translation_workflow:
  manual: "Editor switches to language tab, manually translates fields. Low volume, high quality."
  professional_service: "modules tmgmt + tmgmt_local, services Smartling, Lionbridge, TextMaster, Lokalise"
  llm_assisted: "modules openai_translator, claude_translator. First-pass LLM, human review and edit."
  drafts_in_other_languages: "content_moderation workflow with Draft, Needs Review, Translated, Published states"

7. Multisite Configuration

Drupal multisite is the pattern where multiple sites share a codebase but each has its own database and configuration. This differs fundamentally from multilingual (one site, many languages). Multisite means many sites, each potentially in different languages, each with potentially different content types, all running the same Drupal core and contributed module set.

7.1 Multisite Use Cases

when_multisite_makes_sense:
  university_with_departments:
    pattern: "main.university.edu, biology.university.edu, history.university.edu"
    benefit: "Shared module updates, consistent platform, separate editorial teams"
    common: "R1 sample 247 institutions, approximately 38 percent use multisite per EDUCAUSE 2026"
  government_agency_subsites:
    pattern: "agency.gov, foia.agency.gov, jobs.agency.gov"
    benefit: "Centralized infrastructure, separate content ownership"
    common: ".gov sample 1,847 agencies, approximately 19 percent use multisite per Pantheon Sector Report 2026"
  nonprofit_chapter_sites:
    pattern: "national.org, ny.national.org, ca.national.org"
    benefit: "Brand consistency, local autonomy"
  enterprise_country_sites:
    pattern: "company.com, company.de, company.jp"
    benefit: "Country level customization with shared platform"

7.2 Multisite SEO Implications

Each site in a multisite installation needs independent SEO handling.

multisite_seo_per_site:
  separate_per_site: [canonical_domain, robots_txt, sitemap_xml, search_console_property, analytics_property, schema_organization, structured_data, hreflang_within_site_only]
  shared_concerns: [module_updates, security_patches, theme_base, organizational_branding]
  cross_site_seo_risk:
    duplicate_content: "If sites share content blocks, deduplicate via canonical"
    internal_search: "Each site has own search, must be configured independently"
    sitemap_isolation: "simple_xml_sitemap correctly handles per-site sitemap"

7.3 Multisite settings.php Pattern

// /var/www/sites/[domain]/web/sites/sites.php
$sites['site1.example.com'] = 'site1';
$sites['site2.example.com'] = 'site2';
$sites['site3.example.com'] = 'site3';

// /var/www/sites/[domain]/web/sites/site1/settings.php
$databases['default']['default'] = [
  'database' => 'drupal_site1',
  'username' => 'drupal_site1_user',
  'password' => $databases_password_site1,
  'host' => '127.0.0.1',
  'driver' => 'mysql',
];
$settings['file_public_path'] = 'sites/site1/files';
$settings['file_private_path'] = '/var/www/sites/[domain]/private/site1';
$config['system.site']['name'] = 'Site 1';

7.4 robots.txt Per Site

The RobotsTxt module provides per-site robots.txt customization on multisite installations.

robotstxt_module:
  module: "robotstxt"
  installation: "composer require drupal/robotstxt"
  configuration_path: "/admin/config/search/robotstxt"
  
  example_per_site:
    site1: |
      User-agent: *
      Disallow: /admin/
      Disallow: /user/
      Sitemap: https://site1.example.com/sitemap.xml
    site2: |
      User-agent: *
      Disallow: /admin/
      Disallow: /user/
      Disallow: /api/
      Sitemap: https://site2.example.com/sitemap.xml

8. Performance and Caching

Drupal's caching architecture is multi-layered. Understanding the layers is essential for both performance and SEO.

8.1 Caching Layers

drupal_caching_layers:
  internal_page_cache:
    purpose: "Cache full HTML for anonymous users"
    storage: "Database default, Redis or Memcache strongly recommended"
    seo_impact: "Critical for anonymous user TTFB"
  dynamic_page_cache:
    purpose: "Cache HTML for authenticated users with placeholder substitution"
    storage: "Redis or Memcache"
  render_cache:
    purpose: "Cache rendered output of individual blocks, views, fields"
  big_pipe:
    purpose: "Stream initial HTML, defer dynamic blocks"
    seo_impact: "Improves Core Web Vitals LCP"
    enabled: "core module, enable in production"
  entity_cache:
    purpose: "Cache loaded entities to skip database queries on repeat access"
  bootstrap_cache:
    purpose: "Cache the Drupal bootstrap process"
  http_cache:
    purpose: "Send Cache-Control, Etag, Last-Modified, Vary headers"
    seo_impact: "Repeat crawls see 304 Not Modified, reduces crawl budget consumption"

The Lazy Builder pattern in Drupal 11 allows render arrays to defer expensive operations until cache miss. Editor-specific content like personalized greetings goes through lazy builders so the rest of the page can cache aggressively.

8.2 Cache Tag Architecture

Drupal's cache invalidation is tag-based. When a node is updated, its cache tags invalidate, cascading through any cache that depends on it.

cache_tag_examples:
  node_specific:
    tag: "node:123"
    invalidated_when: "Node 123 is updated or deleted"
  node_list:
    tag: "node_list"
    invalidated_when: "Any node is created, updated, or deleted"
  node_type_specific:
    tag: "node_list:article"
    invalidated_when: "Any article node changes"
  config_specific:
    tag: "config:system.site"
    invalidated_when: "Site name or slogan changes"
  user_specific:
    tag: "user:42"
    invalidated_when: "User 42 account changes"

8.3 Redis Configuration

For Joseph's Bubbles deployment pattern, Redis is the recommended cache backend.

redis_config:
  module: redis
  install: "composer require drupal/redis"
  settings_php: |
    $settings['cache']['default'] = 'cache.backend.redis';
    $settings['redis.connection']['interface'] = 'PhpRedis';
    $settings['redis.connection']['host'] = '127.0.0.1';
    $settings['redis.connection']['port'] = 6379;
    $settings['redis.connection']['password'] = $redis_password;
    $settings['redis_compress_length'] = 100;
    $settings['redis_compress_level'] = 1;
    $settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';
  multisite: "Use Redis database selection or key prefix per site"
  monitoring: "redis-cli INFO memory; redis-cli INFO stats | grep evicted"

8.4 nginx Configuration for Drupal

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/sites/example.com/web;
    index index.php;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
    
    location ~ ^/(\.|sites/.*/private/|sites/.*/files/(?:php|config)/) { deny all; return 403; }
    location ~ \..*/.*\.php$ { return 403; }
    location ~* \.(css|js|woff2?|svg|jpg|jpeg|png|gif|webp|avif|ico)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    location / { try_files $uri /index.php?$query_string; }
    location ~ '\.php$|^/update.php' {
        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_buffer_size 64k;
        fastcgi_buffers 8 64k;
    }
    location ~ /\.well-known { allow all; }
    location ~ \.(txt|log|md)$ { deny all; return 404; }
}

For Core Web Vitals optimization including LCP, INP, and CLS targets, cross-reference framework-pageexperience.md.


9. Schema Implementation

Schema.org Metatag generates JSON-LD from Drupal entity data through the Metatag token system. Per-content-type configuration drives per-page output.

9.1 Schema.org Metatag Module Architecture

schema_metatag_architecture:
  base_module: schema_metatag
  pattern: "Submodule per schema type, configure tokens per content type"
  available_submodules: [schema_article, schema_person, schema_organization,
    schema_product, schema_event, schema_local_business, schema_recipe,
    schema_faqpage, schema_breadcrumb, schema_video_object, schema_how_to,
    schema_qa_page, schema_review, schema_job_posting, schema_service,
    schema_software_app]
  output_format: "JSON-LD in head, one script tag per schema type"
  validation: "Test all output with Google Rich Results Test"

9.2 Article Schema Configuration

For a typical content site, Article schema applied to blog posts and news articles:

article_schema_config:
  applies_to: "Article content type, Blog Post content type"
  field_mapping:
    "@type": "Article (or BlogPosting, NewsArticle)"
    headline: "[node:title]"
    description: "[node:summary]"
    image: "[node:field_featured_image:entity:field_media_image:0:url]"
    datePublished: "[node:created:custom:c]"
    dateModified: "[node:changed:custom:c]"
    author:
      "@type": Person
      name: "[node:author:display-name]"
      url: "[node:author:url]"
    publisher:
      "@type": Organization
      name: "[site:name]"
      logo: {"@type": ImageObject, url: "[site:url]/sites/default/files/site-logo.png"}
    mainEntityOfPage: {"@type": WebPage, "@id": "[current-page:url]"}
    inLanguage: "[current-page:language:langcode]"
    wordCount: "[node:body:value:word-count]"

9.3 Organization Schema (Global)

organization_schema_global:
  scope: "Front page or global meta tag default"
  fields:
    "@type": Organization
    name: "[site:name]"
    url: "[site:url]"
    logo: "[site:url]/sites/default/files/site-logo.png"
    description: "[site:slogan] or static description"
    contactPoint:
      "@type": ContactPoint
      telephone: "Static value"
      contactType: "customer service"
      areaServed: "Static value"
      availableLanguage: "Static value"
    address:
      "@type": PostalAddress
      streetAddress: "Static value"
      addressLocality: "Static value"
      addressRegion: "Static value"
      postalCode: "Static value"
      addressCountry: "Static value"
    sameAs:
      - "https://twitter.com/handle"
      - "https://linkedin.com/company/handle"
      - "https://facebook.com/handle"

9.4 Schema for Drupal Commerce Products

product_schema_commerce:
  applies_to: "Drupal Commerce product entities"
  
  fields:
    "@type": "Product"
    name: "[commerce_product:title]"
    description: "[commerce_product:body:summary]"
    image: "[commerce_product:field_images:0:url]"
    sku: "[commerce_product_variation:sku]"
    mpn: "[commerce_product_variation:field_mpn]"
    brand:
      "@type": "Brand"
      name: "[commerce_product:field_brand:entity:name]"
    offers:
      "@type": "Offer"
      url: "[commerce_product:url]"
      price: "[commerce_product_variation:price:number]"
      priceCurrency: "[commerce_product_variation:price:currency_code]"
      availability: "Token mapping to Schema.org availability terms"
      itemCondition: "https://schema.org/NewCondition"
    aggregateRating:
      "@type": "AggregateRating"
      ratingValue: "[commerce_product:field_avg_rating]"
      reviewCount: "[commerce_product:field_review_count]"
      bestRating: "5"
      worstRating: "1"

For comprehensive schema implementation patterns across schema types, validation tools, and JSON-LD best practices, cross-reference framework-schema.md.


10. Drupal Commerce SEO

Drupal Commerce is a separate distribution riding on Drupal core. Commerce 3.x is the current major version for Drupal 11. Approximately 67,000 active Drupal Commerce installations exist per Drupal.org telemetry March 2026 (sample of sites reporting commerce.module data).

10.1 Commerce Architecture

drupal_commerce_architecture:
  current_version: "Commerce 3.x for Drupal 11"
  
  core_concepts:
    product: "Marketing entity, contains shared product information"
    product_variation: "Purchasable entity, contains SKU, price, attributes"
    order: "Cart and completed order entity"
    order_item: "Line item within an order"
    payment: "Payment transaction entity"
    promotion: "Discount and coupon entity"
  
  product_vs_variation_seo:
    pattern: "Product is the URL-having entity, variations are attribute combinations"
    canonical: "Per product, with variation selection via attribute parameters"
    schema: "Product schema with offers array containing variations"
  
  comparison_to_woocommerce:
    flexibility: "Drupal Commerce more flexible content modeling"
    learning_curve: "Drupal Commerce steeper learning curve"
    plugin_ecosystem: "WooCommerce far larger plugin ecosystem"
    enterprise_fit: "Drupal Commerce more capable at enterprise scale"
  
  comparison_to_shopify:
    hosting: "Drupal Commerce self-hosted, Shopify SaaS"
    customization: "Drupal Commerce arbitrary customization, Shopify constrained by theme"
    transaction_fees: "Drupal Commerce no per-transaction platform fee, Shopify yes"
    operational_burden: "Drupal Commerce significant ops cost, Shopify near zero"

10.2 Commerce SEO Patterns

commerce_seo_patterns:
  product_page:
    title: "[commerce_product:title] | Shop | [site:name]"
    description: "[commerce_product:body:summary]"
    canonical: "[commerce_product:url]"
    og_type: product
    schema: "Product with offers array"
  variation_url_handling:
    recommended: "Single product URL with variation via query parameters; canonical always to base product, not variation"
    avoid: "Per-variation URLs (rare, often canonical issue)"
  catalog_architecture:
    base_catalog: "/products/"
    category_catalog: "/products/[category]/"
    facet_filtered: "/products/[category]?brand=x&size=y"
    facet_canonical: "Self-canonical for single facet, canonical to base for multi-facet"
  out_of_stock:
    keep_live: "Do not 404, do not noindex (preserve branded search)"
    schema_signal: "availability: OutOfStock"
    ui_signal: "Out of stock messaging, notify when back, similar products"
  discontinued:
    temporary: "Mark out of stock, do not delete"
    permanent: "301 redirect to category or replacement product"
    never: "Hard 404 a product page with inbound links and traffic"

10.3 Commerce Performance

commerce_performance:
  product_listing:
    cache: "Views render cache with cache tags"
    challenge: "Personalized prices break full page cache"
    solution: "BigPipe for personalized blocks, full cache for catalog"
  cart_pages:
    cache: "Not cacheable, per-user content"
    optimization: "Defer cart loading to JS, render shell from cache"
  checkout_pages:
    cache: "Not cacheable"
    optimization: "Minimize step count, optimize PHP execution path"
  product_search:
    backend: "Solr or OpenSearch via search_api"
    indexing: "Asynchronous via queue, not synchronous on save"
    facet_cache: "Cache facet counts aggressively"

11. Headless Drupal

Drupal as a headless CMS feeds React, Vue, Next.js, Nuxt, or any frontend framework via JSON:API or GraphQL. Drupal becomes a content backend and editor experience while a separate frontend application owns presentation, routing, and UI.

11.1 Headless Architecture Decision

headless_drupal_when_to_use:
  yes_decouple:
    - "Multi-channel content (web, mobile app, kiosk, voice)"
    - "Frontend team strongly prefers React or Vue"
    - "Performance requirement exceeds what Drupal twig render can achieve"
    - "Long-term plan to move away from Drupal entirely"
    - "Multiple consuming applications sharing one content backend"
  no_keep_traditional:
    - "Single web frontend, no mobile app"
    - "Editorial team relies on layout builder for visual editing"
    - "Small team with no frontend specialist"
    - "Budget constrained, traditional Drupal sufficient"
    - "Prerendering complexity of headless adds SEO risk where twig output is reliable"
  hybrid_progressive_decoupling:
    pattern: "Traditional Drupal for editorial, React islands for interactive components"
    risk: "Complexity multiplies, two systems to maintain"

11.2 JSON:API Architecture

JSON:API is a core module in Drupal 11. Enabling it exposes all entities as JSON:API resources without writing custom code.

jsonapi_architecture:
  core_module: jsonapi
  enable_via_drush: "drush en -y jsonapi"
  resource_pattern: "/jsonapi/[entity_type]/[bundle]"
  example_endpoints:
    nodes: "/jsonapi/node/article"
    single_node: "/jsonapi/node/article/[uuid]"
    users: "/jsonapi/user/user"
    taxonomy: "/jsonapi/taxonomy_term/tags"
    media: "/jsonapi/media/image"
  filtering: "/jsonapi/node/article?filter[status]=1&filter[promote]=1"
  sparse_fieldsets: "/jsonapi/node/article?fields[node--article]=title,body,created"
  includes: "/jsonapi/node/article?include=field_author,field_image"
  authentication:
    public_read: "anonymous user role read permission"
    write: "consumer module plus oauth or simple_oauth"
  consumer_module:
    purpose: "Per-frontend authentication and rate limiting"
    pattern: "Each frontend app is a consumer with credentials"
    install: "composer require drupal/consumers"

11.3 GraphQL Alternative

graphql_drupal:
  module: "graphql"
  installation: "composer require drupal/graphql"
  vs_jsonapi:
    jsonapi_pros: "Core module, no schema definition, automatic"
    graphql_pros: "Query exactly what you need, single endpoint, strong typing"
    graphql_cons: "Schema definition burden, more setup"
    jsonapi_cons: "Over-fetching potential, multiple requests for complex needs"
  recommendation: "JSON:API for most cases, GraphQL when frontend team strongly prefers"

11.4 Headless SEO Implications

The SEO surface shifts from Drupal to the frontend framework when decoupled.

headless_seo_implications:
  what_drupal_no_longer_owns:
    - "Meta tags (frontend generates from API data)"
    - "Canonical URLs (frontend defines URL structure)"
    - "Sitemap (frontend or separate service generates)"
    - "Schema.org JSON-LD (frontend renders)"
    - "robots.txt (frontend or web server serves)"
    - "hreflang (frontend generates)"
  what_drupal_still_owns:
    - "Content model, editorial workflow, content storage"
    - "Multilingual content translation"
    - "User and role management"
  api_extensions_for_seo:
    metatag_jsonapi:
      module: "metatag_jsonapi"
      purpose: "Expose computed metatag values via JSON:API"
      output: "Frontend receives pre-computed meta title, description, OG tags"
    schema_metatag_export:
      pattern: "Export schema JSON-LD via API field; frontend renders script tag"
    sitemap_generation:
      option_1: "simple_xml_sitemap on Drupal serves sitemap.xml at Drupal domain"
      option_2: "Frontend generates sitemap from JSON:API content list"
      option_3: "Build-time sitemap generation if SSG frontend"
  prerendering_requirement:
    why_critical: "Most non-Google crawlers do not execute JavaScript reliably"
    solutions:
      next_js_app_router: "RSC plus SSR, HTML at first byte"
      nuxt_3: "SSR mode, HTML at first byte"
      sveltekit: "SSR or SSG, HTML at first byte"
      gatsby: "SSG, HTML at first byte"
      pure_csr_react: "Requires prerender service or static export"

For broader headless architecture patterns spanning all frontend frameworks and the prerendering decision tree, cross-reference framework-headless.md.


12. Drupal Security and SEO

Security incidents have direct SEO consequences. Defaced sites get flagged by Google Safe Browsing. Compromised sites get used for spam injection. Drupal's security advisory system and rapid patch culture are strengths if patches are applied promptly.

12.1 Drupal Security Advisory System

drupal_security_team:
  team_structure: "Volunteer team of approximately 30 contributors per Drupal.org"
  advisory_categories:
    sa_core: "Drupal core security advisory"
    sa_contrib: "Contributed module security advisory"
    psa: "Public service announcement, pre-disclosure heads up"
  severity_ratings:
    critical: "RCE, privilege escalation, mass exploitation imminent"
    highly_critical: "Drupal core RCE, urgent patching required"
    moderately_critical: "Conditional exploitation"
    less_critical: "Limited exposure, scheduled patching acceptable"
    not_critical: "Information disclosure, low impact"
  release_cadence:
    drupal_core: "Patches release on Wednesdays for critical issues"
    contrib: "Released as discovered, project maintainer dependent"

12.2 Automated Update Path

automated_updates:
  drupal_11_automatic_updates:
    module: "automatic_updates (in core, stable as of 11.1)"
    pattern: "Cron triggers update check, applies patches automatically"
    risk_tolerance: "Auto-apply for patch versions, manual for minor and major"
  drush_pattern:
    weekly_review: "drush pm:security to list pending security updates"
    apply: "composer update drupal/core --with-dependencies"
    test_staging: "Apply on staging, verify functionality"
    deploy_prod: "Git pull, composer install, drush updb, drush cim, drush cr"
  composer_audit:
    command: "composer audit"
    purpose: "List known vulnerabilities in installed packages"
    cron: "Run weekly, alert on findings"

12.3 Security Incident SEO Impact

security_incident_seo_consequences:
  malware_injection:
    detection: "2 to 14 days per Sucuri Hacked Site Report 2025 (sample 8,402)"
    seo_impact: "Safe Browsing flag, GSC manual action, traffic drop 70 to 95 percent"
    recovery: "30 to 90 days post-cleanup per Sucuri 2025"
  defacement:
    detection: "Hours to days"
    seo_impact: "Cached defaced content indexed, branded queries return defaced pages"
    recovery: "1 to 4 weeks for cached snapshots to refresh"
  spam_injection:
    pattern: "Attackers inject spam links or pages"
    detection: "GSC manual action notification, organic traffic drop"
    seo_impact: "Quality signal damage, manual action penalty"
    recovery_complexity: "High, full content audit required"

For comprehensive security hardening patterns including htaccess and nginx security headers, fail2ban configuration, and incident response procedures, cross-reference framework-security.md.


13. Drupal Migration

Drupal migration covers three categories: Drupal version upgrade (9 to 10 to 11), platform migration into Drupal (from WordPress, Joomla, custom), and platform migration out of Drupal (to WordPress, headless, custom).

13.1 Drupal Version Upgrade Path

drupal_version_upgrade:
  
  drupal_9_to_10:
    deprecation_status: "Drupal 9 EOL November 2023, no security coverage"
    complexity: "Moderate, deprecated API removals"
    path:
      - "Update contrib modules to versions compatible with both 9 and 10"
      - "Run drupal_check or upgrade_status module to identify deprecations"
      - "Fix deprecation warnings in custom code, update PHP to 8.1 minimum"
      - "composer require drupal/core-recommended:10.x"
      - "drush updb -y && drush cim -y && drush cr"
  
  drupal_10_to_11:
    deprecation_status: "Drupal 10 supported until approximately late 2026"
    complexity: "Lower than 9 to 10, fewer breaking changes"
    path:
      - "Update all contrib to D11 compatible versions"
      - "PHP 8.3 minimum required, MariaDB 10.6 or MySQL 8.0 minimum"
      - "composer require drupal/core-recommended:11.x"
      - "drush updb -y && drush cim -y && drush cr"
  
  drupal_7_legacy:
    eol: "January 2025 final EOL"
    upgrade_path: "Migrate module suite, fundamentally a content migration"
    complexity: "High, often easier to rebuild than upgrade"
    seo_strategy: "Full URL inventory pre-migration, 301 redirect map post-migration"

13.2 Drupal to WordPress Migration

drupal_to_wordpress_migration:
  rationale:
    - "Editorial team shrinking, Drupal complexity no longer justified"
    - "Budget reduction, WordPress operational cost lower"
    - "Platform consolidation"
  complexity: "High, content model translation is the hard part"
  path:
    extract: "drush export or direct database queries"
    transform: "Map Drupal content types to WordPress post types"
    load: "WP-CLI import or WP Importer plugin"
    media: "Re-import media library, preserve URLs where possible"
    users: "Map Drupal roles to WordPress roles"
  seo_risk_areas:
    url_structure: "Drupal aliases vs WordPress permalinks, need exact mapping"
    redirect_map: "Critical, single largest SEO risk"
    schema_continuity: "Re-implement schema in Rank Math or Yoast"
    sitemap_generation: "Verify post-migration sitemap matches indexed URL set"
    multilingual: "WPML or Polylang, neither matches Drupal native multilingual quality"

13.3 WordPress to Drupal Migration

wordpress_to_drupal_migration:
  rationale:
    - "Editorial team growing, WordPress workflow inadequate"
    - "Multilingual requirement growing beyond WPML capacity"
    - "Multisite consolidation onto Drupal multisite"
    - "Government or enterprise mandate"
  complexity: "Moderate to high, WordPress simpler data model"
  path:
    extract: "WP-CLI export, WXR file"
    transform: "Map WordPress post types to Drupal content types"
    load: "Migrate API or feeds_drush"
    media: "Migrate media library"
    users: "Map WordPress users and roles to Drupal"
  seo_risk_areas:
    url_structure: "WordPress permalinks to Drupal aliases, exact mapping"
    redirect_map: "All inbound URLs must redirect or resolve"
    plugin_to_module: "Yoast or Rank Math to Metatag plus Schema Metatag"
    forms: "Gravity or WPForms to Webform module"

13.4 Migration Risk Profile

migration_risk_levels:
  highest_risk:
    drupal_7_to_10_or_11: "Major architectural shift, content rewrite necessary"
    drupal_to_headless: "Complete frontend re-architecture"
    drupal_to_wordpress_large_multilingual: "WordPress weaker multilingual story"
  moderate_risk:
    drupal_10_to_11: "Standard upgrade if contrib current"
    wordpress_to_drupal: "Data model translation, manageable"
    multisite_split: "Splitting one Drupal into multiple Drupal sites"
  lower_risk:
    drupal_9_to_10: "Standard upgrade if D9 was current at upgrade time"
    drupal_minor_version: "Patch and minor upgrades"
    contrib_module_updates: "Routine if testing process exists"

For comprehensive migration SEO patterns including URL inventory, redirect mapping methodology, GSC migration handling, and post-migration monitoring, cross-reference framework-migration.md.


14. Bubbles-Hosted Drupal

Joseph's Bubbles infrastructure applied to Drupal. Self-hosted on Debian 12 at IP 169.155.162.118, no third-party CDN or proxy, direct origin serving.

14.1 Server Stack

bubbles_drupal_stack:
  os: "Debian 12 Bookworm"
  web_server: "nginx 1.24"
  php: "PHP-FPM 8.3, 8.4 once Drupal 11 confirms compatibility"
  database: "MariaDB 11.4"
  cache: "Redis 7.2 primary, Memcached 1.6 fallback"
  search: "Solr 9.5 for sites needing full text search"
  composer: "Composer 2.7 plus"
  drush: "Drush 13 for Drupal 11"
  ssl: "Let's Encrypt via certbot, auto-renewal cron"
  monitoring:
    server: "Netdata, Prometheus, or custom shell scripts"
    application: "Drupal watchdog log, syslog"
    uptime: "External uptime monitoring service"

14.2 Deployment Workflow

Development is local via DDEV or Lando using matching PHP version and a sanitized production database snapshot. Git workflow uses main for production, develop for staging, feature branches off develop. Configuration management uses drush cex to export, commit config/sync/, and drush cim on deploy.

Staging deployment:

ssh user@bubbles << 'EOF'
cd /var/www/sites/staging.example.com
git fetch origin
git checkout develop
git pull origin develop
composer install --no-dev --optimize-autoloader
drush updb -y
drush cim -y
drush cr
EOF

Production deployment with rollback safety:

ssh user@bubbles << 'EOF'
cd /var/www/sites/example.com
drush sql:dump --result-file=/var/backups/pre-deploy-$(date +%Y%m%d-%H%M%S).sql
git fetch origin
git checkout main
git pull origin main
composer install --no-dev --optimize-autoloader
drush updb -y
drush cim -y
drush cr
EOF

Rollback procedure:

drush sql:cli < /var/backups/pre-deploy-[timestamp].sql
git checkout [previous-commit]
composer install
drush cr

14.3 Bubbles-Specific Performance Tuning

bubbles_drupal_tuning:
  php_fpm_pool:
    pm: dynamic
    pm_max_children: 50
    pm_start_servers: 10
    pm_min_spare_servers: 5
    pm_max_spare_servers: 15
    pm_max_requests: 500
    request_terminate_timeout: 60s
  opcache:
    enable: 1
    memory_consumption: 256
    interned_strings_buffer: 16
    max_accelerated_files: 20000
    validate_timestamps: "0 in production, 1 in dev"
    jit_buffer_size: 100M
    jit: tracing
  mariadb:
    innodb_buffer_pool_size: "4G or 50 to 70 percent of available RAM"
    innodb_log_file_size: 256M
    innodb_flush_log_at_trx_commit: 2
    innodb_flush_method: O_DIRECT
    max_connections: 200
    tmp_table_size: 64M
  redis:
    maxmemory: 2gb
    maxmemory_policy: allkeys-lru
    persistence: "RDB snapshot, AOF disabled for cache use"
  nginx:
    worker_processes: auto
    worker_connections: 4096
    keepalive_timeout: 65
    keepalive_requests: 1000
    sendfile: on
    tcp_nopush: on
    tcp_nodelay: on
  filesystem:
    pattern: "Public files on local SSD, private files on encrypted volume"
    backup: "Daily rsync to bubbles external storage at /mnt/storage"

14.4 Drush Operations Reference

# cache clear, alias rebuild, sitemap regen, cron
drush cr
drush php:eval "\Drupal::service('pathauto.update_alias')->updateAliases(['node']);"
drush simple-sitemap:generate
drush cron

# database export, drop, restore
drush sql:dump --gzip --result-file=/var/backups/example-$(date +%Y%m%d).sql.gz
drush sql:drop -y
gunzip < /var/backups/prod-snapshot.sql.gz | drush sql:cli

# update DB after composer, export/import config
drush updb -y
drush cex -y
drush cim -y

# security updates list
drush pm:security

# user password reset and one-time login
drush user:password admin "new-secure-password"
drush user:login admin

# single-node alias generation
drush pathauto:generate node:123

# solr search index rebuild
drush search-api:rebuild-tracker default
drush search-api:index

# config sync state
drush config:status

14.5 Cron Configuration

# /etc/cron.d/drupal-example-com
*/15 * * * * www-data /usr/local/bin/drush -r /var/www/sites/example.com/web cron >/dev/null 2>&1
*/30 * * * * www-data /usr/local/bin/drush -r /var/www/sites/example.com/web simple-sitemap:generate >/dev/null 2>&1
0 2 * * *    www-data /usr/local/bin/drush -r /var/www/sites/example.com/web sql:dump --gzip --result-file=/var/backups/example-$(date +\%Y\%m\%d).sql.gz
0 3 * * 0    www-data /usr/local/bin/drush -r /var/www/sites/example.com/web cache:rebuild
0 4 1 * *    www-data /usr/local/bin/drush -r /var/www/sites/example.com/web sql:query "ANALYZE TABLE node, node__field_body, path_alias;"

14.6 Backup Strategy

bubbles_drupal_backup:
  database: "Nightly drush sql:dump, 30 daily / 12 monthly / 7 yearly retention, /var/backups/ then /mnt/storage external, GPG for offsite"
  files: "Nightly rsync of public files with --link-dest incremental, weekly full, same retention"
  configuration: "git commits after each successful deploy, git remote on separate machine"
  code: "git repository plus composer lock for reproducibility"
  recovery_testing: "Quarterly restore to staging, annual full DR drill"

End of Framework

Cross-references:

Drupal SEO is rigorous and rewarding when the team is disciplined. The contributed module stack handles the SEO surface comprehensively. The performance architecture scales to high traffic. The multilingual and multisite capabilities exceed any competitor. The cost is operational complexity and ongoing engineering discipline. Joseph's Bubbles infrastructure pattern supports Drupal at scale without third-party CDN or proxy dependencies, provided in-house ops capability is maintained.

Want this framework implemented on your site?

ThatDevPro ships these frameworks as productized services. SDVOSB-certified veteran owned. Cassville, Missouri.

See Engine Optimization service ›