Skip to content

Commit

Permalink
Merge pull request #9 from Flowpack/8-feature-enforce-no-trailing-slash
Browse files Browse the repository at this point in the history
TASK: add option to remove trailing slashes
  • Loading branch information
t-heuser authored Jan 31, 2025
2 parents 2185d3e + 6c1c122 commit c749146
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 21 deletions.
11 changes: 11 additions & 0 deletions Classes/Enum/TrailingSlashModeEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Flowpack\SeoRouting\Enum;

enum TrailingSlashModeEnum: string
{
case ADD = 'add';
case REMOVE = 'remove';
}
8 changes: 7 additions & 1 deletion Classes/Helper/ConfigurationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

namespace Flowpack\SeoRouting\Helper;

use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
use Neos\Flow\Annotations as Flow;

#[Flow\Scope('singleton')]
class ConfigurationHelper
{
/** @var array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int} */
/** @var array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int, trailingSlashMode: string} */
#[Flow\InjectConfiguration(path: 'redirect')]
protected array $configuration;

Expand All @@ -22,6 +23,11 @@ public function isTrailingSlashEnabled(): bool
return $this->configuration['enable']['trailingSlash'] ?? false;
}

public function getTrailingSlashMode(): TrailingSlashModeEnum
{
return TrailingSlashModeEnum::tryFrom($this->configuration['trailingSlashMode']) ?? TrailingSlashModeEnum::ADD;
}

public function isToLowerCaseEnabled(): bool
{
return $this->configuration['enable']['toLowerCase'] ?? false;
Expand Down
26 changes: 22 additions & 4 deletions Classes/Helper/TrailingSlashHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,40 @@
class TrailingSlashHelper
{
public function appendTrailingSlash(UriInterface $uri): UriInterface
{
if (! $this->shouldUriByHandled($uri)) {
return $uri;
}

return $uri->withPath(rtrim($uri->getPath(), '/') . '/');
}

public function removeTrailingSlash(UriInterface $uri): UriInterface
{
if (! $this->shouldUriByHandled($uri)) {
return $uri;
}

return $uri->withPath(rtrim($uri->getPath(), '/'));
}

private function shouldUriByHandled(UriInterface $uri): bool
{
// bypass links without path
if (strlen($uri->getPath()) === 0) {
return $uri;
return false;
}

// bypass links to files
if (array_key_exists('extension', pathinfo($uri->getPath()))) {
return $uri;
return false;
}

// bypass mailto and tel links
if (in_array($uri->getScheme(), ['mailto', 'tel'], true)) {
return $uri;
return false;
}

return $uri->withPath(rtrim($uri->getPath(), '/') . '/');
return true;
}
}
11 changes: 8 additions & 3 deletions Classes/LinkingServiceAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Flowpack\SeoRouting;

use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
use Flowpack\SeoRouting\Helper\BlocklistHelper;
use Flowpack\SeoRouting\Helper\ConfigurationHelper;
use Flowpack\SeoRouting\Helper\TrailingSlashHelper;
Expand All @@ -26,10 +27,10 @@ class LinkingServiceAspect
protected BlocklistHelper $blocklistHelper;

/**
* This ensures that all internal links are rendered with a trailing slash.
* This ensures that all internal links are rendered with/without a trailing slash, depending on configuration.
*/
#[Flow\Around('method(' . LinkingService::class . '->createNodeUri())')]
public function appendTrailingSlashToNodeUri(JoinPointInterface $joinPoint): string
public function handleTrailingSlashForNodeUri(JoinPointInterface $joinPoint): string
{
/** @var string $result */
$result = $joinPoint->getAdviceChain()->proceed($joinPoint);
Expand All @@ -48,6 +49,10 @@ public function appendTrailingSlashToNodeUri(JoinPointInterface $joinPoint): str
return $result;
}

return (string)$this->trailingSlashHelper->appendTrailingSlash($uri);
if ($this->configurationHelper->getTrailingSlashMode() === TrailingSlashModeEnum::ADD) {
return (string)$this->trailingSlashHelper->appendTrailingSlash($uri);
}

return (string)$this->trailingSlashHelper->removeTrailingSlash($uri);
}
}
6 changes: 5 additions & 1 deletion Classes/RoutingMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Flowpack\SeoRouting;

use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
use Flowpack\SeoRouting\Helper\BlocklistHelper;
use Flowpack\SeoRouting\Helper\ConfigurationHelper;
use Flowpack\SeoRouting\Helper\LowerCaseHelper;
Expand Down Expand Up @@ -50,7 +51,10 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
$oldPath = $uri->getPath();

if ($isTrailingSlashEnabled) {
$uri = $this->trailingSlashHelper->appendTrailingSlash($uri);
match ($this->configurationHelper->getTrailingSlashMode()) {
TrailingSlashModeEnum::ADD => $uri = $this->trailingSlashHelper->appendTrailingSlash($uri),
TrailingSlashModeEnum::REMOVE => $uri = $this->trailingSlashHelper->removeTrailingSlash($uri),
};
}

if ($isToLowerCaseEnabled) {
Expand Down
1 change: 1 addition & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Flowpack:
enable:
trailingSlash: true
toLowerCase: false
trailingSlashMode: 'add'
statusCode: 301
blocklist:
'/neos.*': true
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [Installation](#installation)
* [Configuration](#configuration)
* [Standard Configuration](#standard-configuration)
* [Trailing slash mode](#trailing-slash-mode)
* [Blocklist for redirects](#blocklist-for-redirects)
* [Thank you](#thank-you)

Expand All @@ -20,15 +21,15 @@ Thank you [Biallo & Team GmbH](https://www.biallo.de/) for sponsoring the work f

## Introduction

This package allows you to enforce a trailing slash and/or lower case urls in Flow/Neos.
This package allows you to enforce a trailing slash or enforce no trailing slash and/or lower case urls in Flow/Neos.

## Features

This package has 2 main features:
Main features:

- **trailingSlash**: ensure that all rendered internal links in the frontend end with a trailing slash (e.g. `example.
com/test/` instead of `example.com/test`) and all called URLs without trailing slash will be redirected to the same
page with a trailing slash
page with a trailing slash or the opposite (e.g. `example.com/test` instead of `example.com/test/`)
- **toLowerCase**: ensure that camelCase links gets redirected to lowercase (e.g. `example.com/lowercase` instead of
`example.com/lowerCase`)

Expand Down Expand Up @@ -68,11 +69,19 @@ Flowpack:
enable:
trailingSlash: true
toLowerCase: false
trailingSlashMode: 'add'
statusCode: 301
blocklist:
'/neos.*': true
```

### Trailing slash mode

You can set the `trailingSlashMode` to `add` or `remove`. For this setting to have an effect you have to set
`trailingSlash` to true.

This effects redirects and all rendered internal urls.

### Blocklist for redirects

By default, all `/neos` URLs are ignored for redirects. You can extend the blocklist array with regex as you like:
Expand Down
17 changes: 16 additions & 1 deletion Tests/Unit/Helper/ConfigurationHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Flowpack\SeoRouting\Tests\Unit\Helper;

use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
use Flowpack\SeoRouting\Helper\ConfigurationHelper;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -38,6 +39,20 @@ public function testIsTrailingSlashEnabledShouldReturnFalse(): void
self::assertFalse($this->configurationHelper->isTrailingSlashEnabled());
}

public function testGetTrailingSlashModeShouldReturnGivenMode(): void
{
$this->injectConfiguration(['trailingSlashMode' => 'remove']);

self::assertSame(TrailingSlashModeEnum::REMOVE, $this->configurationHelper->getTrailingSlashMode());
}

public function testGetTrailingSlashModeShouldReturnDefaultMode(): void
{
$this->injectConfiguration(['trailingSlashMode' => 'foo']);

self::assertSame(TrailingSlashModeEnum::ADD, $this->configurationHelper->getTrailingSlashMode());
}

public function testIsToLowerCaseEnabledShouldReturnTrue(): void
{
$this->injectConfiguration(['enable' => ['trailingSlash' => false, 'toLowerCase' => true]]);
Expand Down Expand Up @@ -76,7 +91,7 @@ public function testGetStatusCodeShouldReturnConfiguredValue(): void
}

/**
* @param array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int}|array{} $configuration
* @param array{enable?: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int, trailingSlashMode?: string}|array{} $configuration
*/
private function injectConfiguration(array $configuration): void
{
Expand Down
39 changes: 37 additions & 2 deletions Tests/Unit/Helper/TrailingSlashHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,26 @@
#[CoversClass(TrailingSlashHelper::class)]
class TrailingSlashHelperTest extends TestCase
{
#[DataProvider('urlDataProvider')]
#[DataProvider('urlDataProviderForAppendTrailingSlash')]
public function testAppendTrailingSlash(string $input, string $output): void
{
$uri = new Uri($input);

self::assertSame($output, (string)(new TrailingSlashHelper())->appendTrailingSlash($uri));
}

#[DataProvider('urlDataProviderForRemoveTrailingSlash')]
public function testRemoveTrailingSlash(string $input, string $output): void
{
$uri = new Uri($input);

self::assertSame($output, (string)(new TrailingSlashHelper())->removeTrailingSlash($uri));
}

/**
* @return array{string[]}
*/
public static function urlDataProvider(): array
public static function urlDataProviderForAppendTrailingSlash(): array
{
return [
['', ''],
Expand All @@ -47,4 +55,31 @@ public static function urlDataProvider(): array
['https://test.de/foo/bar.css', 'https://test.de/foo/bar.css'],
];
}

/**
* @return array{string[]}
*/
public static function urlDataProviderForRemoveTrailingSlash(): array
{
return [
['', ''],
['/', ''],
['/foo/', '/foo'],
['/foo/bar/', '/foo/bar'],
['https://test.de', 'https://test.de'],
['https://test.de', 'https://test.de'],
['https://test.de/foo/bar/', 'https://test.de/foo/bar'],
['https://test.de/foo/bar', 'https://test.de/foo/bar'],
['/foo/bar/?some-query=foo%20bar', '/foo/bar?some-query=foo%20bar'],
['/foo/bar/#some-fragment', '/foo/bar#some-fragment'],
['/foo/bar/?some-query=foo%20bar#some-fragment', '/foo/bar?some-query=foo%20bar#some-fragment'],
[
'https://test.de/foo/bar/?some-query=foo%20bar#some-fragment',
'https://test.de/foo/bar?some-query=foo%20bar#some-fragment',
],
['mailto:[email protected]', 'mailto:[email protected]'],
['tel:+4906516564', 'tel:+4906516564'],
['https://test.de/foo/bar.css', 'https://test.de/foo/bar.css'],
];
}
}
34 changes: 29 additions & 5 deletions Tests/Unit/LinkingServiceAspectTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Flowpack\SeoRouting\Tests\Unit;

use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
use Flowpack\SeoRouting\Helper\BlocklistHelper;
use Flowpack\SeoRouting\Helper\ConfigurationHelper;
use Flowpack\SeoRouting\Helper\TrailingSlashHelper;
Expand Down Expand Up @@ -61,7 +62,7 @@ public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfTrailingS

assertSame(
$result,
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
);
}

Expand All @@ -74,7 +75,7 @@ public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfUriIsMalf

assertSame(
$result,
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
);
}

Expand All @@ -89,24 +90,47 @@ public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfUriIsInBl

assertSame(
$result,
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
);
}

public function testAppendTrailingSlashToNodeUriShouldChangeResult(): void
public function testAppendTrailingSlashToNodeUriShouldAppendTrailingSlash(): void
{
$result = 'foo/';
$this->adviceChainMock->expects($this->once())->method('proceed')->willReturn('foo');

$this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(true);
$this->configurationHelperMock->expects($this->once())->method('getTrailingSlashMode')->willReturn(
TrailingSlashModeEnum::ADD
);
$this->blocklistHelperMock->expects($this->once())->method('isUriInBlocklist')->willReturn(false);
$this->trailingSlashHelperMock->expects($this->once())->method('appendTrailingSlash')->willReturn(
new Uri($result)
);

assertSame(
$result,
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
);
}

public function testAppendTrailingSlashToNodeUriShouldRemoveTrailingSlash(): void
{
$result = 'foo/';
$this->adviceChainMock->expects($this->once())->method('proceed')->willReturn('foo');

$this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(true);
$this->configurationHelperMock->expects($this->once())->method('getTrailingSlashMode')->willReturn(
TrailingSlashModeEnum::REMOVE
);
$this->blocklistHelperMock->expects($this->once())->method('isUriInBlocklist')->willReturn(false);
$this->trailingSlashHelperMock->expects($this->once())->method('removeTrailingSlash')->willReturn(
new Uri($result)
);

assertSame(
$result,
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
);
}
}
Loading

0 comments on commit c749146

Please sign in to comment.