Skip to content

Commit 90cd78e

Browse files
committed
TASK: add neos 8 compatibility
0 parents  commit 90cd78e

File tree

11 files changed

+551
-0
lines changed

11 files changed

+551
-0
lines changed

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on: [ push ]
4+
5+
jobs:
6+
phpunit:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: php-actions/composer@v6
12+
- name: PHPUnit Tests
13+
uses: php-actions/phpunit@v4
14+
with:
15+
php_extensions: xdebug
16+
bootstrap: vendor/autoload.php
17+
configuration: phpunit.xml
18+
coverage_text: true
19+
env:
20+
XDEBUG_MODE: coverage
21+
22+
phpstan:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: php-actions/composer@v6
27+
- uses: php-actions/phpstan@v3
28+
with:
29+
version: latest
30+
path: Classes Tests
31+
configuration: phpstan.neon

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.idea
2+
composer.lock
3+
vendor
4+
Packages
5+
.phpunit.result.cache
6+
bin
7+
Build

Classes/RoutingMiddleware.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flowpack\SeoRouting;
6+
7+
use Neos\Flow\Annotations as Flow;
8+
use Psr\Http\Message\ResponseFactoryInterface;
9+
use Psr\Http\Message\ResponseInterface;
10+
use Psr\Http\Message\ServerRequestInterface;
11+
use Psr\Http\Message\UriFactoryInterface;
12+
use Psr\Http\Message\UriInterface;
13+
use Psr\Http\Server\MiddlewareInterface;
14+
use Psr\Http\Server\RequestHandlerInterface;
15+
16+
final class RoutingMiddleware implements MiddlewareInterface
17+
{
18+
#[Flow\Inject]
19+
protected ResponseFactoryInterface $responseFactory;
20+
21+
#[Flow\Inject]
22+
protected UriFactoryInterface $uriFactory;
23+
24+
/** @var array{enable: array{trailingSlash: bool, toLowerCase: bool}, statusCode?: int} */
25+
#[Flow\InjectConfiguration(path: 'redirect')]
26+
protected array $configuration;
27+
28+
/** @var array{string: bool} */
29+
#[Flow\InjectConfiguration(path: 'blocklist')]
30+
protected array $blocklist;
31+
32+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
33+
{
34+
$isTrailingSlashEnabled = $this->configuration['enable']['trailingSlash'] ?? false;
35+
$isToLowerCaseEnabled = $this->configuration['enable']['toLowerCase'] ?? false;
36+
37+
$uri = $request->getUri();
38+
39+
if (! $isTrailingSlashEnabled && ! $isToLowerCaseEnabled) {
40+
return $handler->handle($request);
41+
}
42+
43+
if ($this->matchesBlocklist($uri)) {
44+
return $handler->handle($request);
45+
}
46+
47+
$oldPath = $uri->getPath();
48+
49+
if ($isTrailingSlashEnabled) {
50+
$uri = $this->handleTrailingSlash($uri);
51+
}
52+
53+
if ($isToLowerCaseEnabled) {
54+
$uri = $this->handleToLowerCase($uri);
55+
}
56+
57+
if ($uri->getPath() === $oldPath) {
58+
return $handler->handle($request);
59+
}
60+
61+
$response = $this->responseFactory->createResponse($this->configuration['statusCode'] ?? 301);
62+
63+
return $response->withAddedHeader('Location', (string)$uri);
64+
}
65+
66+
private function handleTrailingSlash(UriInterface $uri): UriInterface
67+
{
68+
if (strlen($uri->getPath()) === 0) {
69+
return $uri;
70+
}
71+
72+
if (array_key_exists('extension', pathinfo($uri->getPath()))) {
73+
return $uri;
74+
}
75+
76+
return $uri->withPath(rtrim($uri->getPath(), '/') . '/')
77+
->withQuery($uri->getQuery())
78+
->withFragment($uri->getFragment());
79+
}
80+
81+
private function handleToLowerCase(UriInterface $uri): UriInterface
82+
{
83+
$loweredPath = strtolower($uri->getPath());
84+
85+
if ($uri->getPath() === $loweredPath) {
86+
return $uri;
87+
}
88+
89+
$newUri = str_replace($uri->getPath(), $loweredPath, (string)$uri);
90+
91+
return $this->uriFactory->createUri($newUri);
92+
}
93+
94+
private function matchesBlocklist(UriInterface $uri): bool
95+
{
96+
$path = $uri->getPath();
97+
foreach ($this->blocklist as $rawPattern => $active) {
98+
$pattern = '/' . str_replace('/', '\/', $rawPattern) . '/';
99+
100+
if (! $active) {
101+
continue;
102+
}
103+
104+
if (preg_match($pattern, $path) === 1) {
105+
return true;
106+
}
107+
}
108+
109+
return false;
110+
}
111+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
'Neos.Neos:Document':
2+
properties:
3+
uriPathSegment:
4+
validation:
5+
'Neos.Neos/Validation/RegularExpressionValidator':
6+
regularExpression: '/^[a-z0-9\-]+$/' #override original regex (removes insensitive)

Configuration/Settings.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Flowpack:
2+
SeoRouting:
3+
redirect:
4+
enable:
5+
trailingSlash: true
6+
toLowerCase: false
7+
statusCode: 301
8+
blocklist:
9+
'/neos.*': true
10+
11+
Neos:
12+
Flow:
13+
http:
14+
middlewares:
15+
'before routing':
16+
middleware: 'Flowpack\SeoRouting\RoutingMiddleware'

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018-2025 yeebase media GmbH, Sandstorm Media GmbH
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Flowpack.SeoRouting
2+
3+
<!-- TOC -->
4+
5+
* [Flowpack.SeoRouting](#flowpackseorouting)
6+
* [Introduction](#introduction)
7+
* [Features](#features)
8+
* [Installation](#installation)
9+
* [Configuration](#configuration)
10+
* [Standard Configuration](#standard-configuration)
11+
* [Blocklist for redirects](#blocklist-for-redirects)
12+
* [Thank you](#thank-you)
13+
14+
<!-- TOC -->
15+
16+
## Introduction
17+
18+
This package allows you to enforce a trailing slash and/or lower case urls in Flow/Neos.
19+
20+
## Features
21+
22+
This package has 2 main features:
23+
24+
- **trailingSlash**: ensure that all links ends with a trailing slash (e.g. `example.com/test/` instead of
25+
`example.com/test`)
26+
- **toLowerCase**: ensure that camelCase links gets redirected to lowercase (e.g. `example.com/lowercase` instead of
27+
`example.com/lowerCase`)
28+
29+
You can de- and activate both of them.
30+
31+
Another small feature is to restrict all _new_ neos pages to have a lowercased `uriPathSegment`. This is done by
32+
extending the `NodeTypes.Document.yaml`.
33+
34+
## Installation
35+
36+
Just require it via composer:
37+
38+
`composer require flowpack/seo-routing`
39+
40+
## Configuration
41+
42+
### Standard Configuration
43+
44+
In the standard configuration we have activated the trailingSlash (to redirect all uris without a / at the end to an uri
45+
with / at the end) and do all redirects with a 301 http status.
46+
47+
*Note: The lowercase redirect is deactivated by default, because you have to make sure, that there is
48+
no `uriPathSegment`
49+
with camelCase or upperspace letters - this would lead to redirects in the neverland.*
50+
51+
```
52+
Flowpack:
53+
SeoRouting:
54+
redirect:
55+
enable:
56+
trailingSlash: true
57+
toLowerCase: false
58+
statusCode: 301
59+
blocklist:
60+
'/neos.*': true
61+
```
62+
63+
### Blocklist for redirects
64+
65+
By default, all `/neos` URLs are ignored for redirects. You can extend the blocklist array with regex as you like:
66+
67+
```yaml
68+
Flowpack:
69+
SeoRouting:
70+
blocklist:
71+
'/neos.*': true
72+
```
73+
74+
## Thank you
75+
76+
This package originates from https://github.com/t3n/seo-routing.
77+
78+
Thank you, T3N and associates for your work.

0 commit comments

Comments
 (0)