Skip to content

[#291] Added SEO modules and social sharing cards. - #293

Merged
AlexSkrypnyk merged 11 commits into
developfrom
feature/291-seo-modules
Aug 5, 2026
Merged

[#291] Added SEO modules and social sharing cards.#293
AlexSkrypnyk merged 11 commits into
developfrom
feature/291-seo-modules

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Aug 5, 2026

Copy link
Copy Markdown
Member

Closes #291
Closes #292

Checklist before requesting a review

  • Subject includes ticket number as [#123] Verb in past tense.
  • Ticket number #123 added to description
  • Added context in Changed section
  • Self-reviewed code and commented in commented complex areas.
  • Added tests for fix/feature.
  • Relevant tests run and passed locally.

Changed

Before this branch the site published a description, a title and a canonical link, and nothing else. There were no og:* tags, no twitter:* tags and no structured data anywhere, so a link pasted into X, LinkedIn or Slack rendered as a bare URL. metatag_open_graph and metatag_twitter_cards were already installed but had no tags configured, which is why #292 reports the cards pulling nothing.

  1. Added schema_metatag (with schema_organization, schema_web_site, schema_article) and redirect_404. The first publishes JSON-LD through the metatag defaults that already exist, so it adds no second configuration surface. The second records 404 paths at /admin/config/search/redirect/404 so real lost traffic can be turned into redirects. Deliberately not added: yoast_seo, seo_checklist, linkchecker and metatag_hreflang - reasoning in docs/seo.md.

  2. Configured the Open Graph, Twitter Card and structured data defaults. global carries the site-level values, the card type and handle, and the Organization and WebSite markup; node__blog adds og:type: article, the article timestamps and the Article markup. Every page now emits a complete card, and blog posts additionally emit an Article + Organization + WebSite JSON-LD graph.

  3. Resolved the card image in MetatagsAlterHook rather than in configuration. A metatag default whose token resolves to nothing is dropped rather than falling back to its parent, and most pages have no thumbnail - the homepage, /about-us and most projects included - so a token-only setup would leave exactly the most-shared URLs with no image. The hook uses the node's field_c_n_thumbnail through a new social_share image style (1200x630, focal point aware) and falls back to a site-wide card otherwise. It passes over a thumbnail that no image toolkit can derive (the image field accepts SVG) or whose file is absent from the environment, and it leaves an image an editor set by hand entirely alone. The URL it produces is absolute, because only the Open Graph and Twitter tags declare absolute_url and get a host prepended for them - a relative fallback reached the Article structured data unchanged, which schema.org rejects.

  4. Derived the social title and description from title and description in the same hook. This keeps one source of truth for the wording, and it is the only way those tags reach the front page at all: metatag_get_default_tags() treats the front page, 403 and 404 as special pages and stops after the global and special defaults, never reading the entity or bundle defaults. The homepage had no og:description before this.

  5. Implemented the alter as a hook class, Drupal\do_base\Hook\MetatagsAlterHook, matching the src/Hook/*Hook.php pattern the module already uses for FormAlterHook and LibraryInfoAlterHook. It takes EntityTypeManagerInterface, ConfigFactoryInterface, ModuleExtensionList and RequestStack through the constructor, so nothing in the resolution path reaches for a static \Drupal:: call. do_base.module is untouched by this branch.

  6. Added two brand assets to do_base. social-share.jpg (1200x630) is the fallback card: the wordmark over the dark navy page background, under the site's positioning line with its accent phrase in coral. It is rendered from HTML carrying the same colour tokens, Lexend weights and letter spacing as the branding styleguide, so it matches the brand rather than approximating it. logo.png (600x142) is the Organization logo for the structured data, which requires a raster image where the brand logo exists only as SVG.

  7. Added tests at three levels. MetatagsAlterHookTest drives the hook in-process and covers every branch of the resolver - thumbnail, no thumbnail, no thumbnail field, media holding no image, SVG source, missing file, each editor-override permutation, the text mirroring, and the structured data image - reaching 100% of the class. SocialCardTest proves the same behaviour end to end through rendered pages, and the Behat feature asserts the rendered tags and JSON-LD against the real configuration and theme. The in-process test matters for more than coverage: a functional test exercises this code inside a separate HTTP request, where nothing can observe which branch ran.

  8. Documented the stack in docs/seo.md, including why publication dates come from created (it is the field the blog listing already sorts and presents by, and structured data is expected to agree with what a visitor sees).

Screenshots

The fallback card, served for any page without a thumbnail:

Default social share card

Before / After

BEFORE - every page, regardless of bundle
┌──────────────────────────────────────────────────────────┐
│ <title>                                                  │
│ <meta name="description">                                │
│ <link rel="canonical">                                   │
│                                                          │
│   (no og:*  ·  no twitter:*  ·  no JSON-LD)              │
└──────────────────────────────────────────────────────────┘
        │
        ▼  pasted into X / LinkedIn / Slack
┌──────────────────────────────────────────────────────────┐
│  https://www.drevops.com/blog/meet-vortex                │
│  (bare link, no card)                                    │
└──────────────────────────────────────────────────────────┘


AFTER - image resolution, in Drupal\do_base\Hook\MetatagsAlterHook
                       ┌──────────────────────────┐
   node with           │ field_c_n_thumbnail      │
   a thumbnail    ────►│   → social_share style   │───┐
                       │   → 1200x630, media alt  │   │
                       └──────────────────────────┘   │
                                                      ▼
   no thumbnail                                ┌─────────────┐
   · SVG source        ────────────────────────►│  og:image   │
   · file missing                              │ twitter:img │
   · not an entity     ┌──────────────────────┐ │ Article.img │
   (front, 403, 404)   │ assets/              │ └─────────────┘
                  ────►│  social-share.jpg    │───┘
                       │  1200x630, site name │
                       └──────────────────────┘

   editor set og:image by hand  ──────►  left untouched, no
                                         width/height/alt asserted


AFTER - text resolution   (config → hook, first non-empty wins)

   title ──────────────► og:title ──────────► twitter:title
   description ────────► og:description ────► twitter:description

   Mirroring is what carries these onto the front page, 403 and 404,
   where metatag never reads the bundle defaults.


AFTER - a blog post
┌──────────────────────────────────────────────────────────┐
│ <title> <meta description> <link canonical>              │
│ og:site_name  og:type=article  og:url  og:title          │
│ og:description  og:image (+width/height/alt)             │
│ article:published_time  article:modified_time            │
│ twitter:card=summary_large_image  twitter:site=@drev_ops │
│ twitter:title  twitter:description  twitter:image (+alt) │
│ <script type="application/ld+json">                      │
│   @graph: [ Article, Organization, WebSite ]             │
└──────────────────────────────────────────────────────────┘
        │
        ▼  pasted into X / LinkedIn / Slack
┌──────────────────────────────────────────────────────────┐
│ ┌──────────────────────────────────────────────────────┐ │
│ │                                                      │ │
│ │              [ 1200x630 card image ]                 │ │
│ │                                                      │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ Meet Vortex: your new secret weapon… | DrevOps       │ │
│ │ Vortex is a ready-to-go Drupal project template…     │ │
│ │ drevops.com                                          │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘

@AlexSkrypnyk AlexSkrypnyk added the A1 Board worker 1 label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 72bc5b7e-c2bc-470f-83aa-3d0a728471b0

📥 Commits

Reviewing files that changed from the base of the PR and between cb38d59 and 9505ac1.

⛔ Files ignored due to path filters (3)
  • composer.lock is excluded by !**/*.lock
  • web/modules/custom/do_base/assets/logo.png is excluded by !**/*.png
  • web/modules/custom/do_base/assets/social-share.jpg is excluded by !**/*.jpg
📒 Files selected for processing (16)
  • AGENTS.md
  • composer.json
  • config/default/core.extension.yml
  • config/default/image.style.social_share.yml
  • config/default/metatag.metatag_defaults.front.yml
  • config/default/metatag.metatag_defaults.global.yml
  • config/default/metatag.metatag_defaults.node.yml
  • config/default/metatag.metatag_defaults.node__blog.yml
  • config/default/redirect_404.settings.yml
  • config/default/views.view.redirect_404.yml
  • docs/README.md
  • docs/seo.md
  • tests/behat/features/metatags.feature
  • web/modules/custom/do_base/src/Hook/MetatagsAlterHook.php
  • web/modules/custom/do_base/tests/src/Functional/SocialCardTest.php
  • web/modules/custom/do_base/tests/src/Kernel/MetatagsAlterHookTest.php

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

@AlexSkrypnyk AlexSkrypnyk added the AUTOMERGE Pull request has been approved and set to automerge label Aug 5, 2026
@github-actions

This comment has been minimized.

@codecov-commenter

codecov-commenter commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.85%. Comparing base (cb38d59) to head (9505ac1).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #293      +/-   ##
===========================================
+ Coverage    81.49%   83.85%   +2.36%     
===========================================
  Files           23       24       +1     
  Lines          416      477      +61     
===========================================
+ Hits           339      400      +61     
  Misses          77       77              

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@AlexSkrypnyk AlexSkrypnyk added the Needs review Pull request needs a review from assigned developers label Aug 5, 2026
@AlexSkrypnyk
AlexSkrypnyk temporarily deployed to PR-293 August 5, 2026 07:19 Inactive
@github-actions

This comment has been minimized.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Code coverage (threshold: 80%)

  Classes: 47.83% (11/23)
  Methods: 73.91% (51/69)
  Lines:   87.67% (576/657)
Per-class coverage
Drupal\do_ai_alt_text\AltTextGenerator
  Methods:  85.71% ( 6/ 7)   Lines:  98.72% ( 77/ 78)
Drupal\do_ai_alt_text\Plugin\Action\RegenerateImageAltText
  Methods: 100.00% ( 8/ 8)   Lines: 100.00% ( 63/ 63)
Drupal\do_base\EventSubscriber\PreviewLinkCacheSubscriber
  Methods:  33.33% ( 1/ 3)   Lines:  11.11% (  1/  9)
Drupal\do_base\EventSubscriber\ThemeColorSubscriber
  Methods:  66.67% ( 2/ 3)   Lines:  40.00% (  4/ 10)
Drupal\do_base\Hook\MetatagsAlterHook
  Methods: 100.00% ( 7/ 7)   Lines: 100.00% ( 58/ 58)
Drupal\do_content_api\EventSubscriber\JsonApiWriteGateSubscriber
  Methods:  66.67% ( 2/ 3)   Lines:  88.89% (  8/  9)
Drupal\do_content_api\Hook\EntityCreateAccessHook
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 12/ 12)
Drupal\do_content_api\Hook\ModerationPolicyHook
  Methods:  50.00% ( 1/ 2)   Lines:  93.75% ( 15/ 16)
Drupal\do_content_api\Routing\RouteSubscriber
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  6/  6)
Drupal\do_feed\FeedUrlBuilder
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 18/ 18)
Drupal\do_feed\Form\FeedSettingsForm
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 15/ 15)
Drupal\do_feed\Hook\EntityDeleteHook
  Methods:  50.00% ( 1/ 2)   Lines:  92.31% ( 12/ 13)
Drupal\do_feed\Hook\EntityPresaveHook
  Methods: 100.00% ( 4/ 4)   Lines: 100.00% ( 54/ 54)
Drupal\do_feed\Hook\PreprocessParagraphHook
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 14/ 14)
Drupal\do_feed\Hook\PreprocessViewsViewRowRssHook
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  3/  3)
Drupal\do_feed\Hook\ViewsPreViewHook
  Methods:  50.00% ( 1/ 2)   Lines:  96.43% ( 27/ 28)
Drupal\do_generated_content\Generator\CaseMatrix
  Methods: 100.00% ( 3/ 3)   Lines: 100.00% ( 12/ 12)
Drupal\do_generated_content\Generator\RelativeDate
  Methods: 100.00% ( 1/ 1)   Lines: 100.00% (  1/  1)

@AlexSkrypnyk
AlexSkrypnyk merged commit 9cb9d3c into develop Aug 5, 2026
10 checks passed
@AlexSkrypnyk
AlexSkrypnyk deleted the feature/291-seo-modules branch August 5, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A1 Board worker 1 AUTOMERGE Pull request has been approved and set to automerge Needs review Pull request needs a review from assigned developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check twitter card pull correct images and content Add SEO modules

2 participants