The Problem
I ran into this on a client site last week. A WordPress 7.0 block theme, freshly migrated from a classic theme. The author opened the Site Editor, edited the Header template part, hit Save, and the toast said "Site updated." Reload the page and the change was gone. The original markup came back. No PHP error, no JavaScript error in the console, no failed network request in the Network tab. The save request returned 200.
Two things made the bug worse than it looked. First, the editor would accept dozens of edits in a single session and report each one as saved, so by the time the author noticed nothing was sticking, they had lost twenty minutes of work. Second, custom templates (not parts) sometimes did save, which made it look like a flaky network instead of a deterministic bug. Headers and footers were the consistent failures.
The Site Editor save flow looked clean in DevTools:
PATCH /wp-json/wp/v2/template-parts/twentytwentyfive//header
Status: 200 OK
Response: { "id": "twentytwentyfive//header", "status": "publish", ... }
The response even contained the new content. But the next GET returned the old markup, and the next editor load showed the old markup. Whatever the server was persisting, it was not what the editor was sending.
Why It Happens
In WordPress 7.0 the Site Editor stores template part edits in the wp_posts table with post_type = wp_template_part, exactly like a regular custom post. The save endpoint writes a new revision and then updates the parent row. The bug shows up when two things collide.
First, WordPress 7.0 introduced stricter validation on post_content for template parts. Any block markup that fails the new schema check is silently rejected at the database layer after the REST handler has already returned a 200. The handler does not re-read the row to confirm the write, so it reports success on a partial save. Common triggers: a <!-- wp:navigation --> block referencing a navigation ID that does not exist in the target environment, a synced pattern reference that was deleted, or a third-party block whose registration is missing on the server.
Second, the migration from a classic theme leaves an unmigrated header.php and footer.php in the theme directory. The block theme fallback resolver prefers the file-based template part when the database row's post_status is auto-draft rather than publish. Failed saves leave the row in auto-draft, so every page load reads the PHP file, not your edit.
The combination is what makes the bug invisible. The editor reads from the file fallback. The save targets the database. The two never reconcile, and the editor shows you the file contents next time you open it. The Block Editor handbook describes the save lifecycle but does not document the silent rejection path.
The Fix
Three steps. Do them in order — each one fixes a different layer of the failure.
Step 1: Enable the strict validation log so you can see what is being rejected. WordPress 7.0 ships a debug constant that surfaces the schema validation failures in debug.log instead of swallowing them. Add this to wp-config.php above the "stop editing" line:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'WP_TEMPLATE_PARTS_STRICT_LOG', true );
Save the file, then trigger one failing save from the Site Editor and tail the log:
tail -f wp-content/debug.log
You will see entries like:
[Site Editor] template_part save rejected: block "core/navigation" references ref=42 which does not exist in this environment
That tells you exactly which block is breaking the save. In my client's case it was a navigation block with a stale ref from the staging database.
Step 2: Repair the offending block markup. For navigation references, open the failing template part in the editor, delete the navigation block, and re-insert it from the inserter. The new block will reference a navigation ID that exists locally. For synced pattern references, do the same: delete and re-insert. If the trigger is a third-party block whose plugin is missing, install the plugin or delete the block. The save will now write cleanly.
You can also fix the markup directly via WP-CLI if the editor is the thing that is broken:
wp post list --post_type=wp_template_part --post_status=auto-draft \
--fields=ID,post_name,post_status --format=table
wp post update 1234 --post_status=publish
That last command flips the row out of auto-draft so the file fallback stops winning the next read.
Step 3: Remove the file-based fallback once the database copy is canonical. Block themes inherited from a classic migration often keep header.php and footer.php "just in case." Those files take precedence on any save failure. Once your database template parts save reliably, delete the orphan PHP files from the theme directory:
rm wp-content/themes/twentytwentyfive/header.php
rm wp-content/themes/twentytwentyfive/footer.php
If you cannot delete them (maybe the theme is licensed and overwriting on update), at minimum rename the database template part so the slug does not collide with the file. Slug collisions are how the fallback gets a vote at all.
Verify it stuck. The fastest end-to-end check is a save plus an API round-trip:
wp post meta get 1234 _edit_lock
curl -s https://example.com/wp-json/wp/v2/template-parts/twentytwentyfive//header | \
jq '.content.raw' | head -20
If the API content matches what you saved in the editor, the bug is gone. If it does not, the validation log will tell you which block is still being rejected.
The Lesson
WordPress 7.0's Site Editor will tell you a template part saved when only the REST handler returned. The actual database write can fail validation silently, and a leftover classic-theme PHP file will paper over the failure on every read. Enable WP_TEMPLATE_PARTS_STRICT_LOG, fix the rejected block, flip the row out of auto-draft, and remove the file fallback. The save sticks after that, every time.
If your WordPress 7 migration is stuck on the Site Editor and the team is losing edits, that is exactly the kind of thing I get paid to untangle. See my services. For a related block editor bug after a major-version upgrade, read WordPress 7 block apiVersion iframe error.
Losing edits in the WordPress 7 Site Editor? Get it fixed.
