| @@ -239,18 +239,28 @@ | ||
| 239 | 239 | // Capture the previously-stored value so a genuine write failure can be |
| 240 | 240 | // told apart from a no-op save (payload identical to what's stored). |
| 241 | 241 | $previous = $all_settings[$post_type] ?? null; |
| 242 | 242 | |
| 243 | + // MERGE, never replace. This entity row is shared: the Content Type | |
| 244 | + // Matrix stores its per-feature tri-states (meta_enabled, schema_enabled, | |
| 245 | + // open_graph_enabled, twitter_card_enabled, analytics_enabled) and its | |
| 246 | + // sitemap_include flag under the SAME key, and normalize_settings_patch() | |
| 247 | + // whitelists only the Global SEO fields — so a wholesale replace here | |
| 248 | + // silently dropped every matrix choice the moment the user saved the | |
| 249 | + // Global SEO screen next door. Global SEO's own keys still win, because | |
| 250 | + // sanitize_settings() emits them complete. | |
| 251 | + $merged = array_merge(is_array($previous) ? $previous : [], $sanitized_settings); | |
| 252 | + | |
| 243 | 253 | // Update settings for this post type |
| 244 | - $all_settings[$post_type] = $sanitized_settings; | |
| 254 | + $all_settings[$post_type] = $merged; | |
| 245 | 255 | |
| 246 | 256 | // Save to database |
| 247 | 257 | $updated = update_option(self::OPTION_NAME, $all_settings); |
| 248 | 258 | |
| 249 | - if ($updated || $previous === $sanitized_settings) { | |
| 259 | + if ($updated || $previous === $merged) { | |
| 250 | 260 | return new WP_REST_Response([ |
| 251 | 261 | 'success' => true, |
| 252 | - 'data' => $sanitized_settings, | |
| 262 | + 'data' => $merged, | |
| 253 | 263 | 'post_type' => $post_type, |
| 254 | 264 | 'message' => sprintf('Settings saved successfully for post type: %s', $post_type) |
| 255 | 265 | ], 200); |
| 256 | 266 | } |
| @@ -300,10 +310,21 @@ | ||
| 300 | 310 | |
| 301 | 311 | // Get all settings |
| 302 | 312 | $all_settings = get_option(self::OPTION_NAME, []); |
| 303 | 313 | |
| 304 | - // Remove settings for this post type (will fall back to defaults) | |
| 305 | - unset($all_settings[$post_type]); | |
| 314 | + // Reset only what this screen owns. The entity row is shared with the | |
| 315 | + // Content Type Matrix, so dropping the whole row would reset the user's | |
| 316 | + // per-feature tri-states and sitemap choice as a side effect of a button | |
| 317 | + // that says nothing about them. DEFAULT_SETTINGS is the exact set of | |
| 318 | + // Global SEO keys, so anything outside it belongs to another screen. | |
| 319 | + $stored = $all_settings[$post_type] ?? []; | |
| 320 | + $kept = is_array($stored) ? array_diff_key($stored, self::DEFAULT_SETTINGS) : []; | |
| 321 | + | |
| 322 | + if ($kept === []) { | |
| 323 | + unset($all_settings[$post_type]); | |
| 324 | + } else { | |
| 325 | + $all_settings[$post_type] = $kept; | |
| 326 | + } | |
| 306 | 327 | |
| 307 | 328 | // Save updated settings |
| 308 | 329 | update_option(self::OPTION_NAME, $all_settings); |
| 309 | 330 | |