Skip to content

Commit 6c1c122

Browse files
committed
TASK: add option to remove trailing slashes
1 parent 2185d3e commit 6c1c122

11 files changed

+166
-21
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\SeoRouting\Enum;
6+
7+
enum TrailingSlashModeEnum: string
8+
{
9+
case ADD = 'add';
10+
case REMOVE = 'remove';
11+
}

Classes/Helper/ConfigurationHelper.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
namespace Flowpack\SeoRouting\Helper;
66

7+
use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
78
use Neos\Flow\Annotations as Flow;
89

910
#[Flow\Scope('singleton')]
1011
class ConfigurationHelper
1112
{
12-
/** @var array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int} */
13+
/** @var array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int, trailingSlashMode: string} */
1314
#[Flow\InjectConfiguration(path: 'redirect')]
1415
protected array $configuration;
1516

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

26+
public function getTrailingSlashMode(): TrailingSlashModeEnum
27+
{
28+
return TrailingSlashModeEnum::tryFrom($this->configuration['trailingSlashMode']) ?? TrailingSlashModeEnum::ADD;
29+
}
30+
2531
public function isToLowerCaseEnabled(): bool
2632
{
2733
return $this->configuration['enable']['toLowerCase'] ?? false;

Classes/Helper/TrailingSlashHelper.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,40 @@
1111
class TrailingSlashHelper
1212
{
1313
public function appendTrailingSlash(UriInterface $uri): UriInterface
14+
{
15+
if (! $this->shouldUriByHandled($uri)) {
16+
return $uri;
17+
}
18+
19+
return $uri->withPath(rtrim($uri->getPath(), '/') . '/');
20+
}
21+
22+
public function removeTrailingSlash(UriInterface $uri): UriInterface
23+
{
24+
if (! $this->shouldUriByHandled($uri)) {
25+
return $uri;
26+
}
27+
28+
return $uri->withPath(rtrim($uri->getPath(), '/'));
29+
}
30+
31+
private function shouldUriByHandled(UriInterface $uri): bool
1432
{
1533
// bypass links without path
1634
if (strlen($uri->getPath()) === 0) {
17-
return $uri;
35+
return false;
1836
}
1937

2038
// bypass links to files
2139
if (array_key_exists('extension', pathinfo($uri->getPath()))) {
22-
return $uri;
40+
return false;
2341
}
2442

2543
// bypass mailto and tel links
2644
if (in_array($uri->getScheme(), ['mailto', 'tel'], true)) {
27-
return $uri;
45+
return false;
2846
}
2947

30-
return $uri->withPath(rtrim($uri->getPath(), '/') . '/');
48+
return true;
3149
}
3250
}

Classes/LinkingServiceAspect.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flowpack\SeoRouting;
66

7+
use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
78
use Flowpack\SeoRouting\Helper\BlocklistHelper;
89
use Flowpack\SeoRouting\Helper\ConfigurationHelper;
910
use Flowpack\SeoRouting\Helper\TrailingSlashHelper;
@@ -26,10 +27,10 @@ class LinkingServiceAspect
2627
protected BlocklistHelper $blocklistHelper;
2728

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

51-
return (string)$this->trailingSlashHelper->appendTrailingSlash($uri);
52+
if ($this->configurationHelper->getTrailingSlashMode() === TrailingSlashModeEnum::ADD) {
53+
return (string)$this->trailingSlashHelper->appendTrailingSlash($uri);
54+
}
55+
56+
return (string)$this->trailingSlashHelper->removeTrailingSlash($uri);
5257
}
5358
}

Classes/RoutingMiddleware.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flowpack\SeoRouting;
66

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

5253
if ($isTrailingSlashEnabled) {
53-
$uri = $this->trailingSlashHelper->appendTrailingSlash($uri);
54+
match ($this->configurationHelper->getTrailingSlashMode()) {
55+
TrailingSlashModeEnum::ADD => $uri = $this->trailingSlashHelper->appendTrailingSlash($uri),
56+
TrailingSlashModeEnum::REMOVE => $uri = $this->trailingSlashHelper->removeTrailingSlash($uri),
57+
};
5458
}
5559

5660
if ($isToLowerCaseEnabled) {

Configuration/Settings.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Flowpack:
44
enable:
55
trailingSlash: true
66
toLowerCase: false
7+
trailingSlashMode: 'add'
78
statusCode: 301
89
blocklist:
910
'/neos.*': true

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* [Installation](#installation)
1010
* [Configuration](#configuration)
1111
* [Standard Configuration](#standard-configuration)
12+
* [Trailing slash mode](#trailing-slash-mode)
1213
* [Blocklist for redirects](#blocklist-for-redirects)
1314
* [Thank you](#thank-you)
1415

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

2122
## Introduction
2223

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

2526
## Features
2627

27-
This package has 2 main features:
28+
Main features:
2829

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

@@ -68,11 +69,19 @@ Flowpack:
6869
enable:
6970
trailingSlash: true
7071
toLowerCase: false
72+
trailingSlashMode: 'add'
7173
statusCode: 301
7274
blocklist:
7375
'/neos.*': true
7476
```
7577

78+
### Trailing slash mode
79+
80+
You can set the `trailingSlashMode` to `add` or `remove`. For this setting to have an effect you have to set
81+
`trailingSlash` to true.
82+
83+
This effects redirects and all rendered internal urls.
84+
7685
### Blocklist for redirects
7786

7887
By default, all `/neos` URLs are ignored for redirects. You can extend the blocklist array with regex as you like:

Tests/Unit/Helper/ConfigurationHelperTest.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flowpack\SeoRouting\Tests\Unit\Helper;
66

7+
use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
78
use Flowpack\SeoRouting\Helper\ConfigurationHelper;
89
use PHPUnit\Framework\Attributes\CoversClass;
910
use PHPUnit\Framework\TestCase;
@@ -38,6 +39,20 @@ public function testIsTrailingSlashEnabledShouldReturnFalse(): void
3839
self::assertFalse($this->configurationHelper->isTrailingSlashEnabled());
3940
}
4041

42+
public function testGetTrailingSlashModeShouldReturnGivenMode(): void
43+
{
44+
$this->injectConfiguration(['trailingSlashMode' => 'remove']);
45+
46+
self::assertSame(TrailingSlashModeEnum::REMOVE, $this->configurationHelper->getTrailingSlashMode());
47+
}
48+
49+
public function testGetTrailingSlashModeShouldReturnDefaultMode(): void
50+
{
51+
$this->injectConfiguration(['trailingSlashMode' => 'foo']);
52+
53+
self::assertSame(TrailingSlashModeEnum::ADD, $this->configurationHelper->getTrailingSlashMode());
54+
}
55+
4156
public function testIsToLowerCaseEnabledShouldReturnTrue(): void
4257
{
4358
$this->injectConfiguration(['enable' => ['trailingSlash' => false, 'toLowerCase' => true]]);
@@ -76,7 +91,7 @@ public function testGetStatusCodeShouldReturnConfiguredValue(): void
7691
}
7792

7893
/**
79-
* @param array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int}|array{} $configuration
94+
* @param array{enable?: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int, trailingSlashMode?: string}|array{} $configuration
8095
*/
8196
private function injectConfiguration(array $configuration): void
8297
{

Tests/Unit/Helper/TrailingSlashHelperTest.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@
1313
#[CoversClass(TrailingSlashHelper::class)]
1414
class TrailingSlashHelperTest extends TestCase
1515
{
16-
#[DataProvider('urlDataProvider')]
16+
#[DataProvider('urlDataProviderForAppendTrailingSlash')]
1717
public function testAppendTrailingSlash(string $input, string $output): void
1818
{
1919
$uri = new Uri($input);
2020

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

24+
#[DataProvider('urlDataProviderForRemoveTrailingSlash')]
25+
public function testRemoveTrailingSlash(string $input, string $output): void
26+
{
27+
$uri = new Uri($input);
28+
29+
self::assertSame($output, (string)(new TrailingSlashHelper())->removeTrailingSlash($uri));
30+
}
31+
2432
/**
2533
* @return array{string[]}
2634
*/
27-
public static function urlDataProvider(): array
35+
public static function urlDataProviderForAppendTrailingSlash(): array
2836
{
2937
return [
3038
['', ''],
@@ -47,4 +55,31 @@ public static function urlDataProvider(): array
4755
['https://test.de/foo/bar.css', 'https://test.de/foo/bar.css'],
4856
];
4957
}
58+
59+
/**
60+
* @return array{string[]}
61+
*/
62+
public static function urlDataProviderForRemoveTrailingSlash(): array
63+
{
64+
return [
65+
['', ''],
66+
['/', ''],
67+
['/foo/', '/foo'],
68+
['/foo/bar/', '/foo/bar'],
69+
['https://test.de', 'https://test.de'],
70+
['https://test.de', 'https://test.de'],
71+
['https://test.de/foo/bar/', 'https://test.de/foo/bar'],
72+
['https://test.de/foo/bar', 'https://test.de/foo/bar'],
73+
['/foo/bar/?some-query=foo%20bar', '/foo/bar?some-query=foo%20bar'],
74+
['/foo/bar/#some-fragment', '/foo/bar#some-fragment'],
75+
['/foo/bar/?some-query=foo%20bar#some-fragment', '/foo/bar?some-query=foo%20bar#some-fragment'],
76+
[
77+
'https://test.de/foo/bar/?some-query=foo%20bar#some-fragment',
78+
'https://test.de/foo/bar?some-query=foo%20bar#some-fragment',
79+
],
80+
['mailto:[email protected]', 'mailto:[email protected]'],
81+
['tel:+4906516564', 'tel:+4906516564'],
82+
['https://test.de/foo/bar.css', 'https://test.de/foo/bar.css'],
83+
];
84+
}
5085
}

Tests/Unit/LinkingServiceAspectTest.php

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flowpack\SeoRouting\Tests\Unit;
66

7+
use Flowpack\SeoRouting\Enum\TrailingSlashModeEnum;
78
use Flowpack\SeoRouting\Helper\BlocklistHelper;
89
use Flowpack\SeoRouting\Helper\ConfigurationHelper;
910
use Flowpack\SeoRouting\Helper\TrailingSlashHelper;
@@ -61,7 +62,7 @@ public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfTrailingS
6162

6263
assertSame(
6364
$result,
64-
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
65+
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
6566
);
6667
}
6768

@@ -74,7 +75,7 @@ public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfUriIsMalf
7475

7576
assertSame(
7677
$result,
77-
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
78+
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
7879
);
7980
}
8081

@@ -89,24 +90,47 @@ public function testAppendTrailingSlashToNodeUriShouldNotChangeResultIfUriIsInBl
8990

9091
assertSame(
9192
$result,
92-
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
93+
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
9394
);
9495
}
9596

96-
public function testAppendTrailingSlashToNodeUriShouldChangeResult(): void
97+
public function testAppendTrailingSlashToNodeUriShouldAppendTrailingSlash(): void
9798
{
9899
$result = 'foo/';
99100
$this->adviceChainMock->expects($this->once())->method('proceed')->willReturn('foo');
100101

101102
$this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(true);
103+
$this->configurationHelperMock->expects($this->once())->method('getTrailingSlashMode')->willReturn(
104+
TrailingSlashModeEnum::ADD
105+
);
102106
$this->blocklistHelperMock->expects($this->once())->method('isUriInBlocklist')->willReturn(false);
103107
$this->trailingSlashHelperMock->expects($this->once())->method('appendTrailingSlash')->willReturn(
104108
new Uri($result)
105109
);
106110

107111
assertSame(
108112
$result,
109-
$this->linkingServiceAspect->appendTrailingSlashToNodeUri($this->joinPointMock)
113+
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
114+
);
115+
}
116+
117+
public function testAppendTrailingSlashToNodeUriShouldRemoveTrailingSlash(): void
118+
{
119+
$result = 'foo/';
120+
$this->adviceChainMock->expects($this->once())->method('proceed')->willReturn('foo');
121+
122+
$this->configurationHelperMock->expects($this->once())->method('isTrailingSlashEnabled')->willReturn(true);
123+
$this->configurationHelperMock->expects($this->once())->method('getTrailingSlashMode')->willReturn(
124+
TrailingSlashModeEnum::REMOVE
125+
);
126+
$this->blocklistHelperMock->expects($this->once())->method('isUriInBlocklist')->willReturn(false);
127+
$this->trailingSlashHelperMock->expects($this->once())->method('removeTrailingSlash')->willReturn(
128+
new Uri($result)
129+
);
130+
131+
assertSame(
132+
$result,
133+
$this->linkingServiceAspect->handleTrailingSlashForNodeUri($this->joinPointMock)
110134
);
111135
}
112136
}

0 commit comments

Comments
 (0)