Skip to content

[#675] Added SEO and head assertion steps to 'MetatagTrait'.#688

Merged
AlexSkrypnyk merged 4 commits into
mainfrom
feature/675-seo-head-assertions
Jul 8, 2026
Merged

[#675] Added SEO and head assertion steps to 'MetatagTrait'.#688
AlexSkrypnyk merged 4 commits into
mainfrom
feature/675-seo-head-assertions

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Jul 8, 2026

Copy link
Copy Markdown
Member

Closes #675

Summary

MetatagTrait previously only offered generic <meta> tag presence, content, and no-HTML assertions, leaving common SEO/head checks to be hand-rolled in consumer projects.

This PR adds higher-level SEO and head assertion steps on top of the existing generic assertions: canonical URL presence/absence/value, indexability (robots meta tag and X-Robots-Tag header) and robots-directive checks, hreflang alternate validity (self-reference and language-code format) and reciprocal return-link checks, and Open Graph / Twitter Card minimum-set and explicit-table assertions.

All new steps reuse the trait's existing meta-lookup internals and HelperTrait, and the trait reaches 100% line coverage.

Changes

Canonical URL - the canonical URL should be :url, the canonical URL should exist, and the canonical URL should not exist, comparing the <link rel="canonical"> href resolved to absolute form against the Mink base URL. The three steps treat an empty href consistently as "no canonical URL", so should exist fails and should not exist passes for a present-but-empty canonical.

Indexability and robots - the page should be indexable / should not be indexable (checks both the robots meta tag and the X-Robots-Tag response header for a noindex/none directive), plus the meta robots should include :directive / should not include :directive for direct directive assertions with whole-token, case-insensitive matching.

Hreflang - the hreflang alternates should be valid checks that at least one alternate exists, every hreflang value is a well-formed language code (or x-default), and a self-referencing alternate for the current URL is present. the hreflang alternates should have reciprocal return links first runs that validation, then fetches each non-self, non-x-default alternate out of band via cURL and asserts it links back to the current URL, resolving the fetched page's relative links against that page's own origin.

Open Graph and Twitter Card - the Open Graph tags should be valid / the following Open Graph tags should exist: and the Twitter Card tags should be valid / the following Twitter Card tags should exist:, asserting a default or caller-supplied set of properties is present and non-empty. The default required sets are exposed as overridable protected methods.

Internals - metatagAssertNoHtml() now delegates to a new shared metatagFindMeta() helper instead of duplicating the XPath lookup; the trait pulls in HelperTrait for comma-separated directive parsing (helperSplitCommaSeparated()). Out-of-band HTML parsing saves and restores the libxml internal-error mode.

Fixture module - adds a /mysite_core/test-robots-header route (with a value query parameter controlling the emitted header) to the mysite_core custom module in both the d10 and d11 fixture sites, so the X-Robots-Tag header path can be tested against a real HTTP response.

Fixtures - 10 new HTML fixtures under tests/behat/fixtures/: a fully-populated SEO page, a noindex page, an empty-canonical page, and hreflang variants (self-referencing pair, missing self-reference, missing return link, invalid language code, empty href, 404 target).

BDD coverage - tests/behat/features/metatag.feature grows from 8 to 38 scenarios: positive-path assertions for every new step plus @trait:MetatagTrait failure-message scenarios (run as Behat subprocesses) verifying each exception message, bringing MetatagTrait to 100% line coverage.

Docs - regenerated STEPS.md with the new step documentation and updated the MetatagTrait one-line summary in README.md.

Before / After

BEFORE                                  AFTER
┌──────────────────────────────┐        ┌──────────────────────────────────────────┐
│ MetatagTrait                 │        │ MetatagTrait                               │
├──────────────────────────────┤        ├──────────────────────────────────────────┤
│ • meta tag exists            │        │ • meta tag exists                          │
│ • meta tag does not exist    │        │ • meta tag does not exist                  │
│ • meta tag has no HTML markup│  ───▶  │ • meta tag has no HTML markup              │
│                              │        │ + canonical URL: value / exists / absent   │
│                              │        │ + indexable / not indexable                │
│                              │        │   (robots meta + X-Robots-Tag header)      │
│                              │        │ + robots directive includes / excludes     │
│                              │        │ + hreflang alternates valid                │
│                              │        │   (self-reference, language-code format)   │
│                              │        │ + hreflang alternates reciprocal           │
│                              │        │   (out-of-band cURL fetch)                 │
│                              │        │ + Open Graph tags valid / explicit set     │
│                              │        │ + Twitter Card tags valid / explicit set   │
└──────────────────────────────┘        └──────────────────────────────────────────┘

Added canonical URL, indexability, robots directive, hreflang and social-tag assertions on top of the existing generic meta-tag assertion, reusing its lookup internals:
- Canonical URL presence, absence and value.
- Indexability via the robots meta tag and the 'X-Robots-Tag' response header, plus robots directive inclusion checks.
- hreflang alternate validity (self-reference and language codes) and reciprocal return links fetched out of band.
- Open Graph and Twitter Card minimum-set and explicit-set assertions.

Added a fixture module route that emits an 'X-Robots-Tag' header and BDD coverage for every new step.

Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC
…rage.

Matched the full indexability failure messages and added failure scenarios for the table-driven Open Graph and Twitter Card 'should exist' steps.

Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

MetatagTrait now includes SEO/head assertions for canonical URLs, robots/indexability, hreflang, and Open Graph/Twitter Card tags. The PR also adds supporting fixtures, Drupal test endpoints, Behat scenarios, and updated step documentation.

Changes

SEO metatag assertions

Layer / File(s) Summary
Trait imports, docblock, and shared meta lookup refactor
src/MetatagTrait.php
Expands imports/docblock, mixes in HelperTrait, and refactors metatagAssertNoHtml to use metatagFindMeta.
Public SEO assertion step methods
src/MetatagTrait.php
Adds #[Then] step methods for canonical, indexability, robots directives, hreflang validity/reciprocity, and Open Graph/Twitter Card tag assertions.
Protected helper implementation
src/MetatagTrait.php
Adds helpers for meta lookup, canonical/robots/hreflang parsing, indexability checks, curl-based reciprocal hreflang verification, URL resolution, and required property lists.
Drupal test-support route/controller
tests/behat/fixtures_drupal/d10/.../mysite_core.routing.yml, tests/behat/fixtures_drupal/d10/.../TestContent.php, tests/behat/fixtures_drupal/d11/.../mysite_core.routing.yml, tests/behat/fixtures_drupal/d11/.../TestContent.php
Adds test_robots_header route and controller action returning an X-Robots-Tag response for both Drupal 10 and 11 fixtures.
SEO and hreflang HTML fixtures
tests/behat/fixtures/metatags_seo.html, tests/behat/fixtures/metatags_noindex.html, tests/behat/fixtures/metatags_canonical_empty.html, tests/behat/fixtures/metatags_hreflang_*.html
Adds HTML fixtures covering canonical, robots, hreflang (valid/invalid/empty/no-self/no-return/404), and Open Graph/Twitter Card tag scenarios.
Behat feature scenarios
tests/behat/features/metatag.feature
Adds success and failure scenarios for canonical, indexability/robots, hreflang, and Open Graph/Twitter Card assertions with exact error-message checks.
Documentation updates
README.md, STEPS.md
Updates the generic steps index and step documentation to describe the new SEO/head assertions with Gherkin examples.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scenario as Behat scenario
  participant Trait as MetatagTrait
  participant curl as curl
  participant AltPage as alternate page
  Scenario->>Trait: metatagAssertHreflangReciprocal()
  Trait->>Trait: collect alternates and resolve URLs
  Trait->>curl: fetch alternate URL
  curl->>AltPage: HTTP GET
  AltPage-->>curl: HTML response
  curl-->>Trait: fetched HTML
  Trait->>Trait: verify return link to current URL
  Trait-->>Scenario: pass or fail
Loading

Suggested labels: Needs review

Poem

A rabbit hopped through meta and head,
Canonical, robots, hreflang spread,
Open Graph cards and Twitter too,
Thump-thump—SEO checks came through 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested canonical, robots, hreflang, and Open Graph/Twitter assertions, with docs and Behat coverage updated.
Out of Scope Changes check ✅ Passed The added docs, fixtures, and test route all support the new SEO/head assertions and appear in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding SEO and head assertion steps to MetatagTrait.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/675-seo-head-assertions

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/MetatagTrait.php`:
- Around line 319-320: The reciprocal hreflang check is resolving link hrefs
against the Mink base URL instead of the fetched alternate page URL. Update
metatagHtmlLinksBackTo() so relative links are resolved using the alternate page
URL returned by metatagFetchUrl() (or pass that URL through from the callers in
MetatagTrait), ensuring nested paths and cross-domain alternates are checked
against the correct base. Apply the same fix in the other hreflang validation
call sites mentioned by metatagHtmlLinksBackTo(), metatagFetchUrl(), and the
related hreflang verification logic.
- Around line 175-178: The canonical check in
MetatagTrait::metatagGetCanonicalHref handling is only rejecting non-empty href
values, so an empty href incorrectly passes as “not set.” Update the
canonical-existence logic to treat any present canonical link element as present
even when href is empty, and keep throwing the exception from the surrounding
MetatagTrait validation when a canonical tag exists regardless of its href
contents.
- Around line 574-577: Save the current libxml internal error setting before
calling libxml_use_internal_errors(TRUE) in MetatagTrait’s HTML parsing flow,
then restore that saved value in a finally block after DOMDocument::loadHTML and
libxml_clear_errors run. Update the same code path so the parser state change is
scoped locally and later callers do not inherit the suppressed error mode.
- Around line 303-319: metatagAssertHreflangReciprocal() is using alternate href
values directly, so empty or invalid links can be resolved and fetched before
validation. Update the reciprocal check to validate each alternate with
metatagAssertHreflangValid() (or equivalent validation flow) before calling
metatagResolveUrl() or metatagFetchUrl(), and skip or fail fast on invalid
alternates so the existing clear validation error is surfaced instead of
treating empty hrefs as the base URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f5fa2117-7e4b-48a3-8062-f2a0aaf6ed30

📥 Commits

Reviewing files that changed from the base of the PR and between e9ee7d2 and 9d6125b.

📒 Files selected for processing (17)
  • README.md
  • STEPS.md
  • src/MetatagTrait.php
  • tests/behat/features/metatag.feature
  • tests/behat/fixtures/metatags_hreflang_404.html
  • tests/behat/fixtures/metatags_hreflang_de.html
  • tests/behat/fixtures/metatags_hreflang_emptyhref.html
  • tests/behat/fixtures/metatags_hreflang_en.html
  • tests/behat/fixtures/metatags_hreflang_invalid.html
  • tests/behat/fixtures/metatags_hreflang_noreturn.html
  • tests/behat/fixtures/metatags_hreflang_noself.html
  • tests/behat/fixtures/metatags_noindex.html
  • tests/behat/fixtures/metatags_seo.html
  • tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/mysite_core.routing.yml
  • tests/behat/fixtures_drupal/d10/web/modules/custom/mysite_core/src/Controller/TestContent.php
  • tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/mysite_core.routing.yml
  • tests/behat/fixtures_drupal/d11/web/modules/custom/mysite_core/src/Controller/TestContent.php

Comment thread src/MetatagTrait.php
Comment thread src/MetatagTrait.php Outdated
Comment thread src/MetatagTrait.php Outdated
Comment thread src/MetatagTrait.php Outdated
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.27%. Comparing base (e9ee7d2) to head (965ff99).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #688      +/-   ##
==========================================
+ Coverage   97.19%   97.27%   +0.07%     
==========================================
  Files          52       52              
  Lines        4283     4404     +121     
==========================================
+ Hits         4163     4284     +121     
  Misses        120      120              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…ing.

- Failed 'the canonical URL should not exist' when a canonical link element is present with an empty href.
- Validated hreflang alternates before reciprocal fetches so an empty href no longer resolves to the base URL.
- Resolved a fetched alternate page's relative links against that page's own origin instead of the Mink base URL.
- Saved and restored the libxml internal-error mode around out-of-band HTML parsing.

Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/MetatagTrait.php`:
- Around line 613-626: Handle document-relative hreflang/canonical URLs in
metatagResolveUrl() or document that only root-relative links are supported. The
current origin-only resolution in metatagResolveUrl() strips the fetched page
path, so values like about.html or ../en/about are resolved against the site
origin instead of the current document URL, which breaks
metatagHtmlLinksBackTo() and the self-reference check. Update the resolution
logic to use the fetched page URL when provided (or otherwise resolve relative
to the document context), and keep the special handling for absolute and
root-relative URLs intact.

In `@tests/behat/features/metatag.feature`:
- Around line 206-219: Treat empty canonical href values as absent in
metatagAssertCanonicalNotExists() so it matches metatagAssertCanonicalExists()
and metatagAssertCanonicalEquals(). Update the assertion logic in the
MetatagTrait step implementation to consider both NULL and empty string as
missing, and adjust the behat scenario for “Then the canonical URL should not
exist” to expect success when the canonical link has href="" instead of a
failure message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c23c9a4e-0225-4095-aa99-71516411119c

📥 Commits

Reviewing files that changed from the base of the PR and between 9d6125b and a9b6c91.

📒 Files selected for processing (3)
  • src/MetatagTrait.php
  • tests/behat/features/metatag.feature
  • tests/behat/fixtures/metatags_canonical_empty.html

Comment thread src/MetatagTrait.php
Comment thread tests/behat/features/metatag.feature Outdated
…solution.

- Treated an empty canonical href as absent in 'the canonical URL should not exist', consistent with the presence and value assertions.
- Documented that 'metatagResolveUrl()' supports absolute and root-relative URLs and resolves document-relative URLs against the origin.

Claude-Session: https://claude.ai/code/session_012WhCKmn7GbhNCQgfXmWqXC
@AlexSkrypnyk AlexSkrypnyk added the Needs review Pull request needs a review from assigned developers label Jul 8, 2026
@AlexSkrypnyk AlexSkrypnyk merged commit 3d645bd into main Jul 8, 2026
14 checks passed
@AlexSkrypnyk AlexSkrypnyk deleted the feature/675-seo-head-assertions branch July 8, 2026 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs review Pull request needs a review from assigned developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SEO and head assertion steps to MetatagTrait

1 participant