false, 'limit' => 0, 'shipping' => null, 'sentAt' => null, ); // Not connected: feeds are off and no copy of 7BE's terms is kept or used. if ( '' === $options['key'] ) { return $off; } $cached = get_transient( 'sevenbe_feed_config' ); if ( ! $fresh && is_array( $cached ) ) { return $cached; } $response = wp_remote_get( sevenbe_signed_url( $options, '/api/apps/feeds/config' ), array( 'timeout' => 8, 'headers' => array( 'Accept' => 'application/json' ), ) ); $data = is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ? null : json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $data ) ) { set_transient( 'sevenbe_feed_config', is_array( $cached ) ? $cached : $off, HOUR_IN_SECONDS ); return is_array( $cached ) ? $cached : $off; } $config = array( 'feeds' => ! empty( $data['feeds'] ), 'limit' => isset( $data['limit'] ) ? absint( $data['limit'] ) : 0, 'shipping' => isset( $data['shipping'] ) && is_array( $data['shipping'] ) ? $data['shipping'] : null, 'sentAt' => isset( $data['sentAt'] ) && is_string( $data['sentAt'] ) ? $data['sentAt'] : null, ); set_transient( 'sevenbe_feed_config', $config, 6 * HOUR_IN_SECONDS ); return $config; } // ---------- A product as feed lines ---------- /** * The product's GTIN: WooCommerce's own field (9.2 and later), else the field a feed or barcode plugin keeps. * * @param WC_Product $product The product or variation. * @return string */ function sevenbe_feed_gtin( $product ) { $gtin = method_exists( $product, 'get_global_unique_id' ) ? (string) $product->get_global_unique_id() : ''; foreach ( array( '_gtin', '_wpm_gtin_code', 'hwp_product_gtin', '_alg_ean', '_ean', '_barcode' ) as $key ) { if ( '' !== $gtin ) { break; } $gtin = (string) $product->get_meta( $key, true ); } return preg_replace( '/[^0-9]/', '', $gtin ); } /** * The product's brand: WooCommerce Brands, or a "Brand" attribute. * * @param WC_Product $product The parent product. * @return string */ function sevenbe_feed_brand( $product ) { if ( taxonomy_exists( 'product_brand' ) ) { $brands = get_the_terms( $product->get_id(), 'product_brand' ); if ( is_array( $brands ) && $brands ) { return $brands[0]->name; } } foreach ( array( 'pa_brand', 'brand' ) as $attribute ) { $value = $product->get_attribute( $attribute ); if ( '' !== $value ) { return trim( explode( ',', $value )[0] ); } } return ''; } /** * The product's category with its parents, "Bedding > Sheets". * * @param WC_Product $product The parent product. * @return string */ function sevenbe_feed_category( $product ) { $terms = get_the_terms( $product->get_id(), 'product_cat' ); if ( ! is_array( $terms ) ) { return ''; } $terms = array_values( array_filter( $terms, fn( $t ) => 'uncategorized' !== $t->slug ) ); if ( ! $terms ) { return ''; } $names = array_reverse( array_map( fn( $id ) => get_term( $id )->name, get_ancestors( $terms[0]->term_id, 'product_cat', 'taxonomy' ) ) ); return implode( ' > ', array_merge( $names, array( $terms[0]->name ) ) ); } /** * @param WC_Product $product The product or variation. * @param WC_Product|null $parent_product The variation's parent. * @return string[] Image addresses, the main one first. */ function sevenbe_feed_images( $product, $parent_product = null ) { $ids = array( $product->get_image_id() ); if ( $parent_product ) { $ids[] = $parent_product->get_image_id(); $ids = array_merge( $ids, $parent_product->get_gallery_image_ids() ); } else { $ids = array_merge( $ids, $product->get_gallery_image_ids() ); } $ids = array_slice( array_unique( array_filter( array_map( 'absint', $ids ) ) ), 0, 11 ); return array_values( array_filter( array_map( 'wp_get_attachment_url', $ids ) ) ); } /** * @param WC_Product $product The product or variation. * @return string in_stock, out_of_stock or backorder. */ function sevenbe_feed_availability( $product ) { $status = $product->get_stock_status(); return 'outofstock' === $status ? 'out_of_stock' : ( 'onbackorder' === $status ? 'backorder' : 'in_stock' ); } /** * @param string $html Description HTML. * @return string Plain text. */ function sevenbe_feed_text( $html ) { return trim( preg_replace( '/\s+/', ' ', html_entity_decode( wp_strip_all_tags( strip_shortcodes( (string) $html ) ), ENT_QUOTES, 'UTF-8' ) ) ); } /** * The key a product's lines are grouped under on 7BE: its SKU, else its id. * * @param WC_Product $product The parent product. * @return string */ function sevenbe_feed_key( $product ) { return '' !== (string) $product->get_sku() ? (string) $product->get_sku() : (string) $product->get_id(); } /** * One feed line. * * @param WC_Product $product The product or variation. * @param WC_Product|null $parent_product The variation's parent. * @return array|null Null for a product without a price. */ function sevenbe_feed_line( $product, $parent_product = null ) { $regular = $product->get_regular_price(); $sale = $product->get_sale_price(); if ( '' === $regular && '' === $product->get_price() ) { return null; } $source = $parent_product ? $parent_product : $product; $text = $product->get_description() ? $product->get_description() : ( $source->get_description() ? $source->get_description() : $source->get_short_description() ); // A variant without its own SKU shows its parent's; 'edit' gives the variant's own, so each line keeps a distinct id. $sku = (string) $product->get_sku( 'edit' ); $line = array( 'id' => '' !== $sku ? $sku : ( $parent_product ? sevenbe_feed_key( $parent_product ) . '-' . $product->get_id() : (string) $product->get_id() ), 'title' => wp_strip_all_tags( $product->get_name() ), 'description' => wp_html_excerpt( sevenbe_feed_text( $text ), 5000 ), 'url' => $product->get_permalink(), 'images' => sevenbe_feed_images( $product, $parent_product ), 'price' => (float) ( '' !== $regular ? $regular : $product->get_price() ), 'currency' => get_woocommerce_currency(), 'availability' => sevenbe_feed_availability( $product ), 'brand' => sevenbe_feed_brand( $source ), 'gtin' => sevenbe_feed_gtin( $product ), 'mpn' => (string) $product->get_meta( '_mpn', true ), 'category' => sevenbe_feed_category( $source ), ); if ( '' !== $sale && $product->is_on_sale() ) { $line['salePrice'] = (float) $sale; } if ( $parent_product ) { $line['groupId'] = sevenbe_feed_key( $parent_product ); $line['groupTitle'] = wp_strip_all_tags( $parent_product->get_name() ); $line['options'] = array(); foreach ( $product->get_variation_attributes( false ) as $name => $value ) { if ( '' === $value ) { continue; // "Any" value: the variant does not fix this option. } $term = taxonomy_exists( $name ) ? get_term_by( 'slug', $value, $name ) : null; $label = wc_attribute_label( $name, $parent_product ); $line['options'][ $label ] = $term ? $term->name : $value; } } return $line; } /** * A product's feed lines: one for a simple product, one per variant for a variable one. * * @param WC_Product $product The product. * @return array[] */ function sevenbe_feed_lines( $product ) { if ( ! $product || 'publish' !== $product->get_status() || 'hidden' === $product->get_catalog_visibility() || $product->is_type( 'grouped' ) ) { return array(); } if ( ! $product->is_type( 'variable' ) ) { $line = sevenbe_feed_line( $product ); return $line ? array( $line ) : array(); } $lines = array(); foreach ( $product->get_children() as $variation_id ) { $variation = wc_get_product( $variation_id ); if ( $variation && $variation->exists() && 'publish' === $variation->get_status() ) { $line = sevenbe_feed_line( $variation, $product ); if ( $line ) { $lines[] = $line; } } } return $lines; } // ---------- Sending ---------- /** * The full send, a few pages per run: the first request opens it, the last makes it the catalog on 7BE. * A run that has more to send schedules the next one a minute later. */ function sevenbe_feed_step() { $config = sevenbe_feed_config( true ); $state = get_option( SEVENBE_FEED_OPTION, array() ); if ( empty( $config['feeds'] ) ) { delete_option( SEVENBE_FEED_OPTION ); return; } $page = isset( $state['page'] ) ? absint( $state['page'] ) : 0; $sent = isset( $state['sent'] ) ? absint( $state['sent'] ) : 0; $limit = max( 1, (int) $config['limit'] ); if ( 0 === $page ) { $count = wc_get_products( array( 'status' => 'publish', 'limit' => 1, 'paginate' => true, 'return' => 'ids', ) ); $start = sevenbe_signed_api( array( 'start' => true, 'total' => is_object( $count ) ? (int) $count->total : 0, ), true, '/api/apps/feeds/catalog' ); if ( isset( $start['error'] ) ) { return; } $page = 1; } $done = false; for ( $run = 0; $run < 4 && ! $done; $run++ ) { $products = wc_get_products( array( 'status' => 'publish', 'limit' => 50, 'page' => $page, 'orderby' => 'ID', 'order' => 'ASC', ) ); $lines = array(); foreach ( $products as $product ) { $lines = array_merge( $lines, sevenbe_feed_lines( $product ) ); } $lines = array_slice( $lines, 0, max( 0, $limit - $sent ) ); $done = count( $products ) < 50 || $sent + count( $lines ) >= $limit; foreach ( array_chunk( $lines, 100 ) as $i => $chunk ) { $last = $done && count( $lines ) <= ( $i + 1 ) * 100; $result = sevenbe_signed_api( array_merge( array( 'products' => $chunk ), $last ? array( 'done' => true ) : array() ), true, '/api/apps/feeds/catalog' ); if ( isset( $result['error'] ) ) { update_option( SEVENBE_FEED_OPTION, array( 'page' => 0, 'sent' => 0, 'error' => sanitize_key( $result['error'] ), ), false ); return; } } if ( $done && ! $lines ) { sevenbe_signed_api( array( 'done' => true ), true, '/api/apps/feeds/catalog' ); } $sent += count( $lines ); ++$page; } if ( $done ) { update_option( SEVENBE_FEED_OPTION, array( 'page' => 0, 'sent' => 0, 'last' => gmdate( 'c' ), 'count' => $sent, ), false ); delete_transient( 'sevenbe_feed_config' ); return; } update_option( SEVENBE_FEED_OPTION, array( 'page' => $page, 'sent' => $sent, ), false ); wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'sevenbe_feed_step' ); } add_action( 'sevenbe_feed_step', 'sevenbe_feed_step' ); /** * Follow what 7BE says about feeds (the blocks answer's `feeds`, see sevenbe_follow_server()). A change drops the * cached terms, so product changes follow at once; when feeds are on and the catalog was never sent, the first * full send starts now instead of at the next daily run. * * @param bool $on Whether 7BE keeps feeds for this store. */ function sevenbe_feeds_follow( $on ) { $cached = get_transient( 'sevenbe_feed_config' ); if ( is_array( $cached ) && ! empty( $cached['feeds'] ) !== $on ) { delete_transient( 'sevenbe_feed_config' ); } $state = get_option( SEVENBE_FEED_OPTION, array() ); if ( $on && empty( $state['last'] ) && empty( $state['page'] ) && ! wp_next_scheduled( 'sevenbe_feed_step' ) ) { update_option( SEVENBE_FEED_OPTION, array( 'page' => 0, 'sent' => 0, ), false ); wp_schedule_single_event( time() + 10, 'sevenbe_feed_step' ); } } /** * The store disconnects or the plugin is switched off: no catalog send stays scheduled. The daily send comes back * on the first admin page load once the store is connected again. */ function sevenbe_feeds_stop() { wp_clear_scheduled_hook( 'sevenbe_feed_daily' ); wp_clear_scheduled_hook( 'sevenbe_feed_step' ); wp_unschedule_hook( 'sevenbe_feed_product' ); wp_unschedule_hook( 'sevenbe_feed_removed' ); } // Once a day a full send starts over (the daily event exists while the store is connected; it does nothing while feeds are off). add_action( 'sevenbe_feed_daily', function () { if ( '' === sevenbe_options()['key'] ) { wp_clear_scheduled_hook( 'sevenbe_feed_daily' ); return; } update_option( SEVENBE_FEED_OPTION, array( 'page' => 0, 'sent' => 0, ), false ); sevenbe_feed_step(); } ); add_action( 'admin_init', function () { if ( '' !== sevenbe_options()['key'] && ! wp_next_scheduled( 'sevenbe_feed_daily' ) ) { wp_schedule_event( time() + 5 * MINUTE_IN_SECONDS, 'daily', 'sevenbe_feed_daily' ); } } ); /** * A product saved: its lines go to 7BE half a minute later (repeated saves in that time send once), and Bing hears of its page. * * @param int $product_id The product or variation saved. */ function sevenbe_feed_product_saved( $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product ) { return; } $parent_id = $product->get_parent_id() ? $product->get_parent_id() : $product->get_id(); if ( ! wp_next_scheduled( 'sevenbe_feed_product', array( $parent_id ) ) ) { wp_schedule_single_event( time() + 30, 'sevenbe_feed_product', array( $parent_id ) ); } } add_action( 'woocommerce_new_product', 'sevenbe_feed_product_saved', 20 ); add_action( 'woocommerce_update_product', 'sevenbe_feed_product_saved', 20 ); add_action( 'woocommerce_update_product_variation', 'sevenbe_feed_product_saved', 20 ); add_action( 'sevenbe_feed_product', function ( $product_id ) { $product = wc_get_product( $product_id ); if ( ! $product || $product->get_parent_id() ) { return; } $lines = sevenbe_feed_lines( $product ); if ( $lines ) { sevenbe_indexnow( $product->get_permalink() ); } if ( empty( sevenbe_feed_config()['feeds'] ) ) { return; } // This runs in WP-Cron, not while the merchant waits, so it waits for 7BE's answer. $key = sevenbe_feed_key( $product ); sevenbe_signed_api( $lines ? array( 'changed' => true, 'parents' => array( $key ), 'products' => $lines, ) : array( 'removed' => array( $key ) ), true, '/api/apps/feeds/catalog' ); } ); /** * A product moved to the bin or deleted: its lines leave the feed, sent from WP-Cron right after. * * @param int $post_id The post. */ function sevenbe_feed_product_gone( $post_id ) { if ( 'product' !== get_post_type( $post_id ) || empty( sevenbe_feed_config()['feeds'] ) ) { return; } $product = wc_get_product( $post_id ); if ( $product ) { wp_schedule_single_event( time(), 'sevenbe_feed_removed', array( sevenbe_feed_key( $product ) ) ); } } add_action( 'wp_trash_post', 'sevenbe_feed_product_gone' ); add_action( 'before_delete_post', 'sevenbe_feed_product_gone' ); add_action( 'sevenbe_feed_removed', function ( $key ) { sevenbe_signed_api( array( 'removed' => array( (string) $key ) ), true, '/api/apps/feeds/catalog' ); } ); // ---------- IndexNow ---------- /** * Another plugin that already sends IndexNow, so this fix stays idle. * * @return string Its name, or empty. */ function sevenbe_other_indexnow() { $modules = get_option( 'rank_math_modules', array() ); if ( defined( 'RANK_MATH_VERSION' ) && is_array( $modules ) && in_array( 'instant-indexing', $modules, true ) ) { return 'Rank Math'; } foreach ( (array) get_option( 'active_plugins', array() ) as $plugin ) { if ( false !== stripos( (string) $plugin, 'indexnow' ) ) { return 'IndexNow'; } } return ''; } /** * While 7BE is on for the store, 7BE gives the store an IndexNow key in the blocks answer (0.9.5+): the plugin takes it * as the site's key, serves it at /.txt and sends IndexNow product changes even with the fix off, and 7BE sends the * product pages it changes (its answers in the "Questions and answers" tab) with the same key. When 7BE stops, the key goes * unless the merchant turned the fix on. * * @param mixed $key The `indexNow` field of the blocks answer: 32 hex characters, or null. */ function sevenbe_indexnow_follow( $key ) { if ( is_string( $key ) && preg_match( '/^[a-f0-9]{32}$/', $key ) ) { if ( get_option( SEVENBE_INDEXNOW_OPTION ) !== $key ) { update_option( SEVENBE_INDEXNOW_OPTION, $key, false ); } if ( ! get_option( 'sevenbe_ai_shelf_indexnow_7be' ) ) { update_option( 'sevenbe_ai_shelf_indexnow_7be', 1, true ); } return; } sevenbe_indexnow_forget(); } /** * 7BE no longer gives a key (stopped, or the store disconnected): the key leaves with it unless the fix is on. */ function sevenbe_indexnow_forget() { if ( get_option( 'sevenbe_ai_shelf_indexnow_7be' ) ) { delete_option( 'sevenbe_ai_shelf_indexnow_7be' ); if ( ! sevenbe_fix_on( 'indexnow' ) ) { delete_option( SEVENBE_INDEXNOW_OPTION ); } } } /** * @return bool The site serves an IndexNow key: the merchant's fix is on, or 7BE gave one. */ function sevenbe_indexnow_on() { return sevenbe_fix_on( 'indexnow' ) || (bool) get_option( 'sevenbe_ai_shelf_indexnow_7be' ); } /** * @return string The site's IndexNow key, made once. */ function sevenbe_indexnow_key() { $key = (string) get_option( SEVENBE_INDEXNOW_OPTION, '' ); if ( ! preg_match( '/^[a-f0-9]{32}$/', $key ) ) { $key = bin2hex( random_bytes( 16 ) ); update_option( SEVENBE_INDEXNOW_OPTION, $key, false ); } return $key; } /** * Tell IndexNow (Bing and the other engines that share it) that a page changed. At most once per page in ten * minutes. Called from WP-Cron (sevenbe_feed_product), so the short wait is nobody's. * * @param string $url The page. */ function sevenbe_indexnow( $url ) { if ( ! sevenbe_indexnow_on() || sevenbe_other_indexnow() || ! $url ) { return; } $seen = 'sevenbe_in_' . md5( $url ); if ( get_transient( $seen ) ) { return; } set_transient( $seen, 1, 10 * MINUTE_IN_SECONDS ); wp_remote_get( add_query_arg( array( 'url' => rawurlencode( $url ), 'key' => sevenbe_indexnow_key(), ), SEVENBE_INDEXNOW ), array( 'timeout' => 5 ) ); } // IndexNow checks the key at /.txt on the site (also while another plugin sends IndexNow: 7BE's own sends need it). add_action( 'parse_request', function ( $wp ) { if ( ! sevenbe_indexnow_on() || sevenbe_indexnow_key() . '.txt' !== $wp->request ) { return; } status_header( 200 ); header( 'Content-Type: text/plain; charset=utf-8' ); echo esc_html( sevenbe_indexnow_key() ); exit; }, 0 ); // ---------- Delivery and returns in product data ---------- /** * Add delivery and returns, as the merchant confirmed them in 7BE, to one schema.org Offer. * * @param array $offer schema.org Offer or AggregateOffer. * @return array */ function sevenbe_offer_terms( $offer ) { if ( ! is_array( $offer ) || ! sevenbe_fix_on( 'shipping_ld' ) ) { return $offer; } $terms = sevenbe_feed_config()['shipping']; if ( ! is_array( $terms ) || empty( $terms['country'] ) ) { return $offer; } $currency = isset( $offer['priceCurrency'] ) ? $offer['priceCurrency'] : get_woocommerce_currency(); $price = isset( $offer['price'] ) ? (float) $offer['price'] : ( isset( $offer['lowPrice'] ) ? (float) $offer['lowPrice'] : 0 ); $free = isset( $terms['freeOver'] ) && null !== $terms['freeOver'] && $price >= (float) $terms['freeOver']; $delivery = array( '@type' => 'OfferShippingDetails', 'shippingRate' => array( '@type' => 'MonetaryAmount', 'value' => $free ? 0 : (float) $terms['price'], 'currency' => $currency, ), 'shippingDestination' => array( '@type' => 'DefinedRegion', 'addressCountry' => $terms['country'], ), ); $time = array( '@type' => 'ShippingDeliveryTime' ); $windows = array( 'handlingTime' => 'handlingDays', 'transitTime' => 'transitDays', ); foreach ( $windows as $field => $key ) { if ( ! empty( $terms[ $key ] ) && is_array( $terms[ $key ] ) ) { $time[ $field ] = array( '@type' => 'QuantitativeValue', 'minValue' => (int) $terms[ $key ][0], 'maxValue' => (int) $terms[ $key ][1], 'unitCode' => 'DAY', ); } } if ( count( $time ) > 1 ) { $delivery['deliveryTime'] = $time; } $offer['shippingDetails'] = $delivery; $returns = isset( $terms['returns'] ) && is_array( $terms['returns'] ) ? $terms['returns'] : array(); if ( empty( $returns['accepted'] ) ) { $category = 'https://schema.org/MerchantReturnNotPermitted'; } else { $category = empty( $returns['days'] ) ? 'https://schema.org/MerchantReturnUnspecified' : 'https://schema.org/MerchantReturnFiniteReturnWindow'; } $policy = array( '@type' => 'MerchantReturnPolicy', 'applicableCountry' => $terms['country'], 'returnPolicyCategory' => $category, ); if ( ! empty( $returns['accepted'] ) && ! empty( $returns['days'] ) ) { $policy['merchantReturnDays'] = (int) $returns['days']; } if ( ! empty( $returns['url'] ) ) { $policy['merchantReturnLink'] = esc_url_raw( $returns['url'] ); } $offer['hasMerchantReturnPolicy'] = $policy; return $offer; } /** * Delivery and returns on every Offer of a Product, whether `offers` is one Offer or a list. * * @param array $data schema.org Product. * @return array */ function sevenbe_add_offer_terms( $data ) { if ( ! is_array( $data ) || empty( $data['offers'] ) || ! is_array( $data['offers'] ) ) { return $data; } $data['offers'] = isset( $data['offers']['@type'] ) ? sevenbe_offer_terms( $data['offers'] ) : array_map( 'sevenbe_offer_terms', $data['offers'] ); return $data; } add_filter( 'woocommerce_structured_data_product', 'sevenbe_add_offer_terms', 30 ); add_filter( 'rank_math/snippet/rich_snippet_product_entity', 'sevenbe_add_offer_terms', 30 ); add_filter( 'wpseo_schema_product', 'sevenbe_add_offer_terms', 30 ); // ---------- The feeds card on the plugin page ---------- /** * Where the store's feeds stand, with a link to manage them on 7be.io. */ function sevenbe_render_feeds() { $config = sevenbe_feed_config(); $state = get_option( SEVENBE_FEED_OPTION, array() ); echo '
'; echo '

' . esc_html__( 'Feeds for AI shopping', '7be-ai-shelf' ) . '

'; if ( ! empty( $config['feeds'] ) ) { echo '

' . esc_html__( 'On. The plugin sends your catalog to 7BE every day, and each product again right after you change it, with its GTIN and every variant. 7BE keeps it in the files Google Merchant Center, Microsoft Merchant Center and ChatGPT read.', '7be-ai-shelf' ) . '

'; if ( ! empty( $state['last'] ) ) { /* translators: 1: number of feed lines, 2: date and time. */ echo '

' . esc_html( sprintf( __( 'Last full send: %1$s lines, %2$s.', '7be-ai-shelf' ), number_format_i18n( (int) $state['count'] ), sevenbe_date( $state['last'] ) ) ) . '

'; } elseif ( ! empty( $state['page'] ) ) { echo '

' . esc_html__( 'Sending your catalog now, a few pages a minute.', '7be-ai-shelf' ) . '

'; } } else { echo '

' . esc_html__( 'Off. Feeds turn on by themselves when you start 7BE in the dashboard: your catalog goes into the files Google Merchant Center, Microsoft Merchant Center (Copilot) and ChatGPT read, and the plugin sends it every day.', '7be-ai-shelf' ) . '

'; } echo '
'; echo ''; wp_nonce_field( 'sevenbe_feed_send' ); echo '

' . esc_html__( 'Open 7BE dashboard', '7be-ai-shelf' ) . ' '; if ( ! empty( $config['feeds'] ) ) { submit_button( __( 'Send my catalog now', '7be-ai-shelf' ), 'secondary', 'submit', false ); } echo '

'; } add_action( 'admin_post_sevenbe_feed_send', function () { if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', '7be-ai-shelf' ) ); } check_admin_referer( 'sevenbe_feed_send' ); delete_transient( 'sevenbe_feed_config' ); update_option( SEVENBE_FEED_OPTION, array( 'page' => 0, 'sent' => 0, ), false ); sevenbe_feed_step(); wp_safe_redirect( sevenbe_page_url( array( 'sevenbe' => 'feed-sent' ) ) . '#sevenbe-feeds' ); exit; } );