$cycle->$field ) ) ? (string) $emissions->$cycle->$field : ''; }; // Only imported images, like the vehicle page shows them $thumbnail_id = get_post_thumbnail_id( $post_id ); $additional_images = array(); foreach ( mobile_api_get_vehicle_images( $post_id ) as $image_id ) { if ( $image_id !== $thumbnail_id ) $additional_images[] = wp_get_attachment_image_url( $image_id, 'full' ); } $make = (string) $meta( 'make' ); $model = (string) $meta( 'model' ); $color = (string) $meta( 'manufacturerColorName' ); $construction_year = (string) $meta( 'constructionYear' ); $first_registration = (string) $meta( 'firstRegistration' ); // yyyyMM $first_registered = ''; if ( preg_match( '/^(\d{4})(\d{2})$/', $first_registration, $parts ) ) $first_registered = $parts[1].'-'.$parts[2]; if ( '' === $color ) $color = (string) mobile_api_translate_data_value( (string) $meta( 'exteriorColor' ) ); return array( 'ad_id' => (string) $meta( 'mobileAdId' ), 'title' => get_post_field( 'post_title', $post_id ), 'description' => (string) $meta( 'plainTextDescription' ), 'link' => get_permalink( $post_id ), 'vehicle_class' => (string) $meta( 'vehicleClass' ), 'category' => (string) $meta( 'category' ), 'brand' => 'VW' === $make ? 'Volkswagen' : $make, // mobile.de often leaves the model empty, e.g. for motorhomes 'model' => '' !== $model ? $model : (string) $meta( 'modelDescription' ), 'condition' => strtolower( (string) $meta( 'condition' ) ), 'price' => ( is_object( $price ) && !empty( $price->consumerPriceGross ) ) ? (string) $price->consumerPriceGross : '', 'currency' => ( is_object( $price ) && !empty( $price->currency ) ) ? (string) $price->currency : 'EUR', 'mileage' => intval( $meta( 'mileage' ) ), 'color' => $color, 'vin' => (string) $meta( 'vin' ), 'fuel' => (string) $meta( 'fuel' ), 'year' => '' !== $construction_year ? $construction_year : substr( $first_registration, 0, 4 ), 'first_registered' => $first_registered, 'weight' => intval( $meta( 'licensedWeight' ) ), 'damaged' => !empty( $meta( 'damageUnrepaired' ) ), // The featured image is the local copy of the first mobile.de image 'image' => get_the_post_thumbnail_url( $post_id, 'full' ) ?: '', 'additional_images' => array_slice( $additional_images, 0, 10 ), 'co2' => $co2( 'combined', 'co2' ), 'co2_class' => $co2( 'combined', 'co2Class' ), 'co2_class_discharged' => $co2( 'discharged', 'co2Class' ), ); } function mobile_api_google_get_product_category( $vehicle ) { if ( 'Car' === $vehicle['vehicle_class'] ) return 916; // Cars, Trucks & Vans if ( 'Motorhome' === $vehicle['vehicle_class'] ) { if ( 'Caravan' === $vehicle['category'] ) return 4243; // Travel Trailers if ( 'MobileHome' !== $vehicle['category'] ) // Stationary, not for public roads return 920; // Recreational Vehicles } return 0; // Motorbikes and commercial vehicles are not allowed } function mobile_api_google_get_engine( $fuel ) { if ( '' === $fuel ) return ''; $engines = array( 'PETROL' => 'gasoline', 'DIESEL' => 'diesel', 'ELECTRICITY' => 'electric', 'HYBRID' => 'hybrid', 'HYBRID_DIESEL' => 'hybrid', 'LPG' => 'lpg', 'CNG' => 'natural_gas', ); return $engines[$fuel] ?? 'other'; } function mobile_api_google_get_feed_item( $post_id, $store_code ) { $vehicle = mobile_api_google_get_vehicle( $post_id ); $category = mobile_api_google_get_product_category( $vehicle ); if ( !$category || '' === $vehicle['price'] || '' === $vehicle['image'] || $vehicle['damaged'] ) return null; // Google excludes vehicles that need a special driving licence, in Germany class C1 from 3.5 t if ( $vehicle['weight'] > 3500 && !get_option( 'mobile_api_google_include_heavy' ) ) return null; $certifications = array(); if ( '' !== $vehicle['co2_class'] ) $certifications[] = array( 'certification_authority' => 'BMWK', 'certification_name' => 'Vehicle_CO2_Class', 'certification_code' => $vehicle['co2_class'] ); if ( '' !== $vehicle['co2_class_discharged'] ) $certifications[] = array( 'certification_authority' => 'BMWK', 'certification_name' => 'Vehicle_CO2_Class_Discharged_Battery', 'certification_code' => $vehicle['co2_class_discharged'] ); $item = array( 'id' => $vehicle['ad_id'], 'title' => $vehicle['title'], 'description' => mb_substr( trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( $vehicle['description'] ) ) ), 0, 5000 ), 'link' => $vehicle['link'], // {store_code} is a literal placeholder that Google fills in 'link_template' => $vehicle['link'].( str_contains( $vehicle['link'], '?' ) ? '&' : '?' ).'store={store_code}', 'image_link' => $vehicle['image'], 'additional_image_link' => $vehicle['additional_images'], 'google_product_category' => $category, 'brand' => $vehicle['brand'], 'model' => $vehicle['model'], 'condition' => $vehicle['condition'], 'price' => $vehicle['price'].' '.$vehicle['currency'], 'mileage' => $vehicle['mileage'].' KM', 'color' => $vehicle['color'], 'vin' => $vehicle['vin'], 'year' => $vehicle['year'], 'date_first_registered' => $vehicle['first_registered'], 'engine' => mobile_api_google_get_engine( $vehicle['fuel'] ), 'co2_emissions' => '' !== $vehicle['co2'] ? $vehicle['co2'].' g/km' : '', 'certification' => $certifications, 'vehicle_fulfillment' => array( 'option' => 'in_store', 'store_code' => $store_code ), ); // Lets dealers correct single attributes, e.g. a model that mobile.de does not provide return apply_filters( 'mobile_api_google_feed_item', $item, $post_id, $vehicle ); } // Scalars become one element, lists repeat the element, associative arrays become groups function mobile_api_google_write_attribute( $xml, $name, $value ) { if ( is_array( $value ) && wp_is_numeric_array( $value ) ) { foreach ( $value as $entry ) mobile_api_google_write_attribute( $xml, $name, $entry ); return; } if ( null === $value || '' === $value ) return; $xml->startElementNs( 'g', $name, null ); if ( is_array( $value ) ) { foreach ( $value as $key => $entry ) mobile_api_google_write_attribute( $xml, $key, $entry ); } else { $xml->text( (string) $value ); } $xml->endElement(); } function mobile_api_google_feed_error( $status, $message ) { status_header( $status ); header( 'Content-Type: text/plain; charset=utf-8', true ); echo esc_html( $message ); exit; } function mobile_api_google_feed_render() { header( 'X-Robots-Tag: noindex, nofollow', true ); $store_code = get_option( 'mobile_api_google_store_code' ); if ( empty( $store_code ) ) mobile_api_google_feed_error( 503, __( 'Please enter a store code in the plugin settings first.', 'ad-integration-for-mobile-de-api' ) ); $post_ids = get_posts( array( 'post_type' => 'mobile-api-vehicle', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', 'orderby' => 'date', 'order' => 'DESC', ) ); update_meta_cache( 'post', $post_ids ); $xml = new XMLWriter(); $xml->openMemory(); $xml->setIndent( true ); $xml->startDocument( '1.0', 'UTF-8' ); $xml->startElement( 'rss' ); $xml->writeAttribute( 'version', '2.0' ); $xml->writeAttribute( 'xmlns:g', 'http://base.google.com/ns/1.0' ); $xml->startElement( 'channel' ); $xml->writeElement( 'title', wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ); $xml->writeElement( 'link', home_url( '/' ) ); $xml->writeElement( 'description', __( 'Vehicle inventory for Google vehicle ads', 'ad-integration-for-mobile-de-api' ) ); foreach ( $post_ids as $post_id ) { $item = mobile_api_google_get_feed_item( $post_id, $store_code ); if ( empty( $item ) ) continue; $xml->startElement( 'item' ); foreach ( $item as $name => $value ) mobile_api_google_write_attribute( $xml, $name, $value ); $xml->endElement(); } $xml->endElement(); // channel $xml->endElement(); // rss $xml->endDocument(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- XMLWriter escapes all content echo $xml->outputMemory(); exit; } // Google compares the feed price with the structured data of the landing page function mobile_api_google_structured_data() { if ( !is_singular( 'mobile-api-vehicle' ) || !mobile_api_google_is_active() ) return; $vehicle = mobile_api_google_get_vehicle( get_queried_object_id() ); if ( '' === $vehicle['price'] ) return; $conditions = array( 'new' => 'https://schema.org/NewCondition', 'used' => 'https://schema.org/UsedCondition', ); $data = array_filter( array( '@context' => 'https://schema.org', '@type' => 'Car' === $vehicle['vehicle_class'] ? 'Car' : 'Vehicle', 'name' => $vehicle['title'], 'url' => $vehicle['link'], 'image' => $vehicle['image'], 'productID' => $vehicle['ad_id'], 'brand' => array( '@type' => 'Brand', 'name' => $vehicle['brand'] ), 'model' => $vehicle['model'], 'vehicleIdentificationNumber' => $vehicle['vin'], 'color' => $vehicle['color'], 'itemCondition' => $conditions[$vehicle['condition']] ?? '', 'mileageFromOdometer' => array( '@type' => 'QuantitativeValue', 'value' => $vehicle['mileage'], 'unitCode' => 'KMT' ), 'offers' => array( '@type' => 'Offer', 'price' => $vehicle['price'], 'priceCurrency' => $vehicle['currency'], 'availability' => 'https://schema.org/InStock', 'url' => $vehicle['link'], ), ), function( $value ) { return '' !== $value; } ); echo ''."\n"; } add_action( 'wp_head', 'mobile_api_google_structured_data' ); // In the EEA Google requires the feed id to appear on the landing page function mobile_api_google_ad_number( $content ) { if ( !is_singular( 'mobile-api-vehicle' ) || !in_the_loop() || !is_main_query() || !mobile_api_google_is_active() ) return $content; $ad_id = maybe_unserialize( get_post_meta( get_the_ID(), 'mobile_api_mobileAdId', true ) ); if ( empty( $ad_id ) ) return $content; return $content.'
'; } add_filter( 'the_content', 'mobile_api_google_ad_number', 20 );