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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Your environment will be available at [http://localhost:8888](http://localhost:8
- Use `phpcs` and `PHPStan` (level 8) to validate your code.

```bash
composer run lint
composer run cs
composer run phpstan
```

Expand Down Expand Up @@ -127,4 +127,4 @@ Be respectful and inclusive in all your interactions.
---

Thanks for helping improve MultiSyde! ❤️
— The Syde Team
— The Syde Team
34 changes: 34 additions & 0 deletions modules/PermalinkCleanup/About.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
/**
* Information class for the PermalinkCleanup feature.
*
* @package multisyde
*/

declare(strict_types=1);

namespace Syde\MultiSyde\Modules\PermalinkCleanup;

use Syde\MultiSyde\Summary;
use Syde\MultiSyde\ShareableInformation;

/**
* Provides information about the PermalinkCleanup feature.
*/
class About implements ShareableInformation {

/**
* Get the feature information.
*
* @return Summary
*/
public static function get(): Summary {
return new Summary(
__( 'Permalink Cleanup', 'multisyde' ),
__( 'Handles permalink structure modifications for multisite installations. Removes /blog prefix from the base permalink structure.', 'multisyde' ),
array(
'https://github.com/inpsyde/multisyde/issues/24',
)
);
}
}
3 changes: 3 additions & 0 deletions modules/PermalinkCleanup/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Changelog

All notable changes to this project will be documented in this file.
56 changes: 56 additions & 0 deletions modules/PermalinkCleanup/Feature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* Permalink Cleanup Feature
*
* @package multisyde
*/

declare(strict_types=1);

namespace Syde\MultiSyde\Modules\PermalinkCleanup;

use Syde\MultiSyde\LoadableFeature;

/**
* Feature Class PermalinkCleanup
*/
final class Feature implements LoadableFeature {

/**
* Adds functionality to their respective hooks.
*
* @return void
*/
public static function init(): void {
add_filter( 'sanitize_option_permalink_structure', array( __CLASS__, 'remove_blog_prefix' ) );
add_filter( 'option_permalink_structure', array( __CLASS__, 'remove_blog_prefix' ) );
}

/**
* Remove /blog prefix from the permalink structure on the main site.
*
* In WordPress multisite installations, the main site often has /blog
* prepended to post permalinks. This function strips that prefix while
* leaving other sites untouched.
*
* @param string $value The permalink structure pattern (e.g., '/blog/%postname%').
*
* @return string The modified permalink structure with /blog removed,
* or the original value if conditions aren't met.
*/
public static function remove_blog_prefix( string $value ): string {
if ( ! is_multisite() || ! is_main_site() || empty( $value ) ) {
return $value;
}

if ( str_starts_with( $value, '/blog/' ) ) {
return substr( $value, 5 );
}

if ( '/blog' === $value ) {
return '';
}

return $value;
}
}
37 changes: 37 additions & 0 deletions modules/PermalinkCleanup/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Permalink Cleanup Module

A WordPress multisite feature that automatically removes the `/blog` prefix from the main site's permalink structure.

## Overview

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.


## Installation

This module is part of the MultiSyde package and is loaded automatically.


## How It Works

The module hooks into two WordPress filters:

1. `sanitize_option_permalink_structure` - Cleans the permalink structure when it's saved
2. `option_permalink_structure` - Cleans the permalink structure when it's retrieved

## Disabling the Feature

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:
```php
add_action('init', function() {
remove_filter(
'sanitize_option_permalink_structure',
['Syde\MultiSyde\Modules\PermalinkCleanup\Feature', 'remove_blog_prefix']
);

remove_filter(
'option_permalink_structure',
['Syde\MultiSyde\Modules\PermalinkCleanup\Feature', 'remove_blog_prefix']
);
}, 20); // Priority 20 to ensure it runs after the feature is initialized
```
225 changes: 225 additions & 0 deletions modules/PermalinkCleanup/tests/unit/TestFeature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<?php
/**
* PermalinkCleanup Tests
*
* @package multisyde-unit-tests
*/

declare( strict_types=1 );

namespace Syde\MultiSyde\Modules\PermalinkCleanup\tests\unit;

use Brain\Monkey\Functions;
use Brain\Monkey\Filters;
use Syde\MultiSyde\Modules\PermalinkCleanup\Feature;
use Syde\MultiSydeUnitTests\UnitTestCase;

/**
* Test the PermalinkCleanup class.
*/
final class TestFeature extends UnitTestCase {


/**
* Test that init registers the correct filters.
*
* @covers Feature::init
*
* @return void
*/
public function test_registers_filters(): void {
Filters\expectAdded( 'sanitize_option_permalink_structure' )
->with( array( Feature::class, 'remove_blog_prefix' ) )
->once();
Filters\expectAdded( 'option_permalink_structure' )
->with( array( Feature::class, 'remove_blog_prefix' ) )
->once();

Feature::init();
}

/**
* Test that /blog prefix is removed on main site in multisite.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_removes_blog_prefix_on_main_site(): void {
Functions\when( 'is_multisite' )->justReturn( true );
Functions\when( 'is_main_site' )->justReturn( true );

$result = Feature::remove_blog_prefix( '/blog/%postname%/' );

$this->assertSame( '/%postname%/', $result );
}

/**
* Test that /blog prefix is removed with various permalink structures.
*
* @covers Feature::remove_blog_prefix
* @dataProvider blog_prefix_permalink_provider
*
* @param string $input The input permalink structure.
* @param string $expected The expected output.
*
* @return void
*/
public function test_removes_blog_prefix_various_structures(
string $input,
string $expected
): void {
Functions\when( 'is_multisite' )->justReturn( true );
Functions\when( 'is_main_site' )->justReturn( true );

$result = Feature::remove_blog_prefix( $input );

$this->assertSame( $expected, $result );
}

/**
* Data provider for various permalink structures with /blog prefix.
*
* @return array<string, array<string, string>>
*/
public function blog_prefix_permalink_provider(): array {
return array(
'simple postname' => array(
'input' => '/blog/%postname%/',
'expected' => '/%postname%/',
),
'year month postname' => array(
'input' => '/blog/%year%/%monthnum%/%postname%/',
'expected' => '/%year%/%monthnum%/%postname%/',
),
'category postname' => array(
'input' => '/blog/%category%/%postname%/',
'expected' => '/%category%/%postname%/',
),
'just /blog' => array(
'input' => '/blog',
'expected' => '',
),
'/blog with trailing slash' => array(
'input' => '/blog/',
'expected' => '/',
),
'numeric structure' => array(
'input' => '/blog/%year%/%monthnum%/%day%/%postname%/',
'expected' => '/%year%/%monthnum%/%day%/%postname%/',
),
);
}

/**
* Test that value is unchanged when not on multisite.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_does_not_remove_prefix_when_not_multisite(): void {
Functions\when( 'is_multisite' )->justReturn( false );
Functions\when( 'is_main_site' )->justReturn( true );

$result = Feature::remove_blog_prefix( '/blog/%postname%/' );

$this->assertSame( '/blog/%postname%/', $result );
}

/**
* Test that value is unchanged when not on main site.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_does_not_remove_prefix_when_not_main_site(): void {
Functions\when( 'is_multisite' )->justReturn( true );
Functions\when( 'is_main_site' )->justReturn( false );

$result = Feature::remove_blog_prefix( '/blog/%postname%/' );

$this->assertSame( '/blog/%postname%/', $result );
}

/**
* Test that value is unchanged when both conditions are false.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_does_not_remove_prefix_when_neither_condition_met(): void {
Functions\when( 'is_multisite' )->justReturn( false );
Functions\when( 'is_main_site' )->justReturn( false );

$result = Feature::remove_blog_prefix( '/blog/%postname%/' );

$this->assertSame( '/blog/%postname%/', $result );
}

/**
* Test that empty string is handled correctly.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_handles_empty_string(): void {
Functions\when( 'is_multisite' )->justReturn( true );
Functions\when( 'is_main_site' )->justReturn( true );

$result = Feature::remove_blog_prefix( '' );

$this->assertSame( '', $result );
}

/**
* Test that permalink without /blog prefix is unchanged.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_does_not_modify_permalink_without_blog_prefix(): void {
Functions\when( 'is_multisite' )->justReturn( true );
Functions\when( 'is_main_site' )->justReturn( true );

$result = Feature::remove_blog_prefix( '/%year%/%monthnum%/%postname%/' );

$this->assertSame( '/%year%/%monthnum%/%postname%/', $result );
}

/**
* Test that /blog in the middle of structure is not removed.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_does_not_remove_blog_from_middle_of_structure(): void {
Functions\when( 'is_multisite' )->justReturn( true );
Functions\when( 'is_main_site' )->justReturn( true );

$result = Feature::remove_blog_prefix( '/%category%/blog/%postname%/' );

$this->assertSame( '/%category%/blog/%postname%/', $result );
}

/**
* Test that similar prefixes are not affected.
*
* @covers Feature::remove_blog_prefix
*
* @return void
*/
public function test_does_not_remove_similar_prefixes(): void {
Functions\when( 'is_multisite' )->justReturn( true );
Functions\when( 'is_main_site' )->justReturn( true );

$result = Feature::remove_blog_prefix( '/blogger/%postname%/' );

$this->assertSame( '/blogger/%postname%/', $result );
}
}
3 changes: 3 additions & 0 deletions modules/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
use Syde\MultiSyde\Modules\SiteActivePlugins\About as SiteActivePluginsInformation;
use Syde\MultiSyde\Modules\LastUserLogin\Feature as LastUserLogin;
use Syde\MultiSyde\Modules\LastUserLogin\About as LastUserLoginInformation;
use Syde\MultiSyde\Modules\PermalinkCleanup\Feature as PermalinkCleanup;
use Syde\MultiSyde\Modules\PermalinkCleanup\About as PermalinkCleanupInformation;
use Syde\MultiSyde\Modules\SiteActiveTheme\Feature as SiteActiveTheme;
use Syde\MultiSyde\Modules\SiteActiveTheme\About as SiteActiveThemeInformation;

return array(
GetSiteBy::class => GetSiteByInformation::class,
LastUserLogin::class => LastUserLoginInformation::class,
PermalinkCleanup::class => PermalinkCleanupInformation::class,
SiteActivePlugins::class => SiteActivePluginsInformation::class,
SiteActiveTheme::class => SiteActiveThemeInformation::class,
);