One of my client works with an SEO agency and they ask me to remove the pagination parameter ?page=x (generated by the Views module) and rewrite it like this:





Is there a way to do it with Drupal? I don't see any option in the view or in Pathauto to do this.

Is it possible to handle this with a custom module or should I work with .htaccess file?

I've tried the Clean Pagination module but you have to manually set pages where clean pagination should work and the module doesn't seem to work properly.


1 Answer 1


You should write a PathProcessor that implements:

  • Drupal\Core\PathProcessor\InboundPathProcessorInterface
  • Drupal\Core\PathProcessor\OutboundPathProcessorInterface


file: modules/custom/mymodule/src/PathProcessor/MymodulePathProcessor.php

namespace Drupal\mymodule\PathProcessor;

use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;

class MymodulePathProcessor implements InboundPathProcessorInterface, OutboundPathProcessorInterface {

  public function processInbound($path, Request $request) {
    if (strpos($path, '/my-page-') !== 0) {
       return $path;
    $page =  preg_replace("/^\/my-page-(\d+)/", '$1', $path);
      if (ctype_digit($page)) {
        \Drupal::request()->query->set('page', $page);
        return "/my-page";
    else {
      \Drupal::request()->query->set('page', 0);
      return "/my-page";
    return $path;

  public function processOutbound($path, &$options = [], Request $request = NULL, BubbleableMetadata $bubbleable_metadata = NULL) {
    if ($path === '/my-page' && isset($options['query']['page'])) {
      $page = $options['query']['page'];
      return '/my-page-' . $page;
    return $path;


file: modules/custom/mymodule/mymodule.services.yml

    class: Drupal\mymodule\PathProcessor\MymodulePathProcessor
    - { name: path_processor_outbound}
    - { name: path_processor_inbound}

We also need to do a custom alteration to the pager. For the full pager the links are not created as the usual link render elements, so they don't go through the processOutboud, instead the twig file (classy/templates/navigation/pager.html.twig) uses a custom format. The next hook function does the adjustment.

file: modules/custom/mymodule/mymodule.module

 * Implements hook_preprocess_HOOK().
 * Implements hook_preprocess_pager().
 * @param $variables
function mymodule_preprocess_pager(&$variables) {
  $current_path = \Drupal::service('path.current')->getPath();
  if ($current_path === '/my-page')
  foreach ($variables['items']['pages'] as &$page_link) {
    $page =  preg_replace("/^\?page=(\d+)/", '$1', $page_link['href']);
    $page_link['href'] = 'my-page-' . $page;
  • Since your answer was upvoted multiple times, I guess the solution is here. But when I try it, I have this error : Symfony\Component\HttpKernel\Exception\NotFoundHttpException: No route found for "GET /my-page" in Symfony\Component\HttpKernel\EventListener\RouterListener->onKernelRequest() Do you have any suggestions ? Sep 13, 2018 at 12:29
  • I just updated the code, turns out you can't just pass the page parameter in the $path. You have to add it to the Request object like this: \Drupal::request()->query->set('page', $page); Sep 13, 2018 at 20:18
  • Thank you for taking the time to study this question. Did you test your module or is it untested code ? I’ve tested it, I have no more No route found error but it doesn’t rewrite the url. If it is working for you, I’m doing something wrong, that’s why I ask. Sep 16, 2018 at 20:03
  • You are welcome! I did test it... Sep 16, 2018 at 21:42
  • 2
    Yes, I’ve tested your updated code but I still have some issue (I test it on a clean new Drupal 8 install). For testing, I create a View that list content type article and set pagination. The URL are rewritten but content is not updated and there are issue with next/prev button. Be sure ì will mark your answer as correct when I will be able to make it work. Sep 30, 2018 at 9:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.