If you, just like me, use Drupal’s Contact module to manage your site-wide contact form, you may have noticed that the contact form marked as default is available on two paths. This behavior is very bad for SEO. In this article, I will present a solution to you that solves this problem.

The problem we solve

Let’s say you have the Contact module installed and the feedback form supplied by it is also available. If this is the default form then it is both available on the /contact and on the /contact/feedback path.

This is because the Contact module defines two routes for rendering contact forms. One of them is the entity.contact_form.canonical with the /contact/{contact_form} path, where {contact_form} represents the entity ID of a contact form. The other route is contact.site_page with the /contact path that renders the contact form that is marked default.

And this is why we have two URIs for the same thing: the default contact form can be reached by the /contact page because… yes, because it is the default form. And it is also available on its canonical path, at /contact/feedback, because its ID is feedback.

Solution

My suggestion is: keep the contact.site_page (/contact path) for the default contact form, and redirect the canonical path of the default contact form to the contact.site_page route.

So, this functional test should pass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

namespace Drupal\Tests\st_contact\Functional;

use Drupal\Tests\BrowserTestBase;

/**
 * Tests contact form canonical route redirection.
 *
 * @group contact
 */
class ContactFormPathTest extends BrowserTestBase {

  /**
   * Modules to enable.
   *
   * @var array
   */
  public static $modules = ['block', 'contact_test', 'st_contact'];

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    parent::setUp();
    $this->drupalPlaceBlock('page_title_block');
    $this->drupalPlaceBlock('system_main_block');
  }

  /**
   * Tests node canonical route access.
   */
  public function testContactFormRoutes() {
    user_role_grant_permissions('anonymous', ['access site-wide contact form']);

    // Test that contact path is accessible.
    $this->drupalGet('contact');
    $this->assertSession()->addressEquals('contact');
    $this->assertSession()->statusCodeEquals('200');
    $this->assertSession()->pageTextContains('Website feedback');

    // Test that if the default forms canonical path is requested, we are
    // redirected to the canonical path.
    $this->drupalGet('contact/feedback');
    $this->assertSession()->addressEquals('contact');
    $this->assertSession()->statusCodeEquals('200');
    $this->assertSession()->pageTextContains('Website feedback');
  }

}

The implementation

If you followed my previous articles, you already know what I’m going to do to accomplish this. In a new route subscriber service we will replace the controller of the canonical route with a new one; and in the new controller, we will implement the logic above.

Let’s create a new module with the st_contact name(space)! Contents of st_contact.info.yml:

1
2
3
4
5
6
name: 'Standard Contact'
description: 'A module for contact forms that redirects the path of the default contact form to the contact.site_page route.'
core: '8.x'
type: module
dependencies:
  - drupal:contact

st_contact.services.yml:

1
2
3
4
5
services:
  st_contact.route_subscriber:
    class: Drupal\st_contact\Routing\RouteSubscriber
    tags:
      - { name: 'event_subscriber' }

The RouteSubscriber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace Drupal\st_contact\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

/**
 * Modifies contact form routes.
 */
class RouteSubscriber extends RouteSubscriberBase {

  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    if ($route = $collection->get('entity.contact_form.canonical')) {
      $route->setDefault('_controller', '\Drupal\st_contact\Controller\ContactController::contactSitePage');
    }
  }

}

The new controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

namespace Drupal\st_contact\Controller;

use Drupal\contact\ContactFormInterface;
use Drupal\contact\Controller\ContactController as CoreContactController;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Enhanced Controller for contact form routes.
 */
class ContactController extends CoreContactController {

  /**
   * Presents the site-wide contact form.
   *
   * @param \Drupal\contact\ContactFormInterface|null $contact_form
   *   The contact form to use.
   *
   * @return array|RedirectResponse
   *   The form as render array as expected by
   *   \Drupal\Core\Render\RendererInterface::render() or a redirect response.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
   *   Exception is thrown when user tries to access non existing default
   *   contact form.
   */
  public function contactSitePage(ContactFormInterface $contact_form = NULL) {
    if (!empty($contact_form)) {
      $config = $this->config('contact.settings');
      $default_form = $this->entityManager()
        ->getStorage('contact_form')
        ->load($config->get('default_form'));

      if ($contact_form->id() === $default_form->id()) {
        return new RedirectResponse(Url::fromRoute('contact.site_page', [], ['absolute' => TRUE])->toString(), 307);
      }
    }

    return parent::contactSitePage($contact_form);
  }

}

Problem solved 🥳!