├── docs ├── _manifest_exclude └── en │ ├── Code-of-Conduct.md │ └── Installation.md ├── _config ├── controller.yml └── middleware.yml ├── CONTRIBUTING.md ├── _config.php ├── .editorconfig ├── composer.json ├── LICENSE ├── CHANGELOG.md ├── README.md └── src └── Middleware └── TrailingSlashRedirector.php /docs/_manifest_exclude: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config/controller.yml: -------------------------------------------------------------------------------- 1 | SilverStripe\Control\Controller: 2 | add_trailing_slash: true 3 | -------------------------------------------------------------------------------- /docs/en/Code-of-Conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Any discussions about this module, issues or pull requests should be done through the 4 | [Github project page](https://github.com/axllent/silverstripe-trailing-slash). 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You can contribute by reporting bugs, sharing ideas and of course pull requests, 4 | all through the [Github project page](https://github.com/axllent/silverstripe-trailing-slash). 5 | 6 | This code is open source (please refer to the [LICENSE](LICENSE)). 7 | -------------------------------------------------------------------------------- /_config/middleware.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Name: trailingslashredirector 3 | After: 4 | - requestprocessors 5 | --- 6 | SilverStripe\Core\Injector\Injector: 7 | TrailingSlashRedirector: 8 | class: Axllent\TrailingSlash\Middleware\TrailingSlashRedirector 9 | SilverStripe\Control\Director: 10 | properties: 11 | Middlewares: 12 | CustomMiddleware: '%$TrailingSlashRedirector' 13 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | setEnforceTrailingSlashConfig(false); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in this file, 2 | # please see the EditorConfig documentation: 3 | # http://editorconfig.org 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [{*.yml,package.json}] 14 | indent_size = 2 15 | 16 | # The indent size used in the package.json file cannot be changed: 17 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 18 | -------------------------------------------------------------------------------- /docs/en/Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ``` 4 | composer require axllent/silverstripe-trailing-slash 5 | ``` 6 | 7 | 8 | ## Caveats 9 | 10 | Some modules such as `silverstripe/staticpublishqueue` [override](https://github.com/silverstripe/silverstripe-staticpublishqueue/blob/e58a53ab6ce6f5137014a08c47db681f8fc94294/src/Publisher.php#L82-L99) the `REQUEST_URI` to "snapshot" the pages. This can cause conflicts with silverstripe-trailing-slash. To work around this `silverstripe/staticpublishqueue` has been added to an array of "ignored_agents" that are excluded from redirection. You can edit / update this list by setting: 11 | 12 | ```yaml 13 | Axllent\TrailingSlash\Middleware\TrailingSlashRedirector: 14 | ignore_agents: 15 | - 'silverstripe/staticpublishqueue' 16 | ``` 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axllent/silverstripe-trailing-slash", 3 | "description": "Ensure that a single trailing slash is always added to the URL", 4 | "type": "silverstripe-vendormodule", 5 | "homepage": "https://github.com/axllent/silverstripe-trailing-slash", 6 | "keywords": [ 7 | "silverstripe", 8 | "urls", 9 | "seo" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Ralph Slooten", 15 | "homepage": "http://www.axllent.org/" 16 | } 17 | ], 18 | "support": { 19 | "issues": "https://github.com/axllent/silverstripe-trailing-slash/issues" 20 | }, 21 | "require": { 22 | "silverstripe/framework": "^4.0 || ^5.0 || ^6.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Axllent\\TrailingSlash\\": "src/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Techno Joy www.technojoy.co.nz 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Notable changes to this project will be documented in this file. 4 | 5 | ## [2.2.8] 6 | 7 | - Add support for Silverstripe 6 8 | 9 | 10 | ## [2.2.7] 11 | 12 | - Disable redirection via CanonicalURLMiddleware in Silverstripe 5 as this blanket-redirects all request types including POST. 13 | 14 | 15 | ## [2.2.6] 16 | 17 | - Add support for Silverstripe 5 18 | 19 | 20 | ## [2.2.5] 21 | 22 | - Don't rely on `$_SERVER` values for redirect (thanks [wilr](https://github.com/axllent/silverstripe-trailing-slash/pull/14)) 23 | 24 | 25 | ## [2.2.4] 26 | 27 | - Fixes some setups of unwrapped % in yaml breaking site 28 | 29 | 30 | ## [2.2.3] 31 | 32 | - Add `silverstripe/staticpublishqueue` to configurable list of ignored user agents 33 | 34 | 35 | ## [2.2.2] 36 | 37 | - Add support for `silverstripe/staticpublishqueue` ([link](https://github.com/axllent/silverstripe-trailing-slash/pull/11)) 38 | 39 | 40 | ## [2.2.1] 41 | 42 | - Fix deprecation notice on php7.4 (switched parameters in `implode()` function.) 43 | 44 | 45 | ## [2.2.0] 46 | 47 | - Add config yaml to skip additional paths 48 | 49 | 50 | ## [2.1.3] 51 | 52 | - Better handling as `dev/tasks` do not return getURL() value 53 | 54 | 55 | ## [2.1.2] 56 | 57 | - Don't redirect from within `admin/` or `/dev/` 58 | 59 | 60 | ## [2.1.1] 61 | 62 | - Remove hardcoded check for 'home' 63 | 64 | 65 | ## [2.1.0] 66 | 67 | - Use HTTPMiddleware for SS4 68 | 69 | 70 | ## [2.0.2] 71 | 72 | - Switch to silverstripe-vendormodule 73 | 74 | 75 | ## [2.0.1] 76 | 77 | - Don't redirect 'home' (endless loop) 78 | 79 | 80 | ## [2.0.0] 81 | 82 | - Support for SilverStripe 4 83 | - Different detection logic due to changes in `$request->getURL()` 84 | - Ignore URLs with query parameters 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Silverstripe Trailing Slash 2 | 3 | Ensure that a single trailing slash is always added to the URL. 4 | 5 | Only GET and HEAD requests are redirected, excluding URLS that contain a file extension or query parameter. 6 | Detected ajax requests are also ignored. 7 | 8 | ## Note: 9 | 10 | This module is no longer actively developed since the majority of the functionality is handled natively within Silverstripe 5 & 6. 11 | To enable trailing slashes **without** the need for this module, simply add the following to your yaml configuration: 12 | 13 | ```yaml 14 | SilverStripe\Control\Controller: 15 | add_trailing_slash: true 16 | ``` 17 | 18 | This module has been kept alive for the list of [depended modules](https://github.com/axllent/silverstripe-trailing-slash/network/dependents). 19 | 20 | 21 | ## Examples 22 | 23 | - `example.com/contact` is redirected to `example.com/contact/` 24 | - `example.com/contact//` is redirected to `example.com/contact/` 25 | - `example.com/contact?test` is not redirected 26 | - `example.com/contact.html` is not redirected 27 | 28 | 29 | ## Requirements 30 | 31 | - Silverstripe ^4.0 || ^ 5.0 || ^6.0 32 | 33 | For Silverstripe 3, please refer to the [Silverstripe3 branch](https://github.com/axllent/silverstripe-trailing-slash/tree/silverstripe3). 34 | 35 | ## Installation and configuration 36 | 37 | ``` 38 | composer require axllent/silverstripe-trailing-slash 39 | ``` 40 | 41 | - Run `?flush=1` 42 | 43 | 44 | ## Configuration 45 | 46 | By default it will ignore any `admin/` & `dev/` URLs, as well as all ajax requests. 47 | It also only acts on `$_GET` requests as not to interfere with any posted data, and 48 | ignores any URL containing an extension (eg: `/contact.html`) or query parameter. 49 | 50 | You can create additional "ignore_paths" by creating a yaml config 51 | (eg: `app/_config/trailing-slash.yml`): 52 | 53 | ```yaml 54 | Axllent\TrailingSlash\Middleware\TrailingSlashRedirector: 55 | ignore_paths: 56 | - 'events' 57 | - 'my/other/path' 58 | ``` 59 | 60 | These paths are relative to the base URL (`/`), so `events` will not match `/page/events`, 61 | but will match `/events-2020`. 62 | 63 | Please note that paths do not typically contain a trailing slash unless it is only 64 | underlying URLs you wish to redirect. Wildcards etc are not supported in the syntax. 65 | -------------------------------------------------------------------------------- /src/Middleware/TrailingSlashRedirector.php: -------------------------------------------------------------------------------- 1 | get(TrailingSlashRedirector::class, 'ignore_paths'); 59 | 60 | foreach ($ignore_config as $iurl) { 61 | if ($quoted = preg_quote(ltrim($iurl, '/'), '/')) { 62 | array_push($ignore_paths, $quoted); 63 | } 64 | } 65 | 66 | if ($request && ($request->isGET() || $request->isHEAD())) { 67 | // skip $ignore_paths and home (`/`) 68 | if ($request->getURL() == '' 69 | || preg_match( 70 | '/^(' . implode('|', $ignore_paths) . ')/i', 71 | $request->getURL() 72 | ) 73 | ) { 74 | return $delegate($request); 75 | } 76 | 77 | $requested_url = Environment::getEnv('REQUEST_URI'); 78 | 79 | $expected_url = rtrim( 80 | Director::baseURL() . $request->getURL(), '/' 81 | ) . '/'; 82 | 83 | $urlPathInfo = pathinfo($requested_url); 84 | 85 | $params = $request->getVars(); 86 | 87 | $ignore_agents = Config::inst() 88 | ->get(TrailingSlashRedirector::class, 'ignore_agents'); 89 | 90 | if (!Director::is_ajax() 91 | && !($ignore_agents && in_array( 92 | $request->getHeader('User-Agent'), 93 | $ignore_agents 94 | )) 95 | && !isset($urlPathInfo['extension']) 96 | && empty($params) 97 | && !preg_match( 98 | '/^' . preg_quote($expected_url, '/') . '(?!\/)/i', 99 | $requested_url 100 | ) 101 | ) { 102 | $params = $request->getVars(); 103 | $redirect_url = Controller::join_links($expected_url, '/'); 104 | $response = new HTTPResponse(); 105 | $code = Config::inst()->get( 106 | TrailingSlashRedirector::class, 107 | 'redirection_status_code' 108 | ); 109 | 110 | return $response->redirect($redirect_url, $code); 111 | } 112 | } 113 | 114 | $response = $delegate($request); 115 | 116 | return $response; 117 | } 118 | } 119 | --------------------------------------------------------------------------------