-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f6c17f1
commit 2568a1c
Showing
9 changed files
with
338 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
'Neos.Neos:Document': | ||
properties: | ||
uriPathSegment: | ||
validation: | ||
'Neos.Neos/Validation/RegularExpressionValidator': | ||
regularExpression: '/^[a-z0-9\-]+$' #override original regex (removes insensitive) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,52 @@ | ||
[![CircleCI](https://circleci.com/gh/t3n/seo-routing.svg?style=svg)](https://circleci.com/gh/t3n/seo-routing) [![Latest Stable Version](https://poser.pugx.org/t3n/seo-routing/v/stable)](https://packagist.org/packages/t3n/seo-routing) [![Total Downloads](https://poser.pugx.org/t3n/seo-routing/downloads)](https://packagist.org/packages/t3n/seo-routing) [![License](https://poser.pugx.org/t3n/seo-routing/license)](https://packagist.org/packages/t3n/seo-routing) | ||
|
||
# t3n.SEO.Routing | ||
Package to ensure that all links end with a trailing slash, e.g. `example.com/test/` instead of `example.com/test. | ||
|
||
This package has 2 main features: | ||
- **trailingSlash**: ensure that all links ends with a trailing slash (e.g. `example.com/test/ instead of `example.com/test) | ||
- **toLowerCase**: ensure that camelCase links gets redirected to lowercase (e.g. `exmaple.com/lowercase` instead of `exmaple.com/lowerCase` ) | ||
|
||
You can de- and activate both of them. | ||
|
||
Another small feature is to restrict all _new_ neos pages to have a lowercased `uriPathSegment`. This is done by extending the `NodeTypes.Document.yaml`. | ||
|
||
## Installation | ||
|
||
Just require it via composer:` | ||
|
||
```composer require t3n/seo-routing``` | ||
|
||
## Configuration | ||
|
||
By default, all `/neos/` URLs are ignored. You can extend the blacklist array with regex as you like. | ||
### Standard Configuration | ||
|
||
```yaml | ||
In the standard configuration we have activated the trailingSlash (to redirect all uris without a / at the and to an uri with / at the end) and do all redirects with a 301 http status. | ||
|
||
*Note: The lowercase redirect is deactivated by default, cause you have to make sure, that there is no `uriPathSegment` with camelCase or upperspace letters - this would lead to redirects in the neverland.* | ||
|
||
``` | ||
t3n: | ||
SEO: | ||
Routing: | ||
redirect: | ||
enable: true | ||
statusCode: 303 | ||
enable: | ||
trailingSlash: true | ||
toLowerCase: false | ||
statusCode: 301 | ||
blacklist: | ||
'/neos/.*': true | ||
``` | ||
|
||
### Blacklist for redirects | ||
|
||
By default, all `/neos/` URLs are ignored for redirects. You can extend the blacklist array with regex as you like: | ||
|
||
```yaml | ||
t3n: | ||
SEO: | ||
Routing: | ||
#redirect: | ||
#... | ||
blacklist: | ||
'/neos/.*': true | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace t3n\SEO\Routing\Tests\Unit; | ||
|
||
use Neos\Flow\Http\Component\ComponentContext; | ||
use Neos\Flow\Http\Request; | ||
use Neos\Flow\Http\Response; | ||
use Neos\Flow\Http\Uri; | ||
use Neos\Flow\Mvc\Routing\Router; | ||
use Neos\Flow\Tests\UnitTestCase; | ||
use t3n\SEO\Routing\RoutingComponent; | ||
|
||
class RoutingComponentTest extends UnitTestCase | ||
{ | ||
/** | ||
* @return mixed[] | ||
*/ | ||
public function invalidUrisWithConfig(): array | ||
{ | ||
// invalidUrl, validUrl, trailingSlash, toLowerCase | ||
return [ | ||
['http://dev.local/invalid', 'http://dev.local/invalid/', true, false], | ||
['http://dev.local/invalId', 'http://dev.local/invalid/', true, true], | ||
['http://dev.local/invalId/', 'http://dev.local/invalid/', false, true] | ||
]; | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function uriWithSlashGetsNotModifiedForTrailingSlash(): void | ||
{ | ||
$routingComponent = new RoutingComponent(); | ||
|
||
$uri = new Uri('http://dev.local/testpath/'); | ||
$newUri = $routingComponent->handleTrailingSlash($uri); | ||
|
||
$this->assertEquals($uri, $newUri); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function uriWithOutSlashGetsModifiedForTrailingSlash(): void | ||
{ | ||
$routingComponent = new RoutingComponent(); | ||
|
||
$uri = new Uri('http://dev.local/testpath'); | ||
$newUri = $routingComponent->handleTrailingSlash($uri); | ||
|
||
$this->assertStringEndsWith('/', (string) $newUri); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function uriWithLoweredPathGetsNotModified(): void | ||
{ | ||
$routingComponent = new RoutingComponent(); | ||
|
||
$uri = new Uri('http://dev.local/testpath/'); | ||
$newUri = $routingComponent->handleToLowerCase($uri); | ||
|
||
$this->assertEquals($uri, $newUri); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function uriWithCamelCasePathGetsModifiedToLowereCase(): void | ||
{ | ||
$routingComponent = new RoutingComponent(); | ||
|
||
$camelCasePath = '/testPath/'; | ||
$uri = new Uri('http://dev.local' . $camelCasePath); | ||
$newUri = $routingComponent->handleToLowerCase($uri); | ||
|
||
$this->assertNotEquals($camelCasePath, $newUri->getPath()); | ||
$this->assertEquals(strtolower($camelCasePath), $newUri->getPath()); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function uriWithSpecialCharsDoesNotThrowAnException(): void | ||
{ | ||
$routingComponent = new RoutingComponent(); | ||
|
||
$uri = new Uri('http://dev.local/äß&/'); | ||
$newUri = $routingComponent->handleToLowerCase($uri); | ||
$newUri = $routingComponent->handleTrailingSlash($newUri); | ||
|
||
$this->assertEquals($uri, $newUri); | ||
} | ||
|
||
/** | ||
* @test | ||
* @dataProvider invalidUrisWithConfig | ||
*/ | ||
public function ifPathHasChangesRedirect(string $invalidUrl, string $validUrl, bool $trailingSlash, bool $toLowerCase): void | ||
{ | ||
$configuration = [ | ||
'enable' => [ | ||
'trailingSlash' => $trailingSlash, | ||
'toLowerCase' => $toLowerCase | ||
], | ||
]; | ||
|
||
|
||
$httpRequest = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->setMethods(['getUri', 'withStatus'])->getMock(); | ||
$httpRequest->method('getUri')->willReturn(new Uri($invalidUrl)); | ||
|
||
$httpResponse = $this->getMockBuilder(Response::class)->disableOriginalConstructor()->setMethods(['withStatus', 'withHeader'])->getMock(); | ||
$httpResponse->expects($this->once())->method('withStatus')->with(301); | ||
$httpResponse->expects($this->once())->method('withHeader')->with('Location', $validUrl); | ||
|
||
/** @var ComponentContext $componentContext */ | ||
$componentContext = $this->getMockBuilder(ComponentContext::class)->disableOriginalConstructor()->setMethods(['getHttpRequest', 'getHttpResponse'])->getMock(); | ||
$componentContext->method('getHttpRequest')->willReturn($httpRequest); | ||
$componentContext->method('getHttpResponse')->willReturn($httpResponse); | ||
|
||
$routerMock = $this->getMockBuilder(Router::class)->disableOriginalConstructor()->setMethods(['route'])->getMock(); | ||
$routerMock->method('route')->willReturn([]); | ||
|
||
$routingComponent = new RoutingComponent(); | ||
|
||
$this->inject($routingComponent, 'router', $routerMock); | ||
$this->inject($routingComponent, 'configuration', $configuration); | ||
|
||
$routingComponent->handle($componentContext); | ||
} | ||
|
||
/** | ||
* @test | ||
*/ | ||
public function ifPathHasNoChangesDoNotRedirect(): void | ||
{ | ||
$configuration = [ | ||
'enable' => [ | ||
'trailingSlash' => true, | ||
'toLowerCase' => true | ||
], | ||
]; | ||
|
||
$validPath = 'http://dev.local/validpath/'; | ||
|
||
$httpRequest = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->setMethods(['getUri', 'withStatus'])->getMock(); | ||
$httpRequest->method('getUri')->willReturn(new Uri($validPath)); | ||
|
||
$httpResponse = $this->getMockBuilder(Response::class)->disableOriginalConstructor()->setMethods(['withStatus', 'withHeader'])->getMock(); | ||
$httpResponse->expects($this->never())->method('withStatus'); | ||
$httpResponse->expects($this->never())->method('withHeader'); | ||
|
||
/** @var ComponentContext $componentContext */ | ||
$componentContext = $this->getMockBuilder(ComponentContext::class)->disableOriginalConstructor()->setMethods(['getHttpRequest', 'getHttpResponse'])->getMock(); | ||
$componentContext->method('getHttpRequest')->willReturn($httpRequest); | ||
$componentContext->method('getHttpResponse')->willReturn($httpResponse); | ||
|
||
$routerMock = $this->getMockBuilder(Router::class)->disableOriginalConstructor()->setMethods(['route'])->getMock(); | ||
$routerMock->method('route')->willReturn([]); | ||
|
||
$routingComponent = new RoutingComponent(); | ||
|
||
$this->inject($routingComponent, 'router', $routerMock); | ||
$this->inject($routingComponent, 'configuration', $configuration); | ||
|
||
$routingComponent->handle($componentContext); | ||
} | ||
} |
Oops, something went wrong.