A fresh install leaves `permalink_structure` empty, so WordPress snapshots
$wp_rewrite with no post/page permalink rules and every single post resolves
to 404 until someone re-saves Settings -> Permalinks.
The existing mitigation fired too late to help: routing.php wrote the option
and flushed from `init`, but WP_Rewrite was already constructed with the empty
structure, and core defers pre-`wp_loaded` flushes to a `wp_loaded` callback
that regenerates from that stale snapshot. The rewrite_rules option ended up
non-empty but broken, so the 404 persisted on every request.
New \Jio\Core\RewriteBootstrap owns this: on `after_setup_theme` (still inside
wp-settings.php, before wp-blog-header.php parses the query) it calls
$wp_rewrite->set_permalink_structure() — the same API wp-admin/options-permalink.php
uses — which persists the option *and* refreshes the in-memory snapshot, so both
the current request and the rules written for the next one are correct. A
structure an administrator chose is never overridden.
Also consolidates the two duplicated flush sites that lived inline in
functions.php (`after_switch_theme` closure and the `jio_flushed_version`
block, whose `delete_option('rewrite_rules')` forced a needless regenerate),
and drops the now-redundant permalink write from routing.php. The version
stamp moves to `wp_loaded` priority 20 so it sees the complete rule set
instead of the pre-`add_rewrite_rule()` snapshot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
95 lines
3.3 KiB
PHP
95 lines
3.3 KiB
PHP
<?php
|
|
/**
|
|
* Permalink bootstrap + rewrite rule flush owner.
|
|
*
|
|
* WordPress snapshots `permalink_structure` into `$wp_rewrite` before the theme loads
|
|
* (wp-settings.php, ahead of functions.php). On a fresh install that option is empty, so
|
|
* no post/page permalink rules exist and every single post resolves to 404 until someone
|
|
* re-saves Settings → Permalinks.
|
|
*
|
|
* `ensure_permalink_structure()` mirrors what wp-admin/options-permalink.php does:
|
|
* `WP_Rewrite::set_permalink_structure()` persists the option *and* refreshes the in-memory
|
|
* snapshot for the current request. Hooking it on `after_setup_theme` (still inside
|
|
* wp-settings.php, before the main query is parsed) fixes the very first request instead of
|
|
* only the next one.
|
|
*
|
|
* @package Jio
|
|
*/
|
|
|
|
namespace Jio\Core;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class RewriteBootstrap {
|
|
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Structure installed on a site that never chose one. `/%postname%/` is the CMS-friendly
|
|
* default and is what the theme's own routing has always assumed.
|
|
*/
|
|
public const DEFAULT_STRUCTURE = '/%postname%/';
|
|
|
|
private function __construct() {}
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
public function init(): void {
|
|
// Runs inside wp-settings.php, after the theme's rules are registered but before
|
|
// wp-blog-header.php parses the query — the last point this can still affect the
|
|
// current request.
|
|
add_action('after_setup_theme', [$this, 'ensure_permalink_structure'], 1);
|
|
add_action('after_switch_theme', [$this, 'flush_on_activate']);
|
|
// Version bumps land here rather than on `init`: `init` runs before every
|
|
// add_rewrite_rule() callback, and core defers pre-wp_loaded flushes anyway.
|
|
add_action('wp_loaded', [$this, 'flush_when_version_changes'], 20);
|
|
}
|
|
|
|
/**
|
|
* Guarantee a usable permalink structure so pretty rules actually apply.
|
|
*
|
|
* A structure the administrator chose is never overridden — only an empty one is filled.
|
|
*/
|
|
public function ensure_permalink_structure(): void {
|
|
$structure = (string) get_option('permalink_structure');
|
|
if ($structure !== '') {
|
|
return;
|
|
}
|
|
|
|
global $wp_rewrite;
|
|
if ($wp_rewrite instanceof \WP_Rewrite) {
|
|
$wp_rewrite->set_permalink_structure(self::DEFAULT_STRUCTURE);
|
|
return;
|
|
}
|
|
|
|
// No rewrite object (CLI/tests): persist the option so the next request boots sane.
|
|
update_option('permalink_structure', self::DEFAULT_STRUCTURE);
|
|
}
|
|
|
|
/**
|
|
* Theme activation: persist the theme's own rewrite rules immediately.
|
|
*/
|
|
public function flush_on_activate(): void {
|
|
flush_rewrite_rules();
|
|
}
|
|
|
|
/**
|
|
* Self-heal after a theme version bump without the repeated flush the old
|
|
* `init`-hook version stamp caused on every first request of a new version.
|
|
*/
|
|
public function flush_when_version_changes(): void {
|
|
$flushed = (string) get_option('jio_rewrite_flushed_version', '');
|
|
if ($flushed === JIO_VERSION) {
|
|
return;
|
|
}
|
|
|
|
flush_rewrite_rules();
|
|
update_option('jio_rewrite_flushed_version', JIO_VERSION);
|
|
}
|
|
}
|