Changeset 3479298
- Timestamp:
- 03/10/2026 05:16:22 PM (3 weeks ago)
- Location:
- exporter-for-webflow
- Files:
-
- 2 deleted
- 6 edited
- 1 copied
-
tags/1.1.0 (copied) (copied from exporter-for-webflow/trunk)
-
tags/1.1.0/build.sh (deleted)
-
tags/1.1.0/exporter-for-webflow.php (modified) (8 diffs)
-
tags/1.1.0/includes/class-exporter.php (modified) (5 diffs)
-
tags/1.1.0/readme.txt (modified) (2 diffs)
-
trunk/build.sh (deleted)
-
trunk/exporter-for-webflow.php (modified) (8 diffs)
-
trunk/includes/class-exporter.php (modified) (5 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
exporter-for-webflow/tags/1.1.0/exporter-for-webflow.php
r3472753 r3479298 4 4 * Plugin URI: https://wptowfexport.com 5 5 * Description: Export your WordPress content to a CSV file formatted for Webflow import. 6 * Version: 1. 0.06 * Version: 1.1.0 7 7 * Author: Flow Guys 8 8 * Author URI: https://flowguys.com … … 97 97 98 98 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 ); 100 101 101 102 // Dev mode: adjust limit if export_limit is set … … 254 255 require_once plugin_dir_path( __FILE__ ) . 'includes/class-exporter.php'; 255 256 $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 ); 257 259 $fields = $this->get_available_fields( $post_type ); 258 260 … … 479 481 JOIN $wpdb->posts p ON p.ID = pm.post_id 480 482 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 484 485 ) ); 485 486 … … 489 490 JOIN $wpdb->posts p ON p.ID = pm.post_id 490 491 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 495 494 ) ); 496 495 … … 523 522 } 524 523 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 ) { 525 559 $acf_types = []; 526 560 if ( function_exists( 'acf_get_field_groups' ) ) { … … 535 569 } 536 570 } 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; 562 572 } 563 573 … … 747 757 $post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''; 748 758 $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 ); 750 761 751 762 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 9 9 class Exporter { 10 10 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; 17 19 } 18 20 … … 256 258 default: 257 259 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 ); 271 261 272 262 $terms = get_the_terms( $post->ID, $tax_name ); … … 281 271 } 282 272 } 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 ); 296 274 297 275 $meta_val = get_post_meta( $post->ID, $meta_key, true ); … … 305 283 } 306 284 } 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; 310 311 } 311 312 } … … 628 629 } 629 630 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 630 711 public function flatten_array_field( $data ) { 631 712 if ( ! is_array( $data ) ) { 632 713 return $data; 633 714 } 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 ); 635 800 } 636 801 -
exporter-for-webflow/tags/1.1.0/readme.txt
r3472753 r3479298 5 5 Requires at least: 5.8 6 6 Tested up to: 6.9 7 Stable tag: 1. 0.07 Stable tag: 1.1.0 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 61 61 == Changelog == 62 62 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 63 71 = 1.0.0 = 64 72 * Initial release. -
exporter-for-webflow/trunk/exporter-for-webflow.php
r3472753 r3479298 4 4 * Plugin URI: https://wptowfexport.com 5 5 * Description: Export your WordPress content to a CSV file formatted for Webflow import. 6 * Version: 1. 0.06 * Version: 1.1.0 7 7 * Author: Flow Guys 8 8 * Author URI: https://flowguys.com … … 97 97 98 98 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 ); 100 101 101 102 // Dev mode: adjust limit if export_limit is set … … 254 255 require_once plugin_dir_path( __FILE__ ) . 'includes/class-exporter.php'; 255 256 $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 ); 257 259 $fields = $this->get_available_fields( $post_type ); 258 260 … … 479 481 JOIN $wpdb->posts p ON p.ID = pm.post_id 480 482 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 484 485 ) ); 485 486 … … 489 490 JOIN $wpdb->posts p ON p.ID = pm.post_id 490 491 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 495 494 ) ); 496 495 … … 523 522 } 524 523 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 ) { 525 559 $acf_types = []; 526 560 if ( function_exists( 'acf_get_field_groups' ) ) { … … 535 569 } 536 570 } 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; 562 572 } 563 573 … … 747 757 $post_type = isset( $_POST['post_type'] ) ? sanitize_key( wp_unslash( $_POST['post_type'] ) ) : ''; 748 758 $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 ); 750 761 751 762 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 9 9 class Exporter { 10 10 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; 17 19 } 18 20 … … 256 258 default: 257 259 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 ); 271 261 272 262 $terms = get_the_terms( $post->ID, $tax_name ); … … 281 271 } 282 272 } 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 ); 296 274 297 275 $meta_val = get_post_meta( $post->ID, $meta_key, true ); … … 305 283 } 306 284 } 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; 310 311 } 311 312 } … … 628 629 } 629 630 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 630 711 public function flatten_array_field( $data ) { 631 712 if ( ! is_array( $data ) ) { 632 713 return $data; 633 714 } 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 ); 635 800 } 636 801 -
exporter-for-webflow/trunk/readme.txt
r3472753 r3479298 5 5 Requires at least: 5.8 6 6 Tested up to: 6.9 7 Stable tag: 1. 0.07 Stable tag: 1.1.0 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 61 61 == Changelog == 62 62 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 63 71 = 1.0.0 = 64 72 * Initial release.
Note: See TracChangeset
for help on using the changeset viewer.