Every Drupal fix we catalog

SEOLZ catalogs 8 fixes for Drupal across 5 areas — SEO, answer-engine readiness, accessibility, security and site-health. Each lists the exact steps for Drupal, with a link to the official docs.

SEO · 1 fixes

Missing h1Quick win

Add a single, descriptive H1 tag to every page that currently lacks one, clearly stating the page's main topic.

On Drupal

  1. Drupal outputs the node Title as the H1 by default via the page-title.html.twig template. Ensure every node/product has a Title value.
  2. If the template overrides the tag, locate page-title.html.twig in your active theme and ensure it uses <h1{{ title_attributes }}>{{ title }}</h1>.
  3. For Drupal Commerce product pages, go to Commerce → Products → open the product and fill in the Title field.
  4. Clear the Drupal cache under Admin → Configuration → Performance → Clear all caches after template changes.
Code · Generic HTML (html)
<h1>Women's Running Shoes</h1>

Place exactly one of these per page, as the first heading in the main content area.

Answer Engine Optimization · 1 fixes

Missing schema howtoModerate effort

Add HowTo structured data (JSON-LD) to any page that walks customers through a multi-step process, so Google can display your instructions as rich results in search.

On Drupal

  1. Install the 'Schema.org Metatag' module or 'Metatag' module with the JSON-LD submodule from drupal.org.
  2. Configure schema mappings for the relevant content type (e.g. 'How-to Guide') under Configuration › Search and Metadata › Metatag.
  3. For HowTo, you may need a custom module or a preprocess hook in your theme's .theme file to build the JSON-LD array and attach it via #attached['html_head'] on the relevant node type.
  4. BreadcrumbList is handled by the 'Simple XML Sitemap' or Metatag modules, or via a custom hook_preprocess_page().
  5. Validate with Google's Rich Results Test.
Code · Generic HTML (html)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>How to Complete Your Task | Your Store</title>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "HowTo",
    "name": "How to Complete Your Task",
    "description": "Follow these steps to complete your task easily.",
    "totalTime": "PT10M",
    "step": [
      {
        "@type": "HowToStep",
        "position": 1,
        "name": "Prepare your materials",
        "text": "Gather everything you need before you start: list materials here."
      },
      {
        "@type": "HowToStep",
        "position": 2,
        "name": "Complete the main action",
        "text": "Describe exactly what the user should do in this core step."
      },
      {
        "@type": "HowToStep",
        "position": 3,
        "name": "Verify the result",
        "text": "Explain how the user confirms the task was completed successfully."
      }
    ]
  }
  </script>
</head>
<body>
  <!-- Your how-to page content -->
</body>
</html>

Place this block inside the <head> element of any static HTML page that contains step-by-step instructions.

Accessibility (WCAG) · 2 fixes

Aria command nameModerate effort

Give every button, link, and menuitem a clear, accessible name so screen readers can announce what it does.

On Drupal

  1. In Drupal admin, go to Appearance and identify your active theme.
  2. In your custom or sub-theme, locate the Twig template file responsible for the unlabelled element (e.g., button.html.twig, menu.html.twig).
  3. Add `aria-label="{{ 'Descriptive action'|t }}"` to the element tag (using Drupal's translation function for internationalisation). Add `aria-hidden="true"` to decorative SVGs.
  4. Rebuild the Twig cache (Configuration → Development → Performance → Clear all caches, or `drush cr`) and verify with axe DevTools.
Code · Generic HTML (html)
<!-- OPTION 1: aria-label on an icon-only button -->
<button type="button" aria-label="Open cart">
  <svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24">
    <path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/>
    <line x1="3" y1="6" x2="21" y2="6"/>
    <path d="M16 10a4 4 0 01-8 0"/>
  </svg>
</button>

<!-- OPTION 2: visually-hidden span inside the button -->
<button type="button">
  <svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24">
    <path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/>
  </svg>
  <span class="sr-only">Open cart</span>
</button>

<!-- OPTION 3: aria-labelledby pointing to existing visible text -->
<h2 id="cart-heading">Your Cart</h2>
<button type="button" aria-labelledby="cart-heading">View</button>

<!-- OPTION 4: icon-only link with aria-label -->
<a href="/cart" aria-label="View shopping cart">
  <img src="cart-icon.svg" alt="" role="presentation" />
</a>

Apply one of these patterns to any button, link, or menuitem that contains only an icon or image.

ListModerate effort

Fix all HTML lists so that only valid child elements appear directly inside `<ul>` and `<ol>` tags, and only `<ul>` or `<ol>` elements wrap `<li>` items.

On Drupal

  1. Broken list markup in Drupal usually originates in a Twig template. Find the relevant template in your theme's 'templates/' folder (e.g. 'menu.html.twig', 'item-list.html.twig').
  2. Open the template and verify that any <ul> or <ol> loop outputs <li> wrappers correctly: {% for item in items %}<li>{{ item.content }}</li>{% endfor %}.
  3. If the issue is in a contributed module's template, copy the template into your theme's templates directory (Drupal's template override system), then fix it there.
  4. Run 'drush cr' (cache rebuild) or go to Configuration → Performance → Clear all caches, then re-audit the page.
Code · Generic HTML (html)
<!-- CORRECT: <ul> or <ol> with only <li> children -->
<ul>
  <li>First item</li>
  <li>Second item</li>
  <li>Third item
    <!-- CORRECT: nested list lives INSIDE an <li> -->
    <ul>
      <li>Nested item A</li>
      <li>Nested item B</li>
    </ul>
  </li>
</ul>

<!-- WRONG: <div> as a direct child of <ul> -->
<!-- <ul>
  <div>This breaks the list</div>
  <li>Item</li>
</ul> -->

<!-- WRONG: <li> with no list parent -->
<!-- <li>Orphaned item</li> -->

Correct list structure — all platforms. Use this as a reference for what valid markup looks like.

Security (OWASP) · 3 fixes

Hsts max age too shortQuick win

Increase your HSTS max-age directive to at least 31536000 seconds (1 year) so browsers enforce HTTPS connections for a meaningful period.

On Drupal

  1. Install and configure the 'Security Kit' (SecKit) module: Admin → Extend → Install Security Kit.
  2. Go to Admin → Configuration → System → Security Kit → HTTP Strict Transport Security: set 'Max-Age' to 31536000 and check 'Include Subdomains' if appropriate. Save.
  3. Alternatively set the header at the Nginx or Apache layer as described in the generic steps above.
  4. Clear Drupal caches: Admin → Configuration → Performance → Clear all caches.
Code · Generic HTML (http)
Strict-Transport-Security: max-age=31536000; includeSubDomains

This is an HTTP response header — it must be sent by your server, NOT as an HTML meta tag. The meta tag is not supported for HSTS.

Info disclosure x powered byQuick win

Remove or suppress the X-Powered-By HTTP response header so your server stops advertising which technology stack, hosting platform, or software version powers your store.

On Drupal

  1. In your sites/default/settings.php or a custom module's hook_boot(), call `header_remove('X-Powered-By');` or use `drupal_add_http_header('X-Powered-By', '');`
  2. Install the 'Security Kit' (seckit) module from drupal.org: Admin → Configuration → System → Security Kit — it provides options to strip information-disclosure headers.
  3. For Apache: Add `Header unset X-Powered-By` to your .htaccess or VirtualHost config.
  4. For Nginx: Add `more_clear_headers 'X-Powered-By';` to the server block.
Code · Generic HTML (html)
<!-- X-Powered-By is an HTTP response header set by the server, not in HTML markup.
     Use the server-side code snippets below for your specific platform. -->

There is no HTML tag for this — it is a server-side HTTP response header. Use the server/framework snippets below.

Missing content security policyModerate effort

Add a Content-Security-Policy (CSP) HTTP response header to every page to block unauthorized scripts, styles, and resources from loading in your visitors' browsers.

On Drupal

  1. Install the 'Security Kit' (seckit) Drupal module from drupal.org.
  2. Go to Admin → Configuration → Security → Security Kit.
  3. Enable Content Security Policy and fill in the directive fields (default-src, script-src, object-src, etc.).
  4. Enable 'Report Only' mode first, review Drupal's watchdog logs for violations, then switch to enforce mode.

Site Lifecycle · 1 fixes

Cms versionModerate effort

Update your CMS and ecommerce platform to the latest stable version immediately to close known security vulnerabilities and maintain vendor support.

On Drupal

  1. Log in to Drupal Admin → Reports → Available Updates to see your current Drupal core version and available updates for core and all modules.
  2. Apply updates using Composer: `composer update drupal/core --with-all-dependencies`, then run `drush updatedb` or use the web-based update script at /update.php.
  3. Enable the Update Manager module (Admin → Extend) to receive in-dashboard update notifications.
  4. Review Drupal's Security Advisories page and subscribe to security email alerts for your installed version.
Code · Generic HTML (html)
<!-- REMOVE or suppress this tag from your <head> — it advertises your CMS version to attackers -->
<!-- <meta name="generator" content="WordPress 5.8.1" /> -->

<!-- In WordPress, add this to your theme's functions.php to remove it: -->
<!-- remove_action('wp_head', 'wp_generator'); -->

If your site injects a generator meta tag exposing your CMS version publicly, remove it to reduce information exposure to attackers. This does not replace upgrading — it is a secondary hardening step.