PHP Framework Migration
Migrate a Terra WordPress theme onto the shared PHP framework: a config-driven structure where ACF fields, post types, taxonomies, blocks, admin pages, AJAX, and endpoints are declared as config and built by the framework. The end goal is a functions/ directory with only two folders โ framework (cloned shared code) and project (site-specific code).
The work is organized in five phases: Setup โ Config โ Utilities & cleanup โ Data migration โ Deploy.
Prerequisites
Section titled โPrerequisitesโHave these ready before you start โ several steps stall without them:
- Access to the shared framework repo (to clone in step 1).
- ACF Pro active on the project.
- The exported ACF field-group JSON for the project, split into four groups youโll feed into the config steps:
- all post type fields (everything except flexible modules, flexible heros, and general options),
- general options,
- flexible heros,
- flexible modules.
- A working dev environment (the data-migration scripts in Phase 4 are dev-only).
- A database backup โ Phase 4 writes directly to
wp_postmeta.
Quick reference
Section titled โQuick referenceโTarget functions/ structure
Section titled โTarget functions/ structureโfunctions/โโโ framework/ โ cloned shared repo (gitignored)โโโ project/ โโโ admin-pages/ โ โโโ dashboard.php โ custom admin dashboard screen โ โโโ csv-importer.php โ CSV importer admin screen โโโ deploy/ โ โโโ enqueues.php โ moved from project/ โ โโโ hash.php โ moved from project/ โ โโโ local-variable.php โ moved from project/ โโโ config/ โ โโโ index.php โ โโโ admin-controller_config.php โ โโโ admin-pages_config.php โ โโโ default_config.php โ โโโ post-types_config.php โ replaces post-types.php โ โโโ taxonomy_config.php โ replaces taxonomies.php โ โโโ post-type-fields_config.php โ โโโ dashboard_config.php โ โโโ wysiwyg-toolbars_config.php โ โโโ default-blocks_config.php โ โโโ custom-blocks_config.php โ โโโ ajax_config.php โ โโโ endpoint_config.php โ โโโ general-options/ โ โ โโโ index.php โ โโโ flexible-modules/ โ โ โโโ index.php โ โโโ flexible-heros/ โ โโโ index.php โโโ utilities/ โโโ index.php โโโ acf/ โ custom ACF moved hereFiles removed
Section titled โFiles removedโfunctions/project/post-types.phpโ moved intoconfig/post-types_config.phpfunctions/project/taxonomies.phpโ moved intoconfig/taxonomy_config.php
Files moved (no rename)
Section titled โFiles moved (no rename)โ| From | To |
|---|---|
functions/project/enqueues.php | functions/project/deploy/enqueues.php |
functions/project/hash.php | functions/project/deploy/hash.php |
functions/project/local-variable.php | functions/project/deploy/local-variable.php |
Phase 1 โ Setup
Section titled โPhase 1 โ Setupโ1. Clone the framework into functions/
Section titled โ1. Clone the framework into functions/โClone the shared framework repo into functions/framework/.
Then add it to .gitignore so the framework code is not tracked by the project repo:
functions/framework2. Create admin-pages/
Section titled โ2. Create admin-pages/โCreate the folder functions/project/admin-pages/ and add the following admin-screen files (copy-paste from the template):
dashboard.phpโ custom admin dashboard screencsv-importer.phpโ CSV importer admin screen
3. Create deploy/ and move deploy files
Section titled โ3. Create deploy/ and move deploy filesโCreate functions/project/deploy/ and move these files into it from functions/project/:
enqueues.phphash.phplocal-variable.php
Update any require / include paths that pointed at the old locations.
Phase 2 โ Config
Section titled โPhase 2 โ ConfigโThis phase replaces the themeโs hand-written declarations with config files under functions/project/config/, all wired up from config/index.php.
4. Create config/ and its files
Section titled โ4. Create config/ and its filesโCreate functions/project/config/ and add:
index.phpโ requires all the config files belowadmin-controller_config.phpโ minimal; registers the projectโs admin pagesadmin-pages_config.phpโ copy-paste from templatedefault_config.phpโ copy-paste from templatepost-types_config.phpโ move all post types here, then deletepost-types.phptaxonomy_config.phpโ move all taxonomies here, then deletetaxonomies.php
5. Create post-type-fields_config.php
Section titled โ5. Create post-type-fields_config.phpโCreate functions/project/config/post-type-fields_config.php for the post-type ACF fields.
Following the ACF-from-JSON workflow, use the post type JSON (everything except flexible modules, flexible heros, and general options) to generate this single config file.
6. Create config/general-options/
Section titled โ6. Create config/general-options/โCreate functions/project/config/general-options/ with an index.php.
Using the general options JSON, ask Claude Code to generate one PHP file per tab inside general-options/ and to register each of them in general-options/index.php (one require line per file).
7. Create config/flexible-modules/
Section titled โ7. Create config/flexible-modules/โCreate functions/project/config/flexible-modules/ with an index.php.
Using the flexible modules JSON, ask Claude Code to generate one PHP file per module inside flexible-modules/ and to register each of them in flexible-modules/index.php (one require line per file).
8. Create config/flexible-heros/
Section titled โ8. Create config/flexible-heros/โCreate functions/project/config/flexible-heros/ with an index.php.
Using the flexible heros JSON, ask Claude Code to generate one PHP file per hero inside flexible-heros/ and to register each of them in flexible-heros/index.php (one require line per file).
9. Create the remaining config files
Section titled โ9. Create the remaining config filesโ| File | How |
|---|---|
dashboard_config.php | Copy-paste, then update to the projectโs general options |
wysiwyg-toolbars_config.php | Copy-paste |
default-blocks_config.php | Copy-paste |
custom-blocks_config.php | Copy-paste, then add the projectโs custom blocks here |
ajax_config.php | Move the projectโs AJAX actions here |
endpoint_config.php | Move the projectโs REST API endpoints here |
Phase 3 โ Utilities & cleanup
Section titled โPhase 3 โ Utilities & cleanupโ10. Set up utilities/
Section titled โ10. Set up utilities/โCreate functions/project/utilities/index.php and import all of the projectโs utilities from there.
Move the projectโs custom ACF into functions/project/utilities/acf/ and require them from the utilities index.php.
11. Cleanup & conventions
Section titled โ11. Cleanup & conventionsโ- Confirm
functions/contains only theframeworkandprojectfolders. - Confirm the projectโs own custom ACFs are actually being used.
- Replace
generate_image_tagwithrender_wp_imageacross the project.
Phase 4 โ Data migration
Section titled โPhase 4 โ Data migrationโThese two scripts repair existing post data so it resolves against the frameworkโs field names. They are dev-only, one-shot scripts.
12. Custom Spacing ACF migration (conditional)
Section titled โ12. Custom Spacing ACF migration (conditional)โRun this only if there is a mismatch between the projectโs spacing field name (e.g. spacing) and the frameworkโs field name, which is always section_spacing. The frameworkโs ACF_Builder::spacing() produces section_spacing, so legacy data stored under *_spacing keys wonโt resolve.
Purpose: Renames legacy ACF spacing meta keys from *_spacing to *_section_spacing site-wide โ but only inside the flexible content fields modules and heros (matches modules_*_spacing, heros_*_spacing and their _modules_* / _heros_* field-key siblings, at any nesting depth). Includes drafts, pending, private, and future; excludes trash and revisions.
How to run: Create functions/project/utilities/migrate-spacing-global.php with the script below and require it from functions/project/utilities/index.php. Then โ logged in as an administrator (manage_options) โ visit:
/wp-admin/?run_migration=spacing_globalIt processes 50 posts per request and prints a Next batch link until no posts remain. When you see โNothing left to migrateโ, remove the require line and delete the file.
Script: migrate-spacing-global.php
<?php/** * One-shot DEV migration: rename ACF meta keys `..._spacing` โ `..._section_spacing` * site-wide, but ONLY inside the flexible content fields `modules` and `heros` * (matches `modules_*_spacing`, `heros_*_spacing` and their `_modules_*`, * `_heros_*` field-key siblings, at any nesting depth). * * Includes drafts, pending, private, future. Excludes trash and revisions. * * USAGE (run in dev, then remove the require + this file): * 1. Add to functions/project/utilities/index.php (temporarily): * require THEME_PATH . '/functions/project/utilities/migrate-spacing-global.php'; * 2. Logged in as admin, visit: * /wp-admin/?run_migration=spacing_global * (the script will paginate; click "Next batch" link at the bottom of each page). * 3. When you see "Nothing left to migrate", remove the require + this file. */
if (!defined('ABSPATH')) exit;
add_action('admin_init', function () { if (empty($_GET['run_migration']) || $_GET['run_migration'] !== 'spacing_global') return; if (!current_user_can('manage_options')) wp_die('Unauthorized');
global $wpdb;
$per_batch = 50; $batch = max(1, intval($_GET['batch'] ?? 1));
header('Content-Type: text/plain; charset=utf-8');
// --------------------------------------------------------------------- // Build the meta_key filter: // key starts with one of {modules_, _modules_, heros_, _heros_} // AND ends with _spacing // AND does NOT end with _section_spacing // --------------------------------------------------------------------- $prefix_patterns = array( $wpdb->esc_like('modules_') . '%' . $wpdb->esc_like('_spacing'), $wpdb->esc_like('_modules_') . '%' . $wpdb->esc_like('_spacing'), $wpdb->esc_like('heros_') . '%' . $wpdb->esc_like('_spacing'), $wpdb->esc_like('_heros_') . '%' . $wpdb->esc_like('_spacing'), ); $exclude_pattern = '%' . $wpdb->esc_like('_section_spacing'); $like_or = '(' . implode(' OR ', array_fill(0, count($prefix_patterns), 'meta_key LIKE %s')) . ')';
// --------------------------------------------------------------------- // Find the next $per_batch post_ids that still have un-migrated keys. // --------------------------------------------------------------------- $ids_sql = "SELECT DISTINCT pm.post_id FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE {$like_or} AND pm.meta_key NOT LIKE %s AND p.post_type != 'revision' AND p.post_status != 'trash' ORDER BY pm.post_id ASC LIMIT %d"; $ids_params = array_merge($prefix_patterns, array($exclude_pattern, $per_batch)); $post_ids = $wpdb->get_col($wpdb->prepare($ids_sql, $ids_params));
echo "Batch #{$batch} โ posts with un-migrated spacing in this batch: " . count($post_ids) . "\n\n";
if (empty($post_ids)) { echo "Nothing left to migrate. โ\n"; echo "Safe to remove the require line and this file now.\n"; exit; }
$total_renamed = 0; $total_deleted = 0;
foreach ($post_ids as $post_id) { $rows_sql = "SELECT meta_id, meta_key FROM {$wpdb->postmeta} WHERE post_id = %d AND {$like_or} AND meta_key NOT LIKE %s"; $rows_params = array_merge(array($post_id), $prefix_patterns, array($exclude_pattern)); $rows = $wpdb->get_results($wpdb->prepare($rows_sql, $rows_params));
if (empty($rows)) continue;
$post = get_post($post_id); $title = $post ? ($post->post_title ?: '(no title)') : '(missing)'; $ptype = $post ? $post->post_type : '?'; echo "Post #{$post_id} [{$ptype}] โ {$title}\n";
foreach ($rows as $row) { $old_key = $row->meta_key; $new_key = preg_replace('/_spacing$/', '_section_spacing', $old_key);
$exists = $wpdb->get_var($wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s LIMIT 1", $post_id, $new_key ));
if ($exists) { $wpdb->delete($wpdb->postmeta, array('meta_id' => $row->meta_id)); $total_deleted++; echo " - DELETED stale '{$old_key}' (target already exists)\n"; } else { $wpdb->update( $wpdb->postmeta, array('meta_key' => $new_key), array('meta_id' => $row->meta_id) ); $total_renamed++; echo " - RENAMED '{$old_key}' โ '{$new_key}'\n"; } }
clean_post_cache($post_id); wp_cache_delete($post_id, 'post_meta');
echo "\n"; }
echo "------------------------------------------\n"; echo "Batch #{$batch} done. Renamed: {$total_renamed} | Stale duplicates deleted: {$total_deleted}\n\n";
// --------------------------------------------------------------------- // Check if there's still work and show next batch link // --------------------------------------------------------------------- $remaining_sql = "SELECT COUNT(DISTINCT pm.post_id) FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE {$like_or} AND pm.meta_key NOT LIKE %s AND p.post_type != 'revision' AND p.post_status != 'trash'"; $remaining = (int) $wpdb->get_var($wpdb->prepare($remaining_sql, array_merge($prefix_patterns, array($exclude_pattern))));
if ($remaining > 0) { $next_url = admin_url('/?run_migration=spacing_global&batch=' . ($batch + 1)); echo "Posts still pending: {$remaining}\n"; echo "Next batch: {$next_url}\n"; } else { echo "All done. โ Safe to remove the require line and this file now.\n"; } exit;});13. Auto-refresh of pages (ACF field-key references)
Section titled โ13. Auto-refresh of pages (ACF field-key references)โRun this so all migrated ACF values are saved/resolved correctly without manually re-saving every page in the editor.
Purpose: Repairs the ACF field-key reference rows (the _fieldname meta entries) so they point to the current field key, without modifying any stored data value.
Why itโs needed: When an ACF field is renamed via ACF_Builder (e.g. spacing โ section_spacing), the reference rows in wp_postmeta keep pointing to the old field key. ACF can then no longer resolve the field on read, breaking the front end until the post is manually re-saved in the editor. This script reproduces that re-save at the data level.
How to run: Create functions/project/utilities/refresh-acf-field-key-refs.php with the script below and require it from functions/project/utilities/index.php. Admin-only (manage_options). Runs in dry-run mode by default (reports changes without writing); add &apply=1 to persist.
- Single post:
/wp-admin/?run_refresh_refs=1&test_post_id=NN[&apply=1] - Site-wide:
/wp-admin/?run_refresh_refs=1&per=100[&apply=1]โ processes in batches and prints a Next batch link until complete.
Script: refresh-acf-field-key-refs.php
<?php/** * One-shot DEV script: refresh the `_fieldname` field-key reference postmeta * entries so they point to the CURRENT field_key of each ACF field โ without * touching any actual data values. * * Why this exists: after renaming an ACF field via ACF_Builder (e.g. spacing * โ section_spacing), the reference rows in postmeta like `_modules_0_section_spacing` * still contain the OLD field_key. ACF then can't resolve the field on read * and the template breaks until you manually click Update in the editor (which * refreshes the references). * * SAFETY: * - Default mode is DRY RUN: shows what would change, writes nothing. * - Pass `&apply=1` to actually persist changes. * - Only writes to keys that START WITH UNDERSCORE (`_modules_*`, `_heros_*`, * `_section_spacing`, etc.). The actual data values are never touched. * * USAGE: * 1. Add to functions/project/utilities/index.php (temporarily): * require THEME_PATH . '/functions/project/utilities/refresh-acf-field-key-refs.php'; * * 2. TEST on one post first (dry run): * /wp-admin/?run_refresh_refs=1&test_post_id=NN * Then with apply: * /wp-admin/?run_refresh_refs=1&test_post_id=NN&apply=1 * Open the post's front URL (without clicking Update) and verify. * * 3. SITE-WIDE dry run: * /wp-admin/?run_refresh_refs=1&per=100 * Site-wide apply: * /wp-admin/?run_refresh_refs=1&per=100&apply=1 * * 4. When done, remove the require and this file. */
if (!defined('ABSPATH')) exit;
// -------------------------------------------------------------------------// Recursive walker: for each registered ACF field that applies to $post_id,// check if the reference postmeta `_<path>` value matches the current field_key.// If not, mark for update (or apply if !$dry_run).//// Returns array of changes: each item = ['ref_key' => ..., 'old' => ..., 'new' => ...]// -------------------------------------------------------------------------if (!function_exists('rb_refresh_refs_for_post')) { function rb_refresh_refs_for_post($post_id, $dry_run = true) { if (!function_exists('acf_get_field_groups')) return array(); $changes = array(); $groups = acf_get_field_groups(array('post_id' => $post_id)); foreach ($groups as $group) { $fields = acf_get_fields($group['key']); if (!$fields) continue; foreach ($fields as $field) { rb_refresh_refs_walk($field, $post_id, '', $dry_run, $changes); } } return $changes; }}
if (!function_exists('rb_refresh_refs_walk')) { function rb_refresh_refs_walk($field, $post_id, $parent_path, $dry_run, &$changes) { $name = isset($field['name']) ? $field['name'] : ''; $key = isset($field['key']) ? $field['key'] : ''; $type = isset($field['type']) ? $field['type'] : ''; if ($name === '' || $key === '') return;
$data_key = $parent_path . $name; // e.g. modules_0_section_spacing $ref_key = '_' . $data_key; // e.g. _modules_0_section_spacing
// Only update the ref if the underlying data row exists (don't add new refs // for fields the post never had data for โ those are handled by ACF on read). if (metadata_exists('post', $post_id, $data_key)) { $current_ref = get_post_meta($post_id, $ref_key, true); if ($current_ref !== $key) { $changes[] = array( 'ref_key' => $ref_key, 'old' => $current_ref, 'new' => $key, ); if (!$dry_run) { update_post_meta($post_id, $ref_key, $key); } } }
// Recurse into containers if ($type === 'flexible_content') { // The data row for flex content stores an array of layout names (in order) $rows = get_post_meta($post_id, $data_key, true); if (!is_array($rows)) return; foreach ($rows as $i => $layout_name) { if (!is_string($layout_name)) continue; foreach (($field['layouts'] ?? array()) as $layout) { if (($layout['name'] ?? '') !== $layout_name) continue; foreach (($layout['sub_fields'] ?? array()) as $sub) { rb_refresh_refs_walk($sub, $post_id, $data_key . '_' . $i . '_', $dry_run, $changes); } } } return; }
if ($type === 'repeater') { // Repeater data row stores the number of rows as an integer $count = (int) get_post_meta($post_id, $data_key, true); for ($i = 0; $i < $count; $i++) { foreach (($field['sub_fields'] ?? array()) as $sub) { rb_refresh_refs_walk($sub, $post_id, $data_key . '_' . $i . '_', $dry_run, $changes); } } return; }
if ($type === 'group') { foreach (($field['sub_fields'] ?? array()) as $sub) { rb_refresh_refs_walk($sub, $post_id, $data_key . '_', $dry_run, $changes); } return; } }}
add_action('admin_init', function () { if (empty($_GET['run_refresh_refs'])) return; if (!current_user_can('manage_options')) wp_die('Unauthorized'); if (!function_exists('acf_get_field_groups')) wp_die('ACF not active.');
header('Content-Type: text/plain; charset=utf-8'); $apply = !empty($_GET['apply']) && $_GET['apply'] === '1'; $dry_run = !$apply;
echo "MODE: " . ($dry_run ? "DRY RUN (no changes written)" : "APPLY (changes will be persisted)") . "\n"; echo str_repeat('=', 60) . "\n\n";
// ------------------------------------------------------------------------- // TEST MODE: single post // ------------------------------------------------------------------------- if (!empty($_GET['test_post_id'])) { $test_id = intval($_GET['test_post_id']); $post = get_post($test_id); if (!$post) { echo "Post #{$test_id} not found.\n"; exit; }
echo "Post #{$test_id} [{$post->post_type}] โ {$post->post_title}\n"; echo "Edit URL: " . admin_url('post.php?post=' . $test_id . '&action=edit') . "\n"; echo "Front URL: " . get_permalink($test_id) . "\n\n";
clean_post_cache($test_id); wp_cache_delete($test_id, 'post_meta');
$changes = rb_refresh_refs_for_post($test_id, $dry_run);
if (empty($changes)) { echo "โ No reference rows need updating on this post.\n"; echo " (Either nothing changed, or all references already point to current field keys.)\n"; exit; }
echo (count($changes)) . " reference row(s) " . ($dry_run ? "WOULD be updated" : "updated") . ":\n"; foreach ($changes as $c) { $old = $c['old'] !== '' ? $c['old'] : '(empty)'; echo " {$c['ref_key']}\n"; echo " old: {$old}\n"; echo " new: {$c['new']}\n"; }
if ($dry_run) { echo "\nโ Re-run with &apply=1 to persist these changes.\n"; } else { echo "\nโ Applied. Open the Front URL above in incognito (no editor click) and verify the page renders.\n"; } exit; }
// ------------------------------------------------------------------------- // SITE-WIDE mode (paginated) // ------------------------------------------------------------------------- $per_batch = max(1, min(1000, intval($_GET['per'] ?? 50))); $batch = max(1, intval($_GET['batch'] ?? 1)); $offset = isset($_GET['offset']) ? max(0, intval($_GET['offset'])) : ($batch - 1) * $per_batch;
$post_types = array('page'); $cpt_config_path = get_template_directory() . '/functions/project/config/post-types_config.php'; if (file_exists($cpt_config_path)) { $cpt_config = include $cpt_config_path; if (is_array($cpt_config)) { foreach ($cpt_config as $pt) { if (!empty($pt['post_type'])) $post_types[] = $pt['post_type']; } } } $post_types = array_values(array_unique($post_types)); $statuses = array('publish', 'draft', 'pending', 'private', 'future', 'trash');
$post_ids = get_posts(array( 'post_type' => $post_types, 'post_status' => $statuses, 'posts_per_page' => $per_batch, 'offset' => $offset, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', 'suppress_filters' => true, 'no_found_rows' => true, ));
$total_query = new WP_Query(array( 'post_type' => $post_types, 'post_status' => $statuses, 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => false, 'suppress_filters' => true, )); $total = (int) $total_query->found_posts;
echo "Total posts in scope: {$total}\n"; echo "Batch (offset {$offset}, per_batch {$per_batch}): " . count($post_ids) . " posts\n\n";
if (empty($post_ids)) { echo "Nothing left. โ All done.\n"; exit; }
$posts_with_changes = 0; $total_ref_changes = 0;
foreach ($post_ids as $post_id) { clean_post_cache($post_id); wp_cache_delete($post_id, 'post_meta');
$changes = rb_refresh_refs_for_post($post_id, $dry_run);
if (empty($changes)) { echo "ยท #{$post_id} โ no changes needed\n"; continue; }
$posts_with_changes++; $total_ref_changes += count($changes);
$post = get_post($post_id); $ptype = $post ? $post->post_type : '?'; $title = $post ? ($post->post_title ?: '(no title)') : '(missing)'; $verb = $dry_run ? 'would update' : 'updated';
echo ($dry_run ? '?' : 'โ') . " #{$post_id} [{$ptype}] {$title} โ {$verb} " . count($changes) . " ref row(s)\n"; }
echo "\n" . str_repeat('-', 60) . "\n"; echo "Batch done. Posts with changes: {$posts_with_changes} | Total ref rows " . ($dry_run ? "to update" : "updated") . ": {$total_ref_changes}\n\n";
$processed_so_far = $offset + count($post_ids); if ($processed_so_far < $total) { $next_offset = $processed_so_far; $next_qs = "run_refresh_refs=1&per={$per_batch}&offset={$next_offset}" . ($apply ? '&apply=1' : ''); echo "Progress: {$processed_so_far} / {$total}\n"; echo "Next batch: " . admin_url('/?' . $next_qs) . "\n"; } else { if ($dry_run) { echo "Dry run complete. Re-run with &apply=1 (from offset=0) to persist changes.\n"; } else { echo "All done. โ Safe to remove the require line and this file now.\n"; } } exit;});Phase 5 โ Deploy
Section titled โPhase 5 โ Deployโ14. Before deploying
Section titled โ14. Before deployingโBefore pushing the migrated project, keep these in mind:
- Update the
hash.phppath ingulpfile.jsto its new locationfunctions/project/deploy/hash.php, so the build hash is written to the correct file after the move in step 3. - Run a full PHP deploy (deploy all PHP, not just changed files) so the new
framework/and restructuredproject/files all land on the server. - Manually move any asset files Gulp may have skipped into
functions/framework/andfunctions/project/. The deploy task can omit non-standard assets, so verify they exist on the server and copy them by hand if missing. - Delete every folder and file removed during the migration from the repo so the project doesnโt carry unused files (old
post-types.php,taxonomies.php, the pre-move deploy files, the one-shot migration scripts, etc.).
Verification checklist
Section titled โVerification checklistโfunctions/framework/is cloned and listed in.gitignorefunctions/contains onlyframework/andproject/admin-pages/hasdashboard.phpandcsv-importer.phpdeploy/holdsenqueues.php,hash.php,local-variable.phpand all paths still resolveOPENAI_API_KEYis defined and not committed as a real valuepost-types.phpandtaxonomies.phpare deleted; their content lives inconfig/config/index.phprequires every config file- Post-type, general-options, flexible-modules, and flexible-heros ACFs are generated from JSON via the templates
general-options/has one file per tab;flexible-modules/has one file per module;flexible-heros/has one file per hero- AJAX actions live in
ajax_config.php; REST endpoints inendpoint_config.php - Custom blocks are registered in
custom-blocks_config.php utilities/index.phpimports all utilities; custom ACF moved toutilities/acf/- No remaining calls to
generate_image_tagโ all replaced byrender_wp_image - (If spacing field names mismatched) spacing migration run until โNothing left to migrateโ
- ACF field-key reference refresh applied site-wide; a page renders from its front URL without re-saving in the editor
- Both one-shot scripts and their
requirelines removed after running - Full PHP deploy run so all
framework/and restructuredproject/files are on the server - Any assets Gulp skipped manually copied into
functions/framework/andfunctions/project/on the server - All folders/files removed during the migration are deleted from the repo โ no unused files left behind
Knowledge Check
Test your understanding of this section
Loading questions...