Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 ( strpos( $value, '/blog/' ) === 0 ) {
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,
);