Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:some.email@foo.bar', 'mailto:some.email@foo.bar'],
['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