Skip to content

Commit 02f780c

Browse files
zhyianSoean
andauthored
Add PermalinkCleanup module to remove /blog prefix on main site (#50)
* feat: add PermalinkCleanup module to remove /blog prefix on main site * refactor: use str_starts_with() for better readability Co-authored-by: Sören Wünsch <[email protected]> Signed-off-by: Oleksandr Zhyian <[email protected]> --------- Signed-off-by: Oleksandr Zhyian <[email protected]> Co-authored-by: Sören Wünsch <[email protected]>
1 parent cdb6b9a commit 02f780c

File tree

7 files changed

+360
-2
lines changed

7 files changed

+360
-2
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Your environment will be available at [http://localhost:8888](http://localhost:8
5858
- Use `phpcs` and `PHPStan` (level 8) to validate your code.
5959

6060
```bash
61-
composer run lint
61+
composer run cs
6262
composer run phpstan
6363
```
6464

@@ -127,4 +127,4 @@ Be respectful and inclusive in all your interactions.
127127
---
128128

129129
Thanks for helping improve MultiSyde! ❤️
130-
— The Syde Team
130+
— The Syde Team

modules/PermalinkCleanup/About.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
/**
3+
* Information class for the PermalinkCleanup feature.
4+
*
5+
* @package multisyde
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Syde\MultiSyde\Modules\PermalinkCleanup;
11+
12+
use Syde\MultiSyde\Summary;
13+
use Syde\MultiSyde\ShareableInformation;
14+
15+
/**
16+
* Provides information about the PermalinkCleanup feature.
17+
*/
18+
class About implements ShareableInformation {
19+
20+
/**
21+
* Get the feature information.
22+
*
23+
* @return Summary
24+
*/
25+
public static function get(): Summary {
26+
return new Summary(
27+
__( 'Permalink Cleanup', 'multisyde' ),
28+
__( 'Handles permalink structure modifications for multisite installations. Removes /blog prefix from the base permalink structure.', 'multisyde' ),
29+
array(
30+
'https://github.com/inpsyde/multisyde/issues/24',
31+
)
32+
);
33+
}
34+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
/**
3+
* Permalink Cleanup Feature
4+
*
5+
* @package multisyde
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Syde\MultiSyde\Modules\PermalinkCleanup;
11+
12+
use Syde\MultiSyde\LoadableFeature;
13+
14+
/**
15+
* Feature Class PermalinkCleanup
16+
*/
17+
final class Feature implements LoadableFeature {
18+
19+
/**
20+
* Adds functionality to their respective hooks.
21+
*
22+
* @return void
23+
*/
24+
public static function init(): void {
25+
add_filter( 'sanitize_option_permalink_structure', array( __CLASS__, 'remove_blog_prefix' ) );
26+
add_filter( 'option_permalink_structure', array( __CLASS__, 'remove_blog_prefix' ) );
27+
}
28+
29+
/**
30+
* Remove /blog prefix from the permalink structure on the main site.
31+
*
32+
* In WordPress multisite installations, the main site often has /blog
33+
* prepended to post permalinks. This function strips that prefix while
34+
* leaving other sites untouched.
35+
*
36+
* @param string $value The permalink structure pattern (e.g., '/blog/%postname%').
37+
*
38+
* @return string The modified permalink structure with /blog removed,
39+
* or the original value if conditions aren't met.
40+
*/
41+
public static function remove_blog_prefix( string $value ): string {
42+
if ( ! is_multisite() || ! is_main_site() || empty( $value ) ) {
43+
return $value;
44+
}
45+
46+
if ( str_starts_with( $value, '/blog/' ) ) {
47+
return substr( $value, 5 );
48+
}
49+
50+
if ( '/blog' === $value ) {
51+
return '';
52+
}
53+
54+
return $value;
55+
}
56+
}

modules/PermalinkCleanup/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Permalink Cleanup Module
2+
3+
A WordPress multisite feature that automatically removes the `/blog` prefix from the main site's permalink structure.
4+
5+
## Overview
6+
7+
In WordPress multisite installations, the main site typically has `/blog` prepended to all post permalinks to distinguish posts from pages. This module automatically strips that prefix, allowing cleaner URLs on the main site while leaving subsites untouched.
8+
9+
10+
## Installation
11+
12+
This module is part of the MultiSyde package and is loaded automatically.
13+
14+
15+
## How It Works
16+
17+
The module hooks into two WordPress filters:
18+
19+
1. `sanitize_option_permalink_structure` - Cleans the permalink structure when it's saved
20+
2. `option_permalink_structure` - Cleans the permalink structure when it's retrieved
21+
22+
## Disabling the Feature
23+
24+
If you need to disable the permalink cleanup feature, add this code to your theme's `functions.php` or a custom plugin **after** the feature has been initialized:
25+
```php
26+
add_action('init', function() {
27+
remove_filter(
28+
'sanitize_option_permalink_structure',
29+
['Syde\MultiSyde\Modules\PermalinkCleanup\Feature', 'remove_blog_prefix']
30+
);
31+
32+
remove_filter(
33+
'option_permalink_structure',
34+
['Syde\MultiSyde\Modules\PermalinkCleanup\Feature', 'remove_blog_prefix']
35+
);
36+
}, 20); // Priority 20 to ensure it runs after the feature is initialized
37+
```
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
<?php
2+
/**
3+
* PermalinkCleanup Tests
4+
*
5+
* @package multisyde-unit-tests
6+
*/
7+
8+
declare( strict_types=1 );
9+
10+
namespace Syde\MultiSyde\Modules\PermalinkCleanup\tests\unit;
11+
12+
use Brain\Monkey\Functions;
13+
use Brain\Monkey\Filters;
14+
use Syde\MultiSyde\Modules\PermalinkCleanup\Feature;
15+
use Syde\MultiSydeUnitTests\UnitTestCase;
16+
17+
/**
18+
* Test the PermalinkCleanup class.
19+
*/
20+
final class TestFeature extends UnitTestCase {
21+
22+
23+
/**
24+
* Test that init registers the correct filters.
25+
*
26+
* @covers Feature::init
27+
*
28+
* @return void
29+
*/
30+
public function test_registers_filters(): void {
31+
Filters\expectAdded( 'sanitize_option_permalink_structure' )
32+
->with( array( Feature::class, 'remove_blog_prefix' ) )
33+
->once();
34+
Filters\expectAdded( 'option_permalink_structure' )
35+
->with( array( Feature::class, 'remove_blog_prefix' ) )
36+
->once();
37+
38+
Feature::init();
39+
}
40+
41+
/**
42+
* Test that /blog prefix is removed on main site in multisite.
43+
*
44+
* @covers Feature::remove_blog_prefix
45+
*
46+
* @return void
47+
*/
48+
public function test_removes_blog_prefix_on_main_site(): void {
49+
Functions\when( 'is_multisite' )->justReturn( true );
50+
Functions\when( 'is_main_site' )->justReturn( true );
51+
52+
$result = Feature::remove_blog_prefix( '/blog/%postname%/' );
53+
54+
$this->assertSame( '/%postname%/', $result );
55+
}
56+
57+
/**
58+
* Test that /blog prefix is removed with various permalink structures.
59+
*
60+
* @covers Feature::remove_blog_prefix
61+
* @dataProvider blog_prefix_permalink_provider
62+
*
63+
* @param string $input The input permalink structure.
64+
* @param string $expected The expected output.
65+
*
66+
* @return void
67+
*/
68+
public function test_removes_blog_prefix_various_structures(
69+
string $input,
70+
string $expected
71+
): void {
72+
Functions\when( 'is_multisite' )->justReturn( true );
73+
Functions\when( 'is_main_site' )->justReturn( true );
74+
75+
$result = Feature::remove_blog_prefix( $input );
76+
77+
$this->assertSame( $expected, $result );
78+
}
79+
80+
/**
81+
* Data provider for various permalink structures with /blog prefix.
82+
*
83+
* @return array<string, array<string, string>>
84+
*/
85+
public function blog_prefix_permalink_provider(): array {
86+
return array(
87+
'simple postname' => array(
88+
'input' => '/blog/%postname%/',
89+
'expected' => '/%postname%/',
90+
),
91+
'year month postname' => array(
92+
'input' => '/blog/%year%/%monthnum%/%postname%/',
93+
'expected' => '/%year%/%monthnum%/%postname%/',
94+
),
95+
'category postname' => array(
96+
'input' => '/blog/%category%/%postname%/',
97+
'expected' => '/%category%/%postname%/',
98+
),
99+
'just /blog' => array(
100+
'input' => '/blog',
101+
'expected' => '',
102+
),
103+
'/blog with trailing slash' => array(
104+
'input' => '/blog/',
105+
'expected' => '/',
106+
),
107+
'numeric structure' => array(
108+
'input' => '/blog/%year%/%monthnum%/%day%/%postname%/',
109+
'expected' => '/%year%/%monthnum%/%day%/%postname%/',
110+
),
111+
);
112+
}
113+
114+
/**
115+
* Test that value is unchanged when not on multisite.
116+
*
117+
* @covers Feature::remove_blog_prefix
118+
*
119+
* @return void
120+
*/
121+
public function test_does_not_remove_prefix_when_not_multisite(): void {
122+
Functions\when( 'is_multisite' )->justReturn( false );
123+
Functions\when( 'is_main_site' )->justReturn( true );
124+
125+
$result = Feature::remove_blog_prefix( '/blog/%postname%/' );
126+
127+
$this->assertSame( '/blog/%postname%/', $result );
128+
}
129+
130+
/**
131+
* Test that value is unchanged when not on main site.
132+
*
133+
* @covers Feature::remove_blog_prefix
134+
*
135+
* @return void
136+
*/
137+
public function test_does_not_remove_prefix_when_not_main_site(): void {
138+
Functions\when( 'is_multisite' )->justReturn( true );
139+
Functions\when( 'is_main_site' )->justReturn( false );
140+
141+
$result = Feature::remove_blog_prefix( '/blog/%postname%/' );
142+
143+
$this->assertSame( '/blog/%postname%/', $result );
144+
}
145+
146+
/**
147+
* Test that value is unchanged when both conditions are false.
148+
*
149+
* @covers Feature::remove_blog_prefix
150+
*
151+
* @return void
152+
*/
153+
public function test_does_not_remove_prefix_when_neither_condition_met(): void {
154+
Functions\when( 'is_multisite' )->justReturn( false );
155+
Functions\when( 'is_main_site' )->justReturn( false );
156+
157+
$result = Feature::remove_blog_prefix( '/blog/%postname%/' );
158+
159+
$this->assertSame( '/blog/%postname%/', $result );
160+
}
161+
162+
/**
163+
* Test that empty string is handled correctly.
164+
*
165+
* @covers Feature::remove_blog_prefix
166+
*
167+
* @return void
168+
*/
169+
public function test_handles_empty_string(): void {
170+
Functions\when( 'is_multisite' )->justReturn( true );
171+
Functions\when( 'is_main_site' )->justReturn( true );
172+
173+
$result = Feature::remove_blog_prefix( '' );
174+
175+
$this->assertSame( '', $result );
176+
}
177+
178+
/**
179+
* Test that permalink without /blog prefix is unchanged.
180+
*
181+
* @covers Feature::remove_blog_prefix
182+
*
183+
* @return void
184+
*/
185+
public function test_does_not_modify_permalink_without_blog_prefix(): void {
186+
Functions\when( 'is_multisite' )->justReturn( true );
187+
Functions\when( 'is_main_site' )->justReturn( true );
188+
189+
$result = Feature::remove_blog_prefix( '/%year%/%monthnum%/%postname%/' );
190+
191+
$this->assertSame( '/%year%/%monthnum%/%postname%/', $result );
192+
}
193+
194+
/**
195+
* Test that /blog in the middle of structure is not removed.
196+
*
197+
* @covers Feature::remove_blog_prefix
198+
*
199+
* @return void
200+
*/
201+
public function test_does_not_remove_blog_from_middle_of_structure(): void {
202+
Functions\when( 'is_multisite' )->justReturn( true );
203+
Functions\when( 'is_main_site' )->justReturn( true );
204+
205+
$result = Feature::remove_blog_prefix( '/%category%/blog/%postname%/' );
206+
207+
$this->assertSame( '/%category%/blog/%postname%/', $result );
208+
}
209+
210+
/**
211+
* Test that similar prefixes are not affected.
212+
*
213+
* @covers Feature::remove_blog_prefix
214+
*
215+
* @return void
216+
*/
217+
public function test_does_not_remove_similar_prefixes(): void {
218+
Functions\when( 'is_multisite' )->justReturn( true );
219+
Functions\when( 'is_main_site' )->justReturn( true );
220+
221+
$result = Feature::remove_blog_prefix( '/blogger/%postname%/' );
222+
223+
$this->assertSame( '/blogger/%postname%/', $result );
224+
}
225+
}

modules/config.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
use Syde\MultiSyde\Modules\SiteActivePlugins\About as SiteActivePluginsInformation;
1616
use Syde\MultiSyde\Modules\LastUserLogin\Feature as LastUserLogin;
1717
use Syde\MultiSyde\Modules\LastUserLogin\About as LastUserLoginInformation;
18+
use Syde\MultiSyde\Modules\PermalinkCleanup\Feature as PermalinkCleanup;
19+
use Syde\MultiSyde\Modules\PermalinkCleanup\About as PermalinkCleanupInformation;
1820
use Syde\MultiSyde\Modules\SiteActiveTheme\Feature as SiteActiveTheme;
1921
use Syde\MultiSyde\Modules\SiteActiveTheme\About as SiteActiveThemeInformation;
2022

2123
return array(
2224
GetSiteBy::class => GetSiteByInformation::class,
2325
LastUserLogin::class => LastUserLoginInformation::class,
26+
PermalinkCleanup::class => PermalinkCleanupInformation::class,
2427
SiteActivePlugins::class => SiteActivePluginsInformation::class,
2528
SiteActiveTheme::class => SiteActiveThemeInformation::class,
2629
);

0 commit comments

Comments
 (0)