Plugin Directory

Changeset 3479298


Ignore:
Timestamp:
03/10/2026 05:16:22 PM (3 weeks ago)
Author:
flowguys
Message:

Update to version 1.1.0 from GitHub

Location:
exporter-for-webflow
Files:
2 deleted
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • exporter-for-webflow/tags/1.1.0/exporter-for-webflow.php

    r3472753 r3479298  
    44 * Plugin URI: https://wptowfexport.com
    55 * Description: Export your WordPress content to a CSV file formatted for Webflow import.
    6  * Version: 1.0.0
     6 * Version: 1.1.0
    77 * Author: Flow Guys
    88 * Author URI: https://flowguys.com
     
    9797
    9898        require_once plugin_dir_path( __FILE__ ) . 'includes/class-exporter.php';
    99         $exporter = new \ExportForWf\Exporter( $sanitize_options );
     99        $acf_types = $this->get_acf_types( $post_type );
     100        $exporter = new \ExportForWf\Exporter( $sanitize_options, $acf_types );
    100101
    101102        // Dev mode: adjust limit if export_limit is set
     
    254255        require_once plugin_dir_path( __FILE__ ) . 'includes/class-exporter.php';
    255256        $sanitize_options = get_option( 'export_for_wf_sanitize_options', [] );
    256         $exporter = new \ExportForWf\Exporter( $sanitize_options );
     257        $acf_types = $this->get_acf_types( $post_type );
     258        $exporter = new \ExportForWf\Exporter( $sanitize_options, $acf_types );
    257259        $fields   = $this->get_available_fields( $post_type );
    258260       
     
    479481             JOIN $wpdb->posts p ON p.ID = pm.post_id
    480482             WHERE p.post_type = %s
    481              AND pm.meta_key NOT LIKE %s",
    482             $post_type,
    483             $wpdb->esc_like( '_' ) . '%'
     483             AND LEFT(pm.meta_key, 1) != '_'",
     484            $post_type
    484485        ) );
    485486
     
    489490             JOIN $wpdb->posts p ON p.ID = pm.post_id
    490491             WHERE p.post_type = %s
    491              AND (meta_key LIKE %s OR meta_key LIKE %s)",
    492             $post_type,
    493             $wpdb->esc_like( '_yoast' ) . '%',
    494             $wpdb->esc_like( '_rank_math' ) . '%'
     492             AND (LEFT(pm.meta_key, 12) = '_yoast_wpseo' OR LEFT(pm.meta_key, 11) = '_rank_math_')",
     493            $post_type
    495494        ) );
    496495
     
    523522        }
    524523
     524        $acf_types = $this->get_acf_types( $post_type );
     525
     526        foreach ( $meta_keys as $key ) {
     527            if ( empty( $key ) ) continue;
     528            $label = str_replace( [ '_', '-' ], ' ', $key );
     529            $label = ucwords( trim( $label ) );
     530            if ( empty( $label ) ) continue;
     531            if ( strpos( $key, '_yoast_wpseo_' ) === 0 ) {
     532                $label = 'Yoast: ' . ucwords( str_replace( '_yoast_wpseo_', '', $key ) );
     533            } elseif ( strpos( $key, '_rank_math_' ) === 0 ) {
     534                $label = 'RankMath: ' . ucwords( str_replace( '_rank_math_', '', $key ) );
     535            }
     536            if ( isset( $acf_types[ $key ] ) ) {
     537                $type = $acf_types[ $key ];
     538                if ( in_array( $type, [ 'relationship', 'post_object', 'page_link' ] ) ) {
     539                    $fields[] = [ 'id' => $key . '_names', 'label' => 'ACF: ' . $label . ' (Names)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-names', 'isAdvanced' => true ];
     540                    $fields[] = [ 'id' => $key . '_slugs', 'label' => 'ACF: ' . $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-slugs' ];
     541                    $fields[] = [ 'id' => $key . '_ids', 'label' => 'ACF: ' . $label . ' (IDs)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-ids', 'isAdvanced' => true ];
     542                    continue;
     543                }
     544                $label = 'ACF: ' . $label;
     545            }
     546            $fields[] = [ 'id' => $key, 'label' => $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) ];
     547        }
     548
     549        return $fields;
     550    }
     551
     552    /**
     553     * Builds a map of ACF field names to their types for a given post type.
     554     *
     555     * @param string $post_type The post type to query ACF field groups for.
     556     * @return array Associative array of field_name => field_type.
     557     */
     558    public function get_acf_types( $post_type ) {
    525559        $acf_types = [];
    526560        if ( function_exists( 'acf_get_field_groups' ) ) {
     
    535569            }
    536570        }
    537 
    538         foreach ( $meta_keys as $key ) {
    539             if ( empty( $key ) ) continue;
    540             $label = str_replace( [ '_', '-' ], ' ', $key );
    541             $label = ucwords( trim( $label ) );
    542             if ( empty( $label ) ) continue;
    543             if ( strpos( $key, '_yoast_wpseo_' ) === 0 ) {
    544                 $label = 'Yoast: ' . ucwords( str_replace( '_yoast_wpseo_', '', $key ) );
    545             } elseif ( strpos( $key, '_rank_math_' ) === 0 ) {
    546                 $label = 'RankMath: ' . ucwords( str_replace( '_rank_math_', '', $key ) );
    547             }
    548             if ( isset( $acf_types[ $key ] ) ) {
    549                 $type = $acf_types[ $key ];
    550                 if ( in_array( $type, [ 'relationship', 'post_object', 'page_link' ] ) ) {
    551                     $fields[] = [ 'id' => $key . '_names', 'label' => 'ACF: ' . $label . ' (Names)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-names', 'isAdvanced' => true ];
    552                     $fields[] = [ 'id' => $key . '_slugs', 'label' => 'ACF: ' . $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-slugs' ];
    553                     $fields[] = [ 'id' => $key . '_ids', 'label' => 'ACF: ' . $label . ' (IDs)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-ids', 'isAdvanced' => true ];
    554                     continue;
    555                 }
    556                 $label = 'ACF: ' . $label;
    557             }
    558             $fields[] = [ 'id' => $key, 'label' => $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) ];
    559         }
    560 
    561         return $fields;
     571        return $acf_types;
    562572    }
    563573
     
    747757            $post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : '';
    748758            $sanitize_options = get_option( 'export_for_wf_sanitize_options', [] );
    749             $exporter = new Exporter( $sanitize_options );
     759            $acf_types = $this->get_acf_types( $post_type );
     760            $exporter = new Exporter( $sanitize_options, $acf_types );
    750761
    751762            if ( isset( $_POST['wf_export_action'] ) && 'redirects' === sanitize_key( wp_unslash( $_POST['wf_export_action'] ) ) ) {
  • exporter-for-webflow/tags/1.1.0/includes/class-exporter.php

    r3472753 r3479298  
    99class Exporter {
    1010
    11     private $slugs   = [];
    12     private $notices = [];
    13     private $options = [];
    14 
    15     public function __construct( $options = [] ) {
    16         $this->options = $options;
     11    private $slugs     = [];
     12    private $notices   = [];
     13    private $options   = [];
     14    private $acf_types = [];
     15
     16    public function __construct( $options = [], $acf_types = [] ) {
     17        $this->options   = $options;
     18        $this->acf_types = $acf_types;
    1719    }
    1820
     
    256258                default:
    257259                    if ( strpos( $wp_id, 'tax_' ) === 0 ) {
    258                         $tax_name = '';
    259                         $format   = 'slug';
    260 
    261                         if ( substr( $wp_id, -6 ) === '_slugs' ) {
    262                             $tax_name = substr( $wp_id, 4, -6 );
    263                             $format   = 'slug';
    264                         } elseif ( substr( $wp_id, -6 ) === '_names' ) {
    265                             $tax_name = substr( $wp_id, 4, -6 );
    266                             $format   = 'name';
    267                         } elseif ( substr( $wp_id, -4 ) === '_ids' ) {
    268                             $tax_name = substr( $wp_id, 4, -4 );
    269                             $format   = 'id';
    270                         }
     260                        list( $tax_name, $format ) = $this->parse_format_suffix( $wp_id, 4 );
    271261                       
    272262                        $terms = get_the_terms( $post->ID, $tax_name );
     
    281271                        }
    282272                    } elseif ( substr( $wp_id, -6 ) === '_names' || substr( $wp_id, -6 ) === '_slugs' || substr( $wp_id, -4 ) === '_ids' ) {
    283                         $format   = 'slug';
    284                         $meta_key = '';
    285 
    286                         if ( substr( $wp_id, -6 ) === '_slugs' ) {
    287                             $meta_key = substr( $wp_id, 0, -6 );
    288                             $format   = 'slug';
    289                         } elseif ( substr( $wp_id, -6 ) === '_names' ) {
    290                             $meta_key = substr( $wp_id, 0, -6 );
    291                             $format   = 'name';
    292                         } elseif ( substr( $wp_id, -4 ) === '_ids' ) {
    293                             $meta_key = substr( $wp_id, 0, -4 );
    294                             $format   = 'id';
    295                         }
     273                        list( $meta_key, $format ) = $this->parse_format_suffix( $wp_id );
    296274
    297275                        $meta_val = get_post_meta( $post->ID, $meta_key, true );
     
    305283                        }
    306284                    } else {
    307                         $value = get_post_meta( $post->ID, $wp_id, true );
    308                         if ( is_array( $value ) ) {
    309                             $value = $this->flatten_array_field( $value );
     285                        $meta_value = get_post_meta( $post->ID, $wp_id, true );
     286
     287                        // ACF type-aware resolution: convert raw IDs and structured data to Webflow-compatible values.
     288                        if ( isset( $this->acf_types[ $wp_id ] ) ) {
     289                            $value = $this->resolve_acf_value( $meta_value, $this->acf_types[ $wp_id ], $prefix, $sanitize_options );
     290                        } elseif ( is_array( $meta_value ) ) {
     291                            // Try ACF reference key lookup: _fieldname stores the ACF field key.
     292                            $acf_type = $this->detect_acf_type_from_reference( $post->ID, $wp_id );
     293                            if ( $acf_type ) {
     294                                $value = $this->resolve_acf_value( $meta_value, $acf_type, $prefix, $sanitize_options );
     295                            } elseif ( $this->looks_like_attachment_ids( $meta_value ) ) {
     296                                // Heuristic: array of numeric IDs that all resolve to attachments → URLs.
     297                                $value = $this->resolve_attachment_ids_to_urls( $meta_value );
     298                            } else {
     299                                $value = $this->flatten_array_field( $meta_value );
     300                            }
     301                        } elseif ( is_numeric( $meta_value ) ) {
     302                            // Single numeric value without ACF type — check reference key.
     303                            $acf_type = $this->detect_acf_type_from_reference( $post->ID, $wp_id );
     304                            if ( $acf_type ) {
     305                                $value = $this->resolve_acf_value( $meta_value, $acf_type, $prefix, $sanitize_options );
     306                            } else {
     307                                $value = $meta_value;
     308                            }
     309                        } else {
     310                            $value = $meta_value;
    310311                        }
    311312                    }
     
    628629    }
    629630
     631    /**
     632     * Attempts to detect an ACF field type by reading the reference meta key.
     633     *
     634     * ACF stores a companion meta entry `_fieldname` containing the field key
     635     * (e.g. `field_5f6a08d7a517d`). When ACF is active we can look up the
     636     * field definition from this key even if the field group wasn't loaded
     637     * into `$acf_types` (e.g. imported data, dynamically registered groups).
     638     *
     639     * @param int    $post_id  The post ID to read meta from.
     640     * @param string $meta_key The meta key to check (e.g. 'hover_images').
     641     * @return string|false    The ACF field type, or false if not detectable.
     642     */
     643    public function detect_acf_type_from_reference( $post_id, $meta_key ) {
     644        if ( ! function_exists( 'acf_get_field' ) ) {
     645            return false;
     646        }
     647
     648        $field_key = get_post_meta( $post_id, '_' . $meta_key, true );
     649        if ( empty( $field_key ) || strpos( $field_key, 'field_' ) !== 0 ) {
     650            return false;
     651        }
     652
     653        $field = acf_get_field( $field_key );
     654        if ( $field && ! empty( $field['type'] ) ) {
     655            return $field['type'];
     656        }
     657
     658        return false;
     659    }
     660
     661    /**
     662     * Checks whether an array of values looks like attachment IDs.
     663     *
     664     * Returns true only if every element is numeric AND resolves to an
     665     * attachment post. This is a conservative heuristic — a false positive
     666     * would require every number to coincidentally be an existing attachment.
     667     *
     668     * @param array $values Array of potential IDs.
     669     * @return bool
     670     */
     671    public function looks_like_attachment_ids( $values ) {
     672        if ( empty( $values ) || ! is_array( $values ) ) {
     673            return false;
     674        }
     675
     676        foreach ( $values as $val ) {
     677            if ( ! is_numeric( $val ) ) {
     678                return false;
     679            }
     680            if ( get_post_type( (int) $val ) !== 'attachment' ) {
     681                return false;
     682            }
     683        }
     684
     685        return true;
     686    }
     687
     688    /**
     689     * Parses a format suffix (_slugs, _names, _ids) from a field ID.
     690     *
     691     * Extracts the base key and output format from field IDs like
     692     * 'tax_category_names' or 'related_posts_slugs'.
     693     *
     694     * @param string $wp_id        The full field ID.
     695     * @param int    $prefix_len   Characters to strip from the start (e.g. 4 for 'tax_').
     696     * @return array [ base_key, format ] where format is 'slug', 'name', or 'id'.
     697     */
     698    private function parse_format_suffix( $wp_id, $prefix_len = 0 ) {
     699        if ( substr( $wp_id, -6 ) === '_slugs' ) {
     700            return [ substr( $wp_id, $prefix_len, -6 ), 'slug' ];
     701        }
     702        if ( substr( $wp_id, -6 ) === '_names' ) {
     703            return [ substr( $wp_id, $prefix_len, -6 ), 'name' ];
     704        }
     705        if ( substr( $wp_id, -4 ) === '_ids' ) {
     706            return [ substr( $wp_id, $prefix_len, -4 ), 'id' ];
     707        }
     708        return [ substr( $wp_id, $prefix_len ), 'slug' ];
     709    }
     710
    630711    public function flatten_array_field( $data ) {
    631712        if ( ! is_array( $data ) ) {
    632713            return $data;
    633714        }
    634         return implode( ';', array_map( 'sanitize_title', $data ) );
     715        return implode( '; ', array_map( 'sanitize_title', $data ) );
     716    }
     717
     718    /**
     719     * Resolves an ACF meta value based on its field type.
     720     *
     721     * ACF stores image/file/gallery fields as attachment IDs, link fields as
     722     * arrays, etc. This converts them to Webflow-compatible plain values.
     723     *
     724     * @param mixed  $meta_value       The raw meta value from get_post_meta().
     725     * @param string $acf_type         The ACF field type (e.g. 'image', 'gallery').
     726     * @param string $prefix           URL prefix for rich text link rewriting.
     727     * @param array  $sanitize_options Options passed to sanitize_rich_text().
     728     * @return string
     729     */
     730    public function resolve_acf_value( $meta_value, $acf_type, $prefix = '/blog/', $sanitize_options = [] ) {
     731        switch ( $acf_type ) {
     732            case 'image':
     733            case 'file':
     734                if ( empty( $meta_value ) || ! is_numeric( $meta_value ) ) {
     735                    return '';
     736                }
     737                $url = wp_get_attachment_url( (int) $meta_value );
     738                return $url ? $this->format_url_for_csv( $url ) : '';
     739
     740            case 'gallery':
     741                return $this->resolve_attachment_ids_to_urls( $meta_value );
     742
     743            case 'link':
     744                if ( is_array( $meta_value ) && isset( $meta_value['url'] ) ) {
     745                    return $this->format_url_for_csv( $meta_value['url'] );
     746                }
     747                return is_string( $meta_value ) ? $meta_value : '';
     748
     749            case 'wysiwyg':
     750                if ( empty( $meta_value ) ) {
     751                    return '';
     752                }
     753                return $this->sanitize_rich_text( $meta_value, $prefix, $sanitize_options );
     754
     755            case 'true_false':
     756                return $meta_value ? 'true' : 'false';
     757
     758            case 'select':
     759            case 'checkbox':
     760                if ( is_array( $meta_value ) ) {
     761                    return implode( '; ', $meta_value );
     762                }
     763                return (string) $meta_value;
     764
     765            default:
     766                if ( is_array( $meta_value ) ) {
     767                    return $this->flatten_array_field( $meta_value );
     768                }
     769                return (string) $meta_value;
     770        }
     771    }
     772
     773    /**
     774     * Resolves attachment IDs to their URLs.
     775     *
     776     * @param mixed $ids Single ID or array of IDs.
     777     * @return string Semi-colon separated URLs.
     778     */
     779    public function resolve_attachment_ids_to_urls( $ids ) {
     780        if ( empty( $ids ) ) {
     781            return '';
     782        }
     783
     784        if ( ! is_array( $ids ) ) {
     785            $ids = [ $ids ];
     786        }
     787
     788        $urls = [];
     789        foreach ( $ids as $id ) {
     790            if ( ! is_numeric( $id ) ) {
     791                continue;
     792            }
     793            $url = wp_get_attachment_url( (int) $id );
     794            if ( $url ) {
     795                $urls[] = $this->format_url_for_csv( $url );
     796            }
     797        }
     798
     799        return implode( '; ', $urls );
    635800    }
    636801
  • exporter-for-webflow/tags/1.1.0/readme.txt

    r3472753 r3479298  
    55Requires at least: 5.8
    66Tested up to: 6.9
    7 Stable tag: 1.0.0
     7Stable tag: 1.1.0
    88Requires PHP: 7.4
    99License: GPLv2 or later
     
    6161== Changelog ==
    6262
     63= 1.1.0 =
     64* ACF field support: image, gallery, and file fields now export as full URLs instead of attachment IDs.
     65* ACF field support: true/false fields export as "true"/"false" instead of "1"/"0".
     66* ACF field support: select, checkbox, link, and WYSIWYG fields now export Webflow-compatible values.
     67* Smart detection of image galleries stored as serialized attachment IDs.
     68* Fixed meta field discovery on WordPress 6.9+ (fields were not appearing in the dropdown).
     69* Fixed inconsistent multi-value delimiter for Webflow CSV import compatibility.
     70
    6371= 1.0.0 =
    6472* Initial release.
  • exporter-for-webflow/trunk/exporter-for-webflow.php

    r3472753 r3479298  
    44 * Plugin URI: https://wptowfexport.com
    55 * Description: Export your WordPress content to a CSV file formatted for Webflow import.
    6  * Version: 1.0.0
     6 * Version: 1.1.0
    77 * Author: Flow Guys
    88 * Author URI: https://flowguys.com
     
    9797
    9898        require_once plugin_dir_path( __FILE__ ) . 'includes/class-exporter.php';
    99         $exporter = new \ExportForWf\Exporter( $sanitize_options );
     99        $acf_types = $this->get_acf_types( $post_type );
     100        $exporter = new \ExportForWf\Exporter( $sanitize_options, $acf_types );
    100101
    101102        // Dev mode: adjust limit if export_limit is set
     
    254255        require_once plugin_dir_path( __FILE__ ) . 'includes/class-exporter.php';
    255256        $sanitize_options = get_option( 'export_for_wf_sanitize_options', [] );
    256         $exporter = new \ExportForWf\Exporter( $sanitize_options );
     257        $acf_types = $this->get_acf_types( $post_type );
     258        $exporter = new \ExportForWf\Exporter( $sanitize_options, $acf_types );
    257259        $fields   = $this->get_available_fields( $post_type );
    258260       
     
    479481             JOIN $wpdb->posts p ON p.ID = pm.post_id
    480482             WHERE p.post_type = %s
    481              AND pm.meta_key NOT LIKE %s",
    482             $post_type,
    483             $wpdb->esc_like( '_' ) . '%'
     483             AND LEFT(pm.meta_key, 1) != '_'",
     484            $post_type
    484485        ) );
    485486
     
    489490             JOIN $wpdb->posts p ON p.ID = pm.post_id
    490491             WHERE p.post_type = %s
    491              AND (meta_key LIKE %s OR meta_key LIKE %s)",
    492             $post_type,
    493             $wpdb->esc_like( '_yoast' ) . '%',
    494             $wpdb->esc_like( '_rank_math' ) . '%'
     492             AND (LEFT(pm.meta_key, 12) = '_yoast_wpseo' OR LEFT(pm.meta_key, 11) = '_rank_math_')",
     493            $post_type
    495494        ) );
    496495
     
    523522        }
    524523
     524        $acf_types = $this->get_acf_types( $post_type );
     525
     526        foreach ( $meta_keys as $key ) {
     527            if ( empty( $key ) ) continue;
     528            $label = str_replace( [ '_', '-' ], ' ', $key );
     529            $label = ucwords( trim( $label ) );
     530            if ( empty( $label ) ) continue;
     531            if ( strpos( $key, '_yoast_wpseo_' ) === 0 ) {
     532                $label = 'Yoast: ' . ucwords( str_replace( '_yoast_wpseo_', '', $key ) );
     533            } elseif ( strpos( $key, '_rank_math_' ) === 0 ) {
     534                $label = 'RankMath: ' . ucwords( str_replace( '_rank_math_', '', $key ) );
     535            }
     536            if ( isset( $acf_types[ $key ] ) ) {
     537                $type = $acf_types[ $key ];
     538                if ( in_array( $type, [ 'relationship', 'post_object', 'page_link' ] ) ) {
     539                    $fields[] = [ 'id' => $key . '_names', 'label' => 'ACF: ' . $label . ' (Names)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-names', 'isAdvanced' => true ];
     540                    $fields[] = [ 'id' => $key . '_slugs', 'label' => 'ACF: ' . $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-slugs' ];
     541                    $fields[] = [ 'id' => $key . '_ids', 'label' => 'ACF: ' . $label . ' (IDs)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-ids', 'isAdvanced' => true ];
     542                    continue;
     543                }
     544                $label = 'ACF: ' . $label;
     545            }
     546            $fields[] = [ 'id' => $key, 'label' => $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) ];
     547        }
     548
     549        return $fields;
     550    }
     551
     552    /**
     553     * Builds a map of ACF field names to their types for a given post type.
     554     *
     555     * @param string $post_type The post type to query ACF field groups for.
     556     * @return array Associative array of field_name => field_type.
     557     */
     558    public function get_acf_types( $post_type ) {
    525559        $acf_types = [];
    526560        if ( function_exists( 'acf_get_field_groups' ) ) {
     
    535569            }
    536570        }
    537 
    538         foreach ( $meta_keys as $key ) {
    539             if ( empty( $key ) ) continue;
    540             $label = str_replace( [ '_', '-' ], ' ', $key );
    541             $label = ucwords( trim( $label ) );
    542             if ( empty( $label ) ) continue;
    543             if ( strpos( $key, '_yoast_wpseo_' ) === 0 ) {
    544                 $label = 'Yoast: ' . ucwords( str_replace( '_yoast_wpseo_', '', $key ) );
    545             } elseif ( strpos( $key, '_rank_math_' ) === 0 ) {
    546                 $label = 'RankMath: ' . ucwords( str_replace( '_rank_math_', '', $key ) );
    547             }
    548             if ( isset( $acf_types[ $key ] ) ) {
    549                 $type = $acf_types[ $key ];
    550                 if ( in_array( $type, [ 'relationship', 'post_object', 'page_link' ] ) ) {
    551                     $fields[] = [ 'id' => $key . '_names', 'label' => 'ACF: ' . $label . ' (Names)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-names', 'isAdvanced' => true ];
    552                     $fields[] = [ 'id' => $key . '_slugs', 'label' => 'ACF: ' . $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-slugs' ];
    553                     $fields[] = [ 'id' => $key . '_ids', 'label' => 'ACF: ' . $label . ' (IDs)', 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) . '-ids', 'isAdvanced' => true ];
    554                     continue;
    555                 }
    556                 $label = 'ACF: ' . $label;
    557             }
    558             $fields[] = [ 'id' => $key, 'label' => $label, 'type' => 'meta', 'defaultHeader' => sanitize_title( $label ) ];
    559         }
    560 
    561         return $fields;
     571        return $acf_types;
    562572    }
    563573
     
    747757            $post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : '';
    748758            $sanitize_options = get_option( 'export_for_wf_sanitize_options', [] );
    749             $exporter = new Exporter( $sanitize_options );
     759            $acf_types = $this->get_acf_types( $post_type );
     760            $exporter = new Exporter( $sanitize_options, $acf_types );
    750761
    751762            if ( isset( $_POST['wf_export_action'] ) && 'redirects' === sanitize_key( wp_unslash( $_POST['wf_export_action'] ) ) ) {
  • exporter-for-webflow/trunk/includes/class-exporter.php

    r3472753 r3479298  
    99class Exporter {
    1010
    11     private $slugs   = [];
    12     private $notices = [];
    13     private $options = [];
    14 
    15     public function __construct( $options = [] ) {
    16         $this->options = $options;
     11    private $slugs     = [];
     12    private $notices   = [];
     13    private $options   = [];
     14    private $acf_types = [];
     15
     16    public function __construct( $options = [], $acf_types = [] ) {
     17        $this->options   = $options;
     18        $this->acf_types = $acf_types;
    1719    }
    1820
     
    256258                default:
    257259                    if ( strpos( $wp_id, 'tax_' ) === 0 ) {
    258                         $tax_name = '';
    259                         $format   = 'slug';
    260 
    261                         if ( substr( $wp_id, -6 ) === '_slugs' ) {
    262                             $tax_name = substr( $wp_id, 4, -6 );
    263                             $format   = 'slug';
    264                         } elseif ( substr( $wp_id, -6 ) === '_names' ) {
    265                             $tax_name = substr( $wp_id, 4, -6 );
    266                             $format   = 'name';
    267                         } elseif ( substr( $wp_id, -4 ) === '_ids' ) {
    268                             $tax_name = substr( $wp_id, 4, -4 );
    269                             $format   = 'id';
    270                         }
     260                        list( $tax_name, $format ) = $this->parse_format_suffix( $wp_id, 4 );
    271261                       
    272262                        $terms = get_the_terms( $post->ID, $tax_name );
     
    281271                        }
    282272                    } elseif ( substr( $wp_id, -6 ) === '_names' || substr( $wp_id, -6 ) === '_slugs' || substr( $wp_id, -4 ) === '_ids' ) {
    283                         $format   = 'slug';
    284                         $meta_key = '';
    285 
    286                         if ( substr( $wp_id, -6 ) === '_slugs' ) {
    287                             $meta_key = substr( $wp_id, 0, -6 );
    288                             $format   = 'slug';
    289                         } elseif ( substr( $wp_id, -6 ) === '_names' ) {
    290                             $meta_key = substr( $wp_id, 0, -6 );
    291                             $format   = 'name';
    292                         } elseif ( substr( $wp_id, -4 ) === '_ids' ) {
    293                             $meta_key = substr( $wp_id, 0, -4 );
    294                             $format   = 'id';
    295                         }
     273                        list( $meta_key, $format ) = $this->parse_format_suffix( $wp_id );
    296274
    297275                        $meta_val = get_post_meta( $post->ID, $meta_key, true );
     
    305283                        }
    306284                    } else {
    307                         $value = get_post_meta( $post->ID, $wp_id, true );
    308                         if ( is_array( $value ) ) {
    309                             $value = $this->flatten_array_field( $value );
     285                        $meta_value = get_post_meta( $post->ID, $wp_id, true );
     286
     287                        // ACF type-aware resolution: convert raw IDs and structured data to Webflow-compatible values.
     288                        if ( isset( $this->acf_types[ $wp_id ] ) ) {
     289                            $value = $this->resolve_acf_value( $meta_value, $this->acf_types[ $wp_id ], $prefix, $sanitize_options );
     290                        } elseif ( is_array( $meta_value ) ) {
     291                            // Try ACF reference key lookup: _fieldname stores the ACF field key.
     292                            $acf_type = $this->detect_acf_type_from_reference( $post->ID, $wp_id );
     293                            if ( $acf_type ) {
     294                                $value = $this->resolve_acf_value( $meta_value, $acf_type, $prefix, $sanitize_options );
     295                            } elseif ( $this->looks_like_attachment_ids( $meta_value ) ) {
     296                                // Heuristic: array of numeric IDs that all resolve to attachments → URLs.
     297                                $value = $this->resolve_attachment_ids_to_urls( $meta_value );
     298                            } else {
     299                                $value = $this->flatten_array_field( $meta_value );
     300                            }
     301                        } elseif ( is_numeric( $meta_value ) ) {
     302                            // Single numeric value without ACF type — check reference key.
     303                            $acf_type = $this->detect_acf_type_from_reference( $post->ID, $wp_id );
     304                            if ( $acf_type ) {
     305                                $value = $this->resolve_acf_value( $meta_value, $acf_type, $prefix, $sanitize_options );
     306                            } else {
     307                                $value = $meta_value;
     308                            }
     309                        } else {
     310                            $value = $meta_value;
    310311                        }
    311312                    }
     
    628629    }
    629630
     631    /**
     632     * Attempts to detect an ACF field type by reading the reference meta key.
     633     *
     634     * ACF stores a companion meta entry `_fieldname` containing the field key
     635     * (e.g. `field_5f6a08d7a517d`). When ACF is active we can look up the
     636     * field definition from this key even if the field group wasn't loaded
     637     * into `$acf_types` (e.g. imported data, dynamically registered groups).
     638     *
     639     * @param int    $post_id  The post ID to read meta from.
     640     * @param string $meta_key The meta key to check (e.g. 'hover_images').
     641     * @return string|false    The ACF field type, or false if not detectable.
     642     */
     643    public function detect_acf_type_from_reference( $post_id, $meta_key ) {
     644        if ( ! function_exists( 'acf_get_field' ) ) {
     645            return false;
     646        }
     647
     648        $field_key = get_post_meta( $post_id, '_' . $meta_key, true );
     649        if ( empty( $field_key ) || strpos( $field_key, 'field_' ) !== 0 ) {
     650            return false;
     651        }
     652
     653        $field = acf_get_field( $field_key );
     654        if ( $field && ! empty( $field['type'] ) ) {
     655            return $field['type'];
     656        }
     657
     658        return false;
     659    }
     660
     661    /**
     662     * Checks whether an array of values looks like attachment IDs.
     663     *
     664     * Returns true only if every element is numeric AND resolves to an
     665     * attachment post. This is a conservative heuristic — a false positive
     666     * would require every number to coincidentally be an existing attachment.
     667     *
     668     * @param array $values Array of potential IDs.
     669     * @return bool
     670     */
     671    public function looks_like_attachment_ids( $values ) {
     672        if ( empty( $values ) || ! is_array( $values ) ) {
     673            return false;
     674        }
     675
     676        foreach ( $values as $val ) {
     677            if ( ! is_numeric( $val ) ) {
     678                return false;
     679            }
     680            if ( get_post_type( (int) $val ) !== 'attachment' ) {
     681                return false;
     682            }
     683        }
     684
     685        return true;
     686    }
     687
     688    /**
     689     * Parses a format suffix (_slugs, _names, _ids) from a field ID.
     690     *
     691     * Extracts the base key and output format from field IDs like
     692     * 'tax_category_names' or 'related_posts_slugs'.
     693     *
     694     * @param string $wp_id        The full field ID.
     695     * @param int    $prefix_len   Characters to strip from the start (e.g. 4 for 'tax_').
     696     * @return array [ base_key, format ] where format is 'slug', 'name', or 'id'.
     697     */
     698    private function parse_format_suffix( $wp_id, $prefix_len = 0 ) {
     699        if ( substr( $wp_id, -6 ) === '_slugs' ) {
     700            return [ substr( $wp_id, $prefix_len, -6 ), 'slug' ];
     701        }
     702        if ( substr( $wp_id, -6 ) === '_names' ) {
     703            return [ substr( $wp_id, $prefix_len, -6 ), 'name' ];
     704        }
     705        if ( substr( $wp_id, -4 ) === '_ids' ) {
     706            return [ substr( $wp_id, $prefix_len, -4 ), 'id' ];
     707        }
     708        return [ substr( $wp_id, $prefix_len ), 'slug' ];
     709    }
     710
    630711    public function flatten_array_field( $data ) {
    631712        if ( ! is_array( $data ) ) {
    632713            return $data;
    633714        }
    634         return implode( ';', array_map( 'sanitize_title', $data ) );
     715        return implode( '; ', array_map( 'sanitize_title', $data ) );
     716    }
     717
     718    /**
     719     * Resolves an ACF meta value based on its field type.
     720     *
     721     * ACF stores image/file/gallery fields as attachment IDs, link fields as
     722     * arrays, etc. This converts them to Webflow-compatible plain values.
     723     *
     724     * @param mixed  $meta_value       The raw meta value from get_post_meta().
     725     * @param string $acf_type         The ACF field type (e.g. 'image', 'gallery').
     726     * @param string $prefix           URL prefix for rich text link rewriting.
     727     * @param array  $sanitize_options Options passed to sanitize_rich_text().
     728     * @return string
     729     */
     730    public function resolve_acf_value( $meta_value, $acf_type, $prefix = '/blog/', $sanitize_options = [] ) {
     731        switch ( $acf_type ) {
     732            case 'image':
     733            case 'file':
     734                if ( empty( $meta_value ) || ! is_numeric( $meta_value ) ) {
     735                    return '';
     736                }
     737                $url = wp_get_attachment_url( (int) $meta_value );
     738                return $url ? $this->format_url_for_csv( $url ) : '';
     739
     740            case 'gallery':
     741                return $this->resolve_attachment_ids_to_urls( $meta_value );
     742
     743            case 'link':
     744                if ( is_array( $meta_value ) && isset( $meta_value['url'] ) ) {
     745                    return $this->format_url_for_csv( $meta_value['url'] );
     746                }
     747                return is_string( $meta_value ) ? $meta_value : '';
     748
     749            case 'wysiwyg':
     750                if ( empty( $meta_value ) ) {
     751                    return '';
     752                }
     753                return $this->sanitize_rich_text( $meta_value, $prefix, $sanitize_options );
     754
     755            case 'true_false':
     756                return $meta_value ? 'true' : 'false';
     757
     758            case 'select':
     759            case 'checkbox':
     760                if ( is_array( $meta_value ) ) {
     761                    return implode( '; ', $meta_value );
     762                }
     763                return (string) $meta_value;
     764
     765            default:
     766                if ( is_array( $meta_value ) ) {
     767                    return $this->flatten_array_field( $meta_value );
     768                }
     769                return (string) $meta_value;
     770        }
     771    }
     772
     773    /**
     774     * Resolves attachment IDs to their URLs.
     775     *
     776     * @param mixed $ids Single ID or array of IDs.
     777     * @return string Semi-colon separated URLs.
     778     */
     779    public function resolve_attachment_ids_to_urls( $ids ) {
     780        if ( empty( $ids ) ) {
     781            return '';
     782        }
     783
     784        if ( ! is_array( $ids ) ) {
     785            $ids = [ $ids ];
     786        }
     787
     788        $urls = [];
     789        foreach ( $ids as $id ) {
     790            if ( ! is_numeric( $id ) ) {
     791                continue;
     792            }
     793            $url = wp_get_attachment_url( (int) $id );
     794            if ( $url ) {
     795                $urls[] = $this->format_url_for_csv( $url );
     796            }
     797        }
     798
     799        return implode( '; ', $urls );
    635800    }
    636801
  • exporter-for-webflow/trunk/readme.txt

    r3472753 r3479298  
    55Requires at least: 5.8
    66Tested up to: 6.9
    7 Stable tag: 1.0.0
     7Stable tag: 1.1.0
    88Requires PHP: 7.4
    99License: GPLv2 or later
     
    6161== Changelog ==
    6262
     63= 1.1.0 =
     64* ACF field support: image, gallery, and file fields now export as full URLs instead of attachment IDs.
     65* ACF field support: true/false fields export as "true"/"false" instead of "1"/"0".
     66* ACF field support: select, checkbox, link, and WYSIWYG fields now export Webflow-compatible values.
     67* Smart detection of image galleries stored as serialized attachment IDs.
     68* Fixed meta field discovery on WordPress 6.9+ (fields were not appearing in the dropdown).
     69* Fixed inconsistent multi-value delimiter for Webflow CSV import compatibility.
     70
    6371= 1.0.0 =
    6472* Initial release.
Note: See TracChangeset for help on using the changeset viewer.