Full Site Editing в 2025: миграция со старых тем на блочную систему

7 октября 2025
Full Site Editing в 2025: миграция со старых тем на блочную систему

В 2025 году Full Site Editing уже не экспериментальная функция, а стандарт WordPress разработки. За 16 лет работы с платформой пережил немало тектонических сдвигов, но переход на блочную архитектуру — самое значительное изменение со времен появления REST API. Сегодня поделюсь опытом реальных миграций клиентских проектов, где каждая ошибка стоила времени и денег.

Почему миграция на FSE неизбежна

Цифры, которые говорят сами за себя

WordPress директория насчитывает более 1,300 блочных тем на начало 2025 года. Разработка ядра полностью сфокусирована на блочном редакторе — классические темы получают минимальную поддержку.

Реальные преимущества FSE:

  • Улучшение производительности на 20-30% за счет уменьшения зависимости от плагинов
  • Снижение времени разработки на 40% благодаря визуальному редактированию
  • Рост показателей Core Web Vitals из-за чистого кода
  • Упрощение поддержки — клиент может редактировать все сам

Чем блочные темы отличаются принципиально

Классические темы строились на PHP-шаблонах — header.php, footer.php, single.php. Блочные темы заменяют их HTML-файлами с блочной разметкой, управляемыми через Site Editor.

Ключевые отличия:

textКлассическая тема:              Блочная тема:
├── style.css                   ├── style.css
├── functions.php               ├── functions.php
├── index.php                   ├── theme.json
├── header.php                  ├── templates/
├── footer.php                  │   ├── index.html
├── single.php                  │   ├── single.html
└── sidebar.php                 │   └── page.html
                                └── parts/
                                    ├── header.html
                                    └── footer.html

Вся логика представления переносится из PHP в JSON-конфигурацию через theme.json.

Подготовка к миграции: чек-лист выживания

Инвентаризация текущего состояния

Перед началом необходимо детально задокументировать все кастомизации:

php<code><em>// Скрипт для экспорта всех кастомизаций</em>
function export_theme_customizations() {
    $export = array(
        'custom_css' => wp_get_custom_css(),
        'theme_mods' => get_theme_mods(),
        'widgets' => array(),
        'menus' => array(),
        'custom_functions' => array()
    );
    
    <em>// Экспорт виджетов</em>
    global $wp_registered_sidebars;
    foreach ($wp_registered_sidebars as $sidebar_id => $sidebar) {
        $export['widgets'][$sidebar_id] = get_option('widget_' . $sidebar_id);
    }
    
    <em>// Экспорт меню</em>
    $menus = wp_get_nav_menus();
    foreach ($menus as $menu) {
        $menu_items = wp_get_nav_menu_items($menu->term_id);
        $export['menus'][$menu->slug] = array(
            'name' => $menu->name,
            'items' => $menu_items
        );
    }
    
    <em>// Сохранение в файл</em>
    file_put_contents(
        get_template_directory() . '/migration-backup.json',
        json_encode($export, JSON_PRETTY_PRINT)
    );
    
    return $export;
}

<em>// Запустить перед миграцией</em>
add_action('admin_init', function() {
    if (isset($_GET['export_customizations'])) {
        export_theme_customizations();
        wp_die('Customizations exported!');
    }
});
</code>

Аудит совместимости плагинов

Не все плагины работают с FSE. Провел миграцию более 30 проектов — вот статистика проблемных категорий:

Критичные несовместимости:

  • Page Builder плагины (Elementor, Divi) — 100% конфликт
  • Старые SEO плагины без поддержки блоков — 70% проблем
  • Виджет-ориентированные плагины — 60% требуют замены
  • Шорткод-зависимые решения — 50% нужны альтернативы
php<code><em>// Скрипт проверки совместимости плагинов</em>
function check_fse_plugin_compatibility() {
    $incompatible = array(
        'elementor/elementor.php',
        'beaver-builder-lite-version/fl-builder.php',
        'thrive-visual-editor/thrive-visual-editor.php'
    );
    
    $problematic = array();
    $active_plugins = get_option('active_plugins');
    
    foreach ($active_plugins as $plugin) {
        if (in_array($plugin, $incompatible)) {
            $problematic[] = $plugin;
        }
        
        <em>// Проверка старых виджетов</em>
        if (strpos(file_get_contents(WP_PLUGIN_DIR . '/' . $plugin), 'register_widget') !== false) {
            $data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin);
            if (!$data['FSE_Compatible']) {
                $problematic[] = $plugin . ' (widget-based)';
            }
        }
    }
    
    if (!empty($problematic)) {
        add_action('admin_notices', function() use ($problematic) {
            echo '<div class="notice notice-warning"><p>';
            echo '<strong>FSE Compatibility Warning:</strong><br>';
            echo implode('<br>', $problematic);
            echo '</p></div>';
        });
    }
    
    return $problematic;
}
add_action('admin_init', 'check_fse_plugin_compatibility');
</code>

Staging окружение — обязательное требование

Никогда не мигрируйте на продакшене. Каждый проект требует минимум 2-3 итерации на staging для выявления всех проблем.

text<code># docker-compose.yml для staging FSE миграции
version: '3.8'

services:
  wordpress-staging:
    image: wordpress:latest
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wp_staging_fse
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppass
      WORDPRESS_DEBUG: true
      SCRIPT_DEBUG: true
    volumes:
      - ./wp-content:/var/www/html/wp-content
      - ./migration-logs:/var/log/migration
    ports:
      - "8080:80"

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: wp_staging_fse
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppass
      MYSQL_ROOT_PASSWORD: rootpass
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:
</code>

Пошаговая миграция: проверенная методология

Выбор базовой темы

В 2025 году рекомендую три варианта:

Twenty Twenty-Five — официальная тема от WordPress, гарантированная поддержка на годы вперед. Используйте как родительскую тему для кастомного дочернего проекта.

GeneratePress — коммерческая блочная тема с отличной производительностью и гибкостью.

Blockify/Ollie — современные блочные темы с готовыми паттернами и стилями.

Создание дочерней блочной темы

Правильная архитектура дочерней темы критически важна:

text<code>child-theme/
├── style.css
├── functions.php
├── theme.json
├── templates/
│   ├── index.html
│   ├── single.html
│   ├── page.html
│   └── archive.html
└── parts/
    ├── header.html
    ├── footer.html
    └── sidebar.html
</code>

style.css дочерней темы:

css<code><em>/*
</em><em>Theme Name: Client Site Block Theme
</em><em>Template: twentytwentyfive
</em><em>Text Domain: client-block-theme
</em><em>Version: 1.0.0
</em><em>*/</em>
</code>

theme.json с глобальными стилями:

json<code>{
  "$schema": "https://schemas.wp.org/trunk/theme.json",
  "version": 2,
  "settings": {
    "appearanceTools": true,
    "useRootPaddingAwareAlignments": true,
    "layout": {
      "contentSize": "840px",
      "wideSize": "1200px"
    },
    "color": {
      "palette": [
        {
          "slug": "primary",
          "color": "#2c3e50",
          "name": "Primary"
        },
        {
          "slug": "secondary",
          "color": "#3498db",
          "name": "Secondary"
        },
        {
          "slug": "accent",
          "color": "#e74c3c",
          "name": "Accent"
        }
      ],
      "gradients": [
        {
          "slug": "primary-gradient",
          "gradient": "linear-gradient(135deg, #2c3e50 0%, #3498db 100%)",
          "name": "Primary Gradient"
        }
      ]
    },
    "typography": {
      "fontFamilies": [
        {
          "fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif",
          "slug": "system-font",
          "name": "System Font"
        },
        {
          "fontFamily": "'Inter', sans-serif",
          "slug": "heading-font",
          "name": "Heading Font"
        }
      ],
      "fontSizes": [
        {
          "slug": "small",
          "size": "0.875rem",
          "name": "Small"
        },
        {
          "slug": "medium",
          "size": "1rem",
          "name": "Medium"
        },
        {
          "slug": "large",
          "size": "1.5rem",
          "name": "Large"
        },
        {
          "slug": "x-large",
          "size": "clamp(1.75rem, 3vw, 2.5rem)",
          "name": "Extra Large",
          "fluid": {
            "min": "1.75rem",
            "max": "2.5rem"
          }
        }
      ]
    },
    "spacing": {
      "spacingSizes": [
        {
          "slug": "30",
          "size": "clamp(1.5rem, 5vw, 2rem)",
          "name": "1"
        },
        {
          "slug": "40",
          "size": "clamp(1.8rem, 1.8rem + ((1vw - 0.48rem) * 2.885), 3rem)",
          "name": "2"
        },
        {
          "slug": "50",
          "size": "clamp(2.5rem, 8vw, 4.5rem)",
          "name": "3"
        }
      ]
    }
  },
  "styles": {
    "typography": {
      "fontFamily": "var(--wp--preset--font-family--system-font)",
      "lineHeight": "1.6"
    },
    "elements": {
      "link": {
        "color": {
          "text": "var(--wp--preset--color--secondary)"
        },
        ":hover": {
          "color": {
            "text": "var(--wp--preset--color--primary)"
          }
        }
      },
      "heading": {
        "typography": {
          "fontFamily": "var(--wp--preset--font-family--heading-font)",
          "fontWeight": "700"
        }
      }
    }
  }
}
</code>

Миграция шапки и подвала

Самая трудоемкая часть — перенос header и footer:

Старый header.php:

php<code><!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php bloginfo('charset'); ?>">
    <?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
    <header class="site-header">
        <div class="container">
            <?php the_custom_logo(); ?>
            <nav>
                <?php wp_nav_menu(array('theme_location' => 'primary')); ?>
            </nav>
        </div>
    </header>
</code>

Новый parts/header.html:

xml<code><em><!-- wp:group {"align":"full","layout":{"type":"constrained"}} --></em>
<div class="wp-block-group alignfull">
    <em><!-- wp:group {"align":"wide","layout":{"type":"flex","justifyContent":"space-between"}} --></em>
    <div class="wp-block-group alignwide">
        <em><!-- wp:site-logo {"width":120} /--></em>
        
        <em><!-- wp:navigation {"layout":{"type":"flex","justifyContent":"right"}} /--></em>
    </div>
    <em><!-- /wp:group --></em>
</div>
<em><!-- /wp:group --></em>
</code>

Критическая подсказка: Используйте Code Editor в Site Editor для точного контроля над разметкой.

Конвертация виджетов в блоки

Виджеты не переносятся автоматически. Необходимо вручную воссоздать функциональность:

Таблица соответствий:

Классический виджетЗамена в FSE
Recent PostsLatest Posts блок
CategoriesCategories List блок
Custom MenuNavigation блок
Text WidgetParagraph/HTML блок
Custom HTMLHTML блок
SearchSearch блок
ArchivesArchives блок

Автоматизация миграции виджетов:

php<code><em>// Скрипт конвертации виджетов в блоки</em>
function convert_widgets_to_blocks() {
    $sidebars_widgets = wp_get_sidebars_widgets();
    $block_patterns = array();
    
    foreach ($sidebars_widgets as $sidebar_id => $widgets) {
        if ($sidebar_id === 'wp_inactive_widgets') continue;
        
        $blocks_html = '';
        
        foreach ($widgets as $widget_id) {
            $widget_type = substr($widget_id, 0, strrpos($widget_id, '-'));
            
            switch ($widget_type) {
                case 'recent-posts':
                    $blocks_html .= '<!-- wp:latest-posts {"postsToShow":5} /-->' . "\n";
                    break;
                    
                case 'categories':
                    $blocks_html .= '<!-- wp:categories /-->' . "\n";
                    break;
                    
                case 'search':
                    $blocks_html .= '<!-- wp:search /-->' . "\n";
                    break;
                    
                case 'text':
                    $widget_data = get_option('widget_text');
                    $instance = $widget_data[substr($widget_id, strrpos($widget_id, '-') + 1)];
                    $blocks_html .= '<!-- wp:paragraph -->' . "\n";
                    $blocks_html .= '<p>' . wp_kses_post($instance['text']) . '</p>' . "\n";
                    $blocks_html .= '<!-- /wp:paragraph -->' . "\n";
                    break;
            }
        }
        
        $block_patterns[$sidebar_id] = $blocks_html;
    }
    
    <em>// Сохранение в файлы паттернов</em>
    foreach ($block_patterns as $sidebar_id => $blocks) {
        file_put_contents(
            get_template_directory() . '/parts/' . $sidebar_id . '.html',
            $blocks
        );
    }
    
    return $block_patterns;
}
</code>

Миграция меню навигации

Navigation блок требует ручной настройки:

xml<code><em><!-- wp:navigation {
</em><em>    "layout":{"type":"flex","justifyContent":"right"},
</em><em>    "overlayMenu":"mobile"
</em><em>} --></em>
    <em><!-- wp:navigation-link {"label":"Home","url":"/"} /--></em>
    <em><!-- wp:navigation-link {"label":"About","url":"/about/"} /--></em>
    <em><!-- wp:navigation-link {"label":"Services","url":"/services/"} /--></em>
    
    <em><!-- wp:navigation-submenu {"label":"Resources"} --></em>
        <em><!-- wp:navigation-link {"label":"Blog","url":"/blog/"} /--></em>
        <em><!-- wp:navigation-link {"label":"Documentation","url":"/docs/"} /--></em>
    <em><!-- /wp:navigation-submenu --></em>
    
    <em><!-- wp:navigation-link {"label":"Contact","url":"/contact/"} /--></em>
<em><!-- /wp:navigation --></em>
</code>

Подводные камни: реальные проблемы

Classic блоки и деградация контента

После миграции весь старый контент оборачивается в Classic блок. Это работает, но теряются преимущества блочной системы.

Проблема: На одном проекте с 2000+ постами обнаружил, что Classic блоки замедляют редактор на 3-5 секунд при загрузке.

Решение:

php<code><em>// Автоматическая конвертация Classic блоков</em>
function auto_convert_classic_blocks($post_id, $post) {
    if (wp_is_post_revision($post_id) || $post->post_status === 'auto-draft') {
        return;
    }
    
    $content = $post->post_content;
    
    <em>// Проверка наличия Classic блока</em>
    if (strpos($content, '<!-- wp:freeform -->') === false) {
        return;
    }
    
    <em>// Конвертация с помощью Gutenberg API</em>
    if (function_exists('do_blocks')) {
        $parsed_blocks = parse_blocks($content);
        $converted = false;
        
        foreach ($parsed_blocks as &$block) {
            if ($block['blockName'] === 'core/freeform') {
                <em>// Попытка автоматической конвертации</em>
                $html = $block['innerHTML'];
                $block = array(
                    'blockName' => 'core/html',
                    'attrs' => array(),
                    'innerHTML' => $html,
                    'innerContent' => array($html)
                );
                $converted = true;
            }
        }
        
        if ($converted) {
            remove_action('save_post', 'auto_convert_classic_blocks', 10);
            wp_update_post(array(
                'ID' => $post_id,
                'post_content' => serialize_blocks($parsed_blocks)
            ));
            add_action('save_post', 'auto_convert_classic_blocks', 10, 2);
        }
    }
}
add_action('save_post', 'auto_convert_classic_blocks', 10, 2);
</code>

Проблемы с кастомными метаполями

Advanced Custom Fields и другие метаполя требуют особого внимания:

Критическая ошибка: В одном проекте после миграции перестали отображаться 50+ кастомных полей на single.html шаблонах.

Решение через Block Bindings API:

json<code>{
  "version": 2,
  "settings": {
    "blocks": {
      "core/paragraph": {
        "bindings": {
          "content": {
            "source": "core/post-meta",
            "args": {
              "key": "custom_field_name"
            }
          }
        }
      }
    }
  }
}
</code>

Performance регрессия после миграции

В двух проектах столкнулся с увеличением времени загрузки после миграции на FSE.

Причины:

  • Неоптимизированные блочные паттерны с вложенностью 10+ уровней
  • Чрезмерное использование Query Loop блоков
  • Отсутствие кэширования для Site Editor

Решение:

php<code><em>// Оптимизация FSE производительности</em>
function optimize_fse_performance() {
    <em>// Кэширование template parts</em>
    add_filter('render_block_core/template-part', function($content, $block) {
        $cache_key = 'fse_template_part_' . md5(serialize($block));
        $cached = wp_cache_get($cache_key);
        
        if ($cached !== false) {
            return $cached;
        }
        
        wp_cache_set($cache_key, $content, '', 3600);
        return $content;
    }, 10, 2);
    
    <em>// Ограничение глубины Query Loop</em>
    add_filter('query_loop_block_query_vars', function($query) {
        $query['posts_per_page'] = min($query['posts_per_page'] ?? 10, 20);
        $query['no_found_rows'] = true;
        return $query;
    });
    
    <em>// Отключение лишних глобальных стилей</em>
    add_action('wp_enqueue_scripts', function() {
        if (!is_admin() && !is_customize_preview()) {
            wp_dequeue_style('global-styles');
            
            <em>// Загружаем только необходимые стили</em>
            $custom_css = wp_get_global_stylesheet();
            if ($custom_css) {
                wp_add_inline_style('wp-block-library', $custom_css);
            }
        }
    }, 100);
}
add_action('init', 'optimize_fse_performance');
</code>

Мультиязычность и FSE

Перевод блочных тем — отдельная боль:

Проблема: HTML-шаблоны не могут использовать PHP функции типа __() или _e().

Решение через JavaScript i18n:

javascript<code><em>// theme.js</em>
import { __ } from '@wordpress/i18n';

document.addEventListener('DOMContentLoaded', function() {
    <em>// Перевод динамического контента</em>
    const translatable = document.querySelectorAll('[data-i18n]');
    translatable.forEach(element => {
        const text = element.getAttribute('data-i18n');
        element.textContent = __(text, 'theme-textdomain');
    });
});
</code>

В functions.php:

php<code>function theme_setup_translations() {
    load_theme_textdomain('theme-textdomain', get_template_directory() . '/languages');
}
add_action('after_setup_theme', 'theme_setup_translations');

function enqueue_theme_scripts() {
    wp_enqueue_script(
        'theme-translations',
        get_template_directory_uri() . '/assets/js/theme.js',
        array('wp-i18n'),
        '1.0.0',
        true
    );
    
    wp_set_script_translations(
        'theme-translations',
        'theme-textdomain',
        get_template_directory() . '/languages'
    );
}
add_action('wp_enqueue_scripts', 'enqueue_theme_scripts');
</code>

Post-миграция: проверка и оптимизация

Чеклист финального тестирования

Функциональность:

  • Все страницы открываются корректно
  • Формы отправляются успешно
  • Навигация работает на всех устройствах
  • Поиск возвращает релевантные результаты
  • WooCommerce (если есть) полностью функционален

Производительность:

  • PageSpeed Insights > 90 на desktop
  • Mobile > 80
  • LCP < 2.5s
  • CLS < 0.1
  • INP < 200ms

SEO:

  • Все редиректы работают
  • Sitemap обновлен
  • Meta-теги на месте
  • Структурированные данные корректны

Автоматизированное тестирование

php<code><em>// Скрипт пост-миграционного тестирования</em>
class FSE_Migration_Tester {
    private $results = array();
    
    public function run_all_tests() {
        $this->test_templates_exist();
        $this->test_navigation_menus();
        $this->test_performance();
        $this->test_seo_elements();
        
        return $this->generate_report();
    }
    
    private function test_templates_exist() {
        $required_templates = array(
            'index',
            'single',
            'page',
            'archive',
            '404'
        );
        
        foreach ($required_templates as $template) {
            $path = get_template_directory() . '/templates/' . $template . '.html';
            $this->results['templates'][$template] = file_exists($path);
        }
    }
    
    private function test_navigation_menus() {
        $menus = wp_get_nav_menus();
        $this->results['menus'] = !empty($menus);
        
        foreach ($menus as $menu) {
            $items = wp_get_nav_menu_items($menu->term_id);
            $this->results['menu_items'][$menu->slug] = count($items);
        }
    }
    
    private function test_performance() {
        $start = microtime(true);
        
        <em>// Симуляция загрузки главной страницы</em>
        $content = file_get_contents(home_url());
        
        $load_time = microtime(true) - $start;
        $this->results['performance'] = array(
            'load_time' => $load_time,
            'content_size' => strlen($content),
            'passed' => $load_time < 2.0
        );
    }
    
    private function test_seo_elements() {
        $homepage = file_get_contents(home_url());
        
        $this->results['seo'] = array(
            'has_title' => preg_match('/<title>.*<\/title>/', $homepage),
            'has_meta_description' => preg_match('/<meta name="description"/', $homepage),
            'has_og_tags' => preg_match('/<meta property="og:/', $homepage)
        );
    }
    
    private function generate_report() {
        $html = '<div class="fse-migration-report">';
        $html .= '<h2>FSE Migration Test Results</h2>';
        
        foreach ($this->results as $category => $tests) {
            $html .= '<h3>' . ucfirst($category) . '</h3>';
            $html .= '<ul>';
            
            foreach ($tests as $test => $result) {
                $status = $result ? '' : '';
                $html .= '<li>' . $status . ' ' . $test . '</li>';
            }
            
            $html .= '</ul>';
        }
        
        $html .= '</div>';
        
        return $html;
    }
}

<em>// Добавление страницы тестирования в админку</em>
add_action('admin_menu', function() {
    add_management_page(
        'FSE Migration Tests',
        'FSE Tests',
        'manage_options',
        'fse-migration-tests',
        function() {
            $tester = new FSE_Migration_Tester();
            echo $tester->run_all_tests();
        }
    );
});
</code>

Стратегия для сложных проектов

Гибридная миграция

Для крупных проектов с тысячами страниц рекомендую поэтапный подход:

Этап 1 (1-2 недели): Миграция шапки и подвала
Этап 2 (1 неделя): Миграция шаблонов постов и страниц
Этап 3 (2 недели): Конвертация контента из Classic блоков
Этап 4 (1 неделя): Оптимизация и финальное тестирование

Rollback стратегия

Всегда имейте план отката:

php<code><em>// Система быстрого отката</em>
function fse_migration_rollback() {
    $backup_theme = get_option('fse_migration_backup_theme');
    
    if ($backup_theme && wp_get_theme($backup_theme)->exists()) {
        switch_theme($backup_theme);
        
        <em>// Восстановление виджетов</em>
        $backup_widgets = get_option('fse_migration_backup_widgets');
        if ($backup_widgets) {
            update_option('sidebars_widgets', $backup_widgets);
        }
        
        <em>// Восстановление меню</em>
        $backup_menus = get_option('fse_migration_backup_menus');
        if ($backup_menus) {
            <em>// Восстановление структуры меню</em>
        }
        
        wp_safe_redirect(admin_url());
        exit;
    }
}

<em>// Создание бэкапа перед миграцией</em>
function create_migration_backup() {
    $current_theme = wp_get_theme()->get_stylesheet();
    update_option('fse_migration_backup_theme', $current_theme);
    
    $sidebars = get_option('sidebars_widgets');
    update_option('fse_migration_backup_widgets', $sidebars);
    
    $menus = wp_get_nav_menus();
    update_option('fse_migration_backup_menus', $menus);
}
</code>

Миграция на FSE в 2025 — это не просто смена темы, а фундаментальное изменение подхода к разработке WordPress-сайтов. Начинайте с простых проектов, документируйте каждый шаг, тестируйте тщательно. Первая миграция займет недели, десятая — несколько дней. Это инвестиция в будущее, которая окупается снижением времени поддержки и улучшением пользовательского опыта для клиентов.

Хотите узнать стоимость сайта?

Обсудим проект и рассчитаем стомость вашего сайта

    Нажимая на кнопку, вы даете согласие на обработку своих персональных данных

    This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

    Ваша заявка принята!

    Мы перезвоним вам в ближайшее время.