' . esc_html__( 'Settings', 'acf-flexible-layout-preview' ) . '' ); } return $links; } /* ========================================================= 1. ACTIVATION / DEACTIVATION ========================================================= */ register_activation_hook( ACFLP_FILE, 'acflp_activate' ); function acflp_activate() { if ( ! acflp_is_acf_pro_active() ) { deactivate_plugins( plugin_basename( ACFLP_FILE ) ); wp_die( esc_html__( 'ACF Layout Preview Manager requires Advanced Custom Fields Pro to be installed and active before this plugin can be activated.', 'acf-flexible-layout-preview' ), esc_html__( 'ACF Pro Required', 'acf-flexible-layout-preview' ), [ 'back_link' => true ] ); } acflp_init_filesystem(); global $wp_filesystem; if ( ! $wp_filesystem->exists( acflp_upload_dir() ) ) { $wp_filesystem->mkdir( acflp_upload_dir() ); } // Protect directory listing $htaccess = acflp_upload_dir() . '.htaccess'; if ( ! $wp_filesystem->exists( $htaccess ) ) { $wp_filesystem->put_contents( $htaccess, "Options -Indexes\n", FS_CHMOD_FILE ); } } register_deactivation_hook( ACFLP_FILE, 'acflp_deactivate' ); function acflp_deactivate() { // Clear cached layouts delete_transient( 'acflp_all_layouts' ); } function acflp_is_acf_pro_active(): bool { return defined( 'ACF_PRO' ) || class_exists( 'acf_pro' ) || function_exists( 'acf_pro_get_license' ) || ( function_exists( 'acf_get_setting' ) && acf_get_setting( 'pro' ) ); } add_action( 'admin_init', 'acflp_enforce_acf_pro_dependency' ); function acflp_enforce_acf_pro_dependency() { if ( acflp_is_acf_pro_active() || ! current_user_can( 'activate_plugins' ) ) { return; } deactivate_plugins( plugin_basename( ACFLP_FILE ) ); add_action( 'admin_notices', 'acflp_acf_pro_missing_notice' ); } function acflp_acf_pro_missing_notice() { ?>

.

'', 'grid_columns' => 4, 'theme' => 'light', ]; } function acflp_get_settings(): array { $settings = get_option( ACFLP_OPTION, [] ); if ( ! is_array( $settings ) ) { $settings = []; } return array_merge( acflp_default_settings(), $settings ); } function acflp_sanitize_settings( array $settings ): array { $defaults = acflp_default_settings(); $grid = isset( $settings['grid_columns'] ) ? absint( $settings['grid_columns'] ) : $defaults['grid_columns']; $theme = isset( $settings['theme'] ) ? sanitize_key( $settings['theme'] ) : $defaults['theme']; return [ 'default_preview_url' => isset( $settings['default_preview_url'] ) ? esc_url_raw( $settings['default_preview_url'] ) : '', 'grid_columns' => min( 6, max( 2, $grid ) ), 'theme' => in_array( $theme, [ 'light', 'dark' ], true ) ? $theme : 'light', ]; } // Get custom layouts from database function acflp_get_custom_layouts(): array { $custom = get_option( ACFLP_CUSTOM_OPTION, [] ); return is_array( $custom ) ? $custom : []; } // Save custom layout to database function acflp_save_custom_layout( string $name, array $data ): void { $custom = acflp_get_custom_layouts(); $custom[ $name ] = [ 'name' => $name, 'label' => sanitize_text_field( $data['label'] ), 'preview_url' => esc_url_raw( $data['preview_url'] ?? '' ), 'preview_source' => 'custom', ]; update_option( ACFLP_CUSTOM_OPTION, $custom ); delete_transient( 'acflp_all_layouts' ); } // Remove custom layout function acflp_remove_custom_layout( string $name ): void { $custom = acflp_get_custom_layouts(); if ( isset( $custom[ $name ] ) ) { unset( $custom[ $name ] ); update_option( ACFLP_CUSTOM_OPTION, $custom ); delete_transient( 'acflp_all_layouts' ); } } /* ========================================================= 2. SETTINGS PAGE ========================================================= */ add_action( 'admin_menu', 'acflp_register_menu', 20 ); function acflp_register_menu() { if ( ! acflp_is_acf_pro_active() ) { return; } add_submenu_page( 'edit.php?post_type=acf-field-group', __( 'ACF Layout Previews', 'acf-flexible-layout-preview' ), __( 'Layout Previews', 'acf-flexible-layout-preview' ), 'manage_options', 'acf-layout-preview', 'acflp_settings_page' ); } add_action( 'admin_enqueue_scripts', 'acflp_admin_assets' ); function acflp_admin_assets( $hook ) { if ( ! acflp_is_acf_pro_active() ) { return; } $settings = acflp_get_settings(); // Plugin settings page only - no nonce needed for asset loading // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( 'acf-layout-preview' === $page ) { wp_enqueue_media(); wp_enqueue_style( 'acflp-admin', ACFLP_URL . 'assets/admin.css', [], ACFLP_VERSION ); wp_enqueue_script( 'acflp-admin', ACFLP_URL . 'assets/admin.js', [ 'jquery' ], ACFLP_VERSION, true ); wp_localize_script( 'acflp-admin', 'acflpData', [ 'uploadUrl' => acflp_upload_url(), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'acflp_nonce' ), 'settings' => $settings, 'maxFileSize' => ACFLP_MAX_FILE_SIZE, ] ); } // Popup + inline button: load on ALL admin pages that may have ACF wp_enqueue_style( 'acflp-popup', ACFLP_URL . 'assets/popup.css', [], ACFLP_VERSION ); wp_enqueue_script( 'acflp-popup', ACFLP_URL . 'assets/popup.js', [ 'jquery' ], ACFLP_VERSION, true ); wp_localize_script( 'acflp-popup', 'acflpData', [ 'uploadUrl' => acflp_upload_url(), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'acflp_nonce' ), 'settings' => $settings, 'maxFileSize' => ACFLP_MAX_FILE_SIZE, ] ); } /** * Redirect wp_handle_upload() to our custom folder inside wp-uploads. */ function acflp_custom_upload_dir( array $dirs ): array { $dirs['subdir'] = '/acf-layout-previews'; $dirs['path'] = $dirs['basedir'] . '/acf-layout-previews'; $dirs['url'] = $dirs['baseurl'] . '/acf-layout-previews'; return $dirs; } /* ---- AJAX: Upload image ---- */ add_action( 'wp_ajax_acflp_upload_image', 'acflp_ajax_upload_image' ); function acflp_ajax_upload_image() { check_ajax_referer( 'acflp_nonce', 'nonce' ); if ( ! current_user_can( 'edit_posts' ) ) { wp_send_json_error( 'Unauthorized' ); } if ( empty( $_FILES['file'] ) || empty( $_POST['layout_name'] ) ) { wp_send_json_error( 'Missing data' ); } $layout = sanitize_key( wp_unslash( $_POST['layout_name'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $file = $_FILES['file']; // Check for upload errors if ( $file['error'] !== UPLOAD_ERR_OK ) { wp_send_json_error( 'Upload error code: ' . (int) $file['error'] ); } // Check file size if ( $file['size'] > ACFLP_MAX_FILE_SIZE ) { wp_send_json_error( 'File too large. Maximum size is 2MB.' ); } // Use wp_handle_upload() — the WordPress-approved way to handle uploaded files require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $allowed_mimes = [ 'jpg|jpeg' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', 'gif' => 'image/gif', ]; add_filter( 'upload_dir', 'acflp_custom_upload_dir' ); $uploaded = wp_handle_upload( $file, [ 'test_form' => false, 'mimes' => $allowed_mimes, 'unique_filename_callback' => null, ] ); remove_filter( 'upload_dir', 'acflp_custom_upload_dir' ); if ( isset( $uploaded['error'] ) ) { wp_send_json_error( $uploaded['error'] ); } // Initialize WP_Filesystem for directory operations and old-file cleanup acflp_init_filesystem(); global $wp_filesystem; // Determine final filename based on layout slug $extension = strtolower( pathinfo( $uploaded['file'], PATHINFO_EXTENSION ) ); $final_name = ( $layout === '__default_preview' ) ? 'default_preview.' . $extension : $layout . '.' . $extension; $dest = acflp_upload_dir() . $final_name; // Remove any old files for this slot (any extension) $prefix = ( $layout === '__default_preview' ) ? 'default_preview.' : $layout . '.'; $old_files = $wp_filesystem->dirlist( acflp_upload_dir() ); if ( $old_files ) { foreach ( $old_files as $old_file ) { if ( strpos( $old_file['name'], $prefix ) === 0 ) { $wp_filesystem->delete( acflp_upload_dir() . $old_file['name'] ); } } } // Rename/move from WP's default upload location to our custom folder if ( $uploaded['file'] !== $dest ) { $wp_filesystem->move( $uploaded['file'], $dest, true ); } // Resize image to a reasonable max size $image_editor = wp_get_image_editor( $dest ); if ( ! is_wp_error( $image_editor ) ) { $image_editor->resize( 800, 450, false ); $image_editor->save( $dest ); } // Clear layout cache delete_transient( 'acflp_all_layouts' ); $url = acflp_upload_url() . $final_name; wp_send_json_success( [ 'url' => $url ] ); } /* ---- AJAX: Delete image ---- */ add_action( 'wp_ajax_acflp_delete_image', 'acflp_ajax_delete_image' ); function acflp_ajax_delete_image() { check_ajax_referer( 'acflp_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized' ); } $layout = isset( $_POST['layout_name'] ) ? sanitize_key( wp_unslash( $_POST['layout_name'] ) ) : ''; if ( ! $layout ) { wp_send_json_error( 'Missing layout name' ); } // Don't delete default preview files via this method if ( $layout === '__default_preview' ) { wp_send_json_error( 'Cannot delete default preview from here.' ); } acflp_init_filesystem(); global $wp_filesystem; // Delete files for this layout $old_files = $wp_filesystem->dirlist( acflp_upload_dir() ); if ( $old_files ) { foreach ( $old_files as $old_file ) { if ( strpos( $old_file['name'], $layout . '.' ) === 0 ) { $wp_filesystem->delete( acflp_upload_dir() . $old_file['name'] ); } } } // Clear cache delete_transient( 'acflp_all_layouts' ); wp_send_json_success(); } /* ---- AJAX: Get all layouts from ACF ---- */ add_action( 'wp_ajax_acflp_get_layouts', 'acflp_ajax_get_layouts' ); function acflp_ajax_get_layouts() { check_ajax_referer( 'acflp_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized' ); } $layouts = acflp_get_all_flex_layouts(); wp_send_json_success( $layouts ); } /* ---- AJAX: Save display settings ---- */ add_action( 'wp_ajax_acflp_save_settings', 'acflp_ajax_save_settings' ); function acflp_ajax_save_settings() { check_ajax_referer( 'acflp_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized' ); } // Sanitize POST data $default_preview_url = isset( $_POST['default_preview_url'] ) ? esc_url_raw( wp_unslash( $_POST['default_preview_url'] ) ) : ''; $grid_columns = isset( $_POST['grid_columns'] ) ? intval( wp_unslash( $_POST['grid_columns'] ) ) : 4; $theme = isset( $_POST['theme'] ) ? sanitize_text_field( wp_unslash( $_POST['theme'] ) ) : 'light'; $settings = acflp_sanitize_settings( [ 'default_preview_url' => $default_preview_url, 'grid_columns' => $grid_columns, 'theme' => $theme, ] ); update_option( ACFLP_OPTION, $settings ); // Clear cache when settings change delete_transient( 'acflp_all_layouts' ); wp_send_json_success( $settings ); } /* ---- AJAX: Save custom layout ---- */ add_action( 'wp_ajax_acflp_save_custom_layout', 'acflp_ajax_save_custom_layout' ); function acflp_ajax_save_custom_layout() { check_ajax_referer( 'acflp_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( 'Unauthorized' ); } $name = isset( $_POST['name'] ) ? sanitize_key( wp_unslash( $_POST['name'] ) ) : ''; $label = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : ''; if ( ! $name || ! $label ) { wp_send_json_error( 'Missing name or label' ); } acflp_save_custom_layout( $name, [ 'label' => $label ] ); wp_send_json_success(); } /* ========================================================= 3. HELPER — collect all flex-content layouts from ACF ========================================================= */ function acflp_get_all_flex_layouts(): array { // Check cache first $cached = get_transient( 'acflp_all_layouts' ); if ( false !== $cached ) { return $cached; } $result = []; if ( ! function_exists( 'acf_get_field_groups' ) ) return $result; $settings = acflp_get_settings(); foreach ( acf_get_field_groups() as $group ) { $fields = acf_get_fields( $group['key'] ); if ( ! $fields ) continue; acflp_recurse_fields( $fields, $result, $settings ); } // Add custom layouts $custom_layouts = acflp_get_custom_layouts(); foreach ( $custom_layouts as $name => $layout ) { if ( ! isset( $result[ $name ] ) ) { $preview_url = ''; $preview_source = 'none'; // Check if preview file exists using WP_Filesystem acflp_init_filesystem(); global $wp_filesystem; $old_files = $wp_filesystem->dirlist( acflp_upload_dir() ); if ( $old_files ) { foreach ( $old_files as $old_file ) { if ( strpos( $old_file['name'], $name . '.' ) === 0 ) { $preview_url = acflp_upload_url() . $old_file['name']; $preview_source = 'custom'; break; } } } if ( ! $preview_url && ! empty( $settings['default_preview_url'] ) ) { $preview_url = $settings['default_preview_url']; $preview_source = 'default'; } $result[ $name ] = [ 'name' => $name, 'label' => $layout['label'], 'preview_url' => $preview_url, 'preview_source' => $preview_source, ]; } } // Cache for 5 minutes set_transient( 'acflp_all_layouts', $result, 5 * MINUTE_IN_SECONDS ); return $result; } function acflp_recurse_fields( array $fields, array &$result, array $settings = [] ) { foreach ( $fields as $field ) { if ( $field['type'] === 'flexible_content' && ! empty( $field['layouts'] ) ) { foreach ( $field['layouts'] as $layout ) { $name = $layout['name']; $label = $layout['label']; // Find preview image using WP_Filesystem $preview_url = ''; $preview_source = 'none'; acflp_init_filesystem(); global $wp_filesystem; $old_files = $wp_filesystem->dirlist( acflp_upload_dir() ); if ( $old_files ) { foreach ( $old_files as $old_file ) { if ( strpos( $old_file['name'], $name . '.' ) === 0 ) { $preview_url = acflp_upload_url() . $old_file['name']; $preview_source = 'custom'; break; } } } if ( ! $preview_url && ! empty( $settings['default_preview_url'] ) ) { $preview_url = $settings['default_preview_url']; $preview_source = 'default'; } $result[ $name ] = [ 'name' => $name, 'label' => $label, 'preview_url' => $preview_url, 'preview_source' => $preview_source, ]; } } // Recurse into sub-fields if ( ! empty( $field['sub_fields'] ) ) { acflp_recurse_fields( $field['sub_fields'], $result, $settings ); } } } /* ========================================================= 4. SETTINGS PAGE HTML ========================================================= */ function acflp_settings_page() { if ( ! current_user_can( 'manage_options' ) ) return; $acf_active = acflp_is_acf_pro_active() && function_exists( 'acf_get_field_groups' ); $settings = acflp_get_settings(); ?>

ACF Layout Preview Manager

Manage ACF Flexible Content preview images, display settings, and editor guidance

v
Advanced Custom Fields Pro is not active. Please install and activate ACF Pro to manage layouts.

Loading layouts...

Default Preview Image

Used when a layout does not have its own preview image. Max file size: 2MB.

Default preview No default preview set

Layout Display

Control how the Choose a Layout popup looks when editors add a Flexible Content layout.

How To Use

  1. Create or edit an ACF Flexible Content field and add your layouts.
  2. Open ACF > Layout Previews and upload one preview image per layout.
  3. Use Add Custom Layout for layouts that are not discoverable yet.
  4. Set a Default Preview Image so empty layouts still look polished.
  5. When editing content, click Add Row/Add Layout and choose layouts from the visual preview modal.

Benefits

  • Editors can recognize layouts visually instead of guessing from slugs.
  • Preview images are managed from WordPress admin, with no FTP work.
  • The default preview keeps the grid and picker consistent while real screenshots are being prepared.
  • Search, drag-and-drop uploads, and custom layout registration speed up setup for large ACF builds.

Supported Image Types

  • JPEG / JPG
  • PNG
  • WebP
  • GIF
$data ) { if ( ! empty( $data['preview_url'] ) ) { $map[ $name ] = esc_url( $data['preview_url'] ); } } $settings = acflp_get_settings(); $inline = sprintf( 'window.acflpMap = %s; window.acflpSettings = %s;', wp_json_encode( $map ), wp_json_encode( $settings ) ); wp_add_inline_script( 'acflp-popup', $inline, 'before' ); }