/, with its card, a map with * its pin, its week of hours and anything written about it, and schema.org LocalBusiness data so search engines * read the address, the point and the hours. /locations/ itself is a page with the store locator, made as a draft * when the pages are turned on. */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** Whether locations have pages of their own. */ function deltabreen_maps_location_pages() { return ! empty( deltabreen_maps_options()['location_pages'] ); } /** The part of a location page's address before its name. */ function deltabreen_maps_location_slug() { /** * The part of a location page's address before its name, "locations" unless changed. * * @param string $slug The slug. */ return sanitize_title( apply_filters( 'deltabreen_maps_location_slug', 'locations' ) ); } // Turning the pages on or off changes the site's addresses: rebuild them once, on the next request. add_action( 'update_option_deltabreen_maps', function ( $old, $new ) { $was = ! empty( $old['location_pages'] ); $now = ! empty( $new['location_pages'] ); if ( $was === $now ) { return; } update_option( 'deltabreen_maps_flush', 1, false ); deltabreen_maps_bump_pins(); if ( $now ) { deltabreen_maps_ensure_locations_page(); } }, 10, 2 ); add_action( 'init', function () { if ( get_option( 'deltabreen_maps_flush' ) ) { delete_option( 'deltabreen_maps_flush' ); flush_rewrite_rules(); } }, 99 ); /** A draft page at /locations/ with the store locator, unless the site has a page there already. */ function deltabreen_maps_ensure_locations_page() { $slug = deltabreen_maps_location_slug(); if ( get_page_by_path( $slug, OBJECT, 'page' ) ) { return; } $patterns = deltabreen_maps_patterns(); $page = wp_insert_post( array( 'post_type' => 'page', 'post_status' => 'draft', 'post_name' => $slug, 'post_title' => __( 'Our locations', 'maps-by-deltabreen' ), 'post_content' => $patterns['locator']['content'], ) ); if ( $page && ! is_wp_error( $page ) ) { set_transient( 'deltabreen_maps_locations_page', (int) $page, DAY_IN_SECONDS ); } } add_action( 'admin_notices', function () { $page = (int) get_transient( 'deltabreen_maps_locations_page' ); if ( ! $page || ! current_user_can( 'edit_pages' ) || 'draft' !== get_post_status( $page ) ) { return; } printf( '

%s %s

', esc_html__( 'Each location has a page of its own now. A draft page with a store locator is ready to list them all:', 'maps-by-deltabreen' ), esc_url( (string) get_edit_post_link( $page ) ), esc_html__( 'open it and publish', 'maps-by-deltabreen' ) ); } ); // A location's page: its card, a map with its pin and its week, before whatever is written about it. add_filter( 'the_content', function ( $content ) { if ( ! is_singular( 'deltabreen_location' ) || ! in_the_loop() || ! is_main_query() || ! deltabreen_maps_location_pages() ) { return $content; } $id = get_the_ID(); $place = deltabreen_maps_location_view( $id ); if ( ! $place ) { return $content; } $block = function ( $name, $attrs ) { return render_block( array( 'blockName' => $name, 'attrs' => $attrs, 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array(), ) ); }; $map = ''; if ( null !== $place['lat'] ) { $from = deltabreen_maps_location_pins( array() ); $pins = array_values( array_filter( $from['pins'], function ( $p ) use ( $id ) { return ( $p['id'] ?? '' ) === 'loc-' . $id; } ) ); $map = deltabreen_maps_render( array( 'pins' => $pins, 'center' => $place['lng'] . ',' . $place['lat'], 'zoom' => '15', 'height' => '360px', ) ); } $html = '
' . $block( 'deltabreen/location-card', array( 'location' => $id, 'showPhoto' => true, 'showHours' => true, 'showTitle' => false ) ) . $map . $block( 'deltabreen/hours', array( 'location' => $id, 'showWeek' => true ) ) . '
'; /** * What a location's page shows before its own text. * * @param string $html The card, the map and the week. * @param WP_Post $post The location. */ return apply_filters( 'deltabreen_maps_location_page', $html, get_post( $id ) ) . $content; }, 20 ); /** * Hours in the embed's part of OpenStreetMap's syntax as minutes after midnight: each weekday's ranges, Monday first, * and the dated days; null when unreadable (assets/hours-view.js reads the same). * * @param string $text Such as "Mo-Fr 09:00-17:30; Sa 10:00-14:00; Su off; Dec 25 off". * @return array{week: array>, dated: array}|null */ function deltabreen_maps_parse_hours( $text ) { $text = trim( (string) $text ); if ( '' === $text || false !== strpos( $text, '||' ) ) { return null; } $week = array_fill( 0, 7, array() ); if ( '24/7' === $text ) { return array( 'week' => array_fill( 0, 7, array( array( 0, 1440 ) ) ), 'dated' => array(), ); } $days = array( 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su' ); $months = array( 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ); $rest = function ( $s ) { $s = trim( $s ); if ( preg_match( '/^(off|closed)$/i', $s ) ) { return array(); } if ( '24/7' === $s ) { return array( array( 0, 1440 ) ); } $out = array(); foreach ( '' === $s ? array( 'x' ) : explode( ',', $s ) as $part ) { if ( ! preg_match( '/^\s*(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})\s*$/', $part, $m ) || (int) $m[1] > 24 || (int) $m[3] > 24 ) { return null; } $open = (int) $m[1] * 60 + (int) $m[2]; $close = (int) $m[3] * 60 + (int) $m[4]; $out[] = array( $open, $close <= $open ? $close + 1440 : $close ); } return $out; }; $dated = array(); foreach ( array_filter( array_map( 'trim', preg_split( '/[;\n]/', $text ) ) ) as $rule ) { if ( preg_match( '/^(?:(\d{4})\s+)?(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2})(?:\s*-\s*(\d{1,2}))?(?:\s+(.*))?$/i', $rule, $d ) ) { $ranges = $rest( $d[5] ?? '' ); $from = (int) $d[3]; $to = isset( $d[4] ) && '' !== $d[4] ? (int) $d[4] : $from; if ( null === $ranges || $from < 1 || $to > 31 || $to < $from ) { return null; } for ( $day = $from; $day <= $to; $day++ ) { $dated[] = array( 'year' => '' !== $d[1] ? (int) $d[1] : null, 'month' => array_search( strtolower( $d[2] ), $months, true ) + 1, 'day' => $day, 'ranges' => $ranges, ); } continue; } if ( ! preg_match( '/^((?:Mo|Tu|We|Th|Fr|Sa|Su|PH|SH)(?:\s*[-,]\s*(?:Mo|Tu|We|Th|Fr|Sa|Su|PH|SH))*)?\s*(.*)$/i', $rule, $m ) ) { return null; } $which = array(); if ( '' === ( $m[1] ?? '' ) ) { $which = range( 0, 6 ); } else { foreach ( explode( ',', preg_replace( '/\s+/', '', strtolower( $m[1] ) ) ) as $part ) { $ends = explode( '-', $part ); if ( in_array( $ends[0], array( 'ph', 'sh' ), true ) && 1 === count( $ends ) ) { continue; } $a = array_search( $ends[0], $days, true ); $b = isset( $ends[1] ) ? array_search( $ends[1], $days, true ) : $a; if ( false === $a || false === $b || count( $ends ) > 2 ) { return null; } for ( $n = $a; ; $n = ( $n + 1 ) % 7 ) { $which[] = $n; if ( $n === $b ) { break; } } } } $ranges = $rest( $m[2] ?? '' ); if ( null === $ranges ) { return null; } foreach ( $which as $n ) { $week[ $n ] = $ranges; } } return array( 'week' => $week, 'dated' => $dated, ); } /** A minute after midnight as schema.org writes a time; midnight at the end of a day is 23:59. */ function deltabreen_maps_schema_time( $minute ) { $minute = $minute >= 1440 ? ( 1440 === $minute ? 1439 : $minute - 1440 ) : $minute; return sprintf( '%02d:%02d', intdiv( $minute, 60 ), $minute % 60 ); } /** * A location as schema.org LocalBusiness data: name, address, point, phone, page, photo and opening hours, with a * temporary closure and special days as special opening hours. * * @param int $post_id The location. * @return array|null */ function deltabreen_maps_location_schema( $post_id ) { $place = deltabreen_maps_location_view( $post_id ); if ( ! $place ) { return null; } $get = function ( $field ) use ( $post_id ) { return trim( (string) get_post_meta( $post_id, '_deltabreen_' . $field, true ) ); }; $data = array( '@context' => 'https://schema.org', '@type' => 'LocalBusiness', 'name' => $place['title'], 'url' => get_permalink( $post_id ), ); $street = array_filter( array( '@type' => 'PostalAddress', 'streetAddress' => $get( 'address' ), 'addressLocality' => $get( 'city' ), 'postalCode' => $get( 'postcode' ), 'addressCountry' => $get( 'country' ), ) ); if ( count( $street ) > 1 ) { $data['address'] = $street; } if ( null !== $place['lat'] ) { $data['geo'] = array( '@type' => 'GeoCoordinates', 'latitude' => $place['lat'], 'longitude' => $place['lng'], ); } if ( '' !== $place['phone'] ) { $data['telephone'] = $place['phone']; } if ( '' !== $place['url'] ) { $data['sameAs'] = $place['url']; } $photo = '' !== $place['photo'] ? $place['photo'] : (string) get_the_post_thumbnail_url( $post_id, 'large' ); if ( '' !== $photo ) { $data['image'] = $photo; } $hours = deltabreen_maps_parse_hours( $place['hours'] ); if ( $hours ) { $names = array( 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ); $specs = array(); foreach ( $hours['week'] as $n => $ranges ) { foreach ( $ranges as $r ) { $at = deltabreen_maps_schema_time( $r[0] ) . '-' . deltabreen_maps_schema_time( $r[1] ); if ( ! isset( $specs[ $at ] ) ) { $specs[ $at ] = array( '@type' => 'OpeningHoursSpecification', 'dayOfWeek' => array(), 'opens' => deltabreen_maps_schema_time( $r[0] ), 'closes' => deltabreen_maps_schema_time( $r[1] ), ); } $specs[ $at ]['dayOfWeek'][] = $names[ $n ]; } } if ( $specs ) { $data['openingHoursSpecification'] = array_values( $specs ); } $special = array(); $year = (int) wp_date( 'Y' ); foreach ( $hours['dated'] as $d ) { $on = sprintf( '%04d-%02d-%02d', $d['year'] ?? $year, $d['month'], $d['day'] ); $times = $d['ranges'] ? $d['ranges'] : array( array( 0, 0 ) ); foreach ( $times as $r ) { $special[] = array( '@type' => 'OpeningHoursSpecification', 'validFrom' => $on, 'validThrough' => $on, 'opens' => deltabreen_maps_schema_time( $r[0] ), 'closes' => deltabreen_maps_schema_time( $r[1] ), ); } } if ( '' !== $place['closed_until'] ) { $special[] = array( '@type' => 'OpeningHoursSpecification', 'validFrom' => wp_date( 'Y-m-d' ), 'validThrough' => $place['closed_until'], 'opens' => '00:00', 'closes' => '00:00', ); } if ( $special ) { $data['specialOpeningHoursSpecification'] = $special; } } /** * A location's schema.org data, before it is printed on its page. * * @param array $data The LocalBusiness object. * @param int $post_id The location. */ return apply_filters( 'deltabreen_maps_location_schema', $data, $post_id ); } add_action( 'wp_head', function () { if ( ! is_singular( 'deltabreen_location' ) || ! deltabreen_maps_location_pages() ) { return; } $data = deltabreen_maps_location_schema( get_queried_object_id() ); if ( $data ) { echo wp_get_inline_script_tag( wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ), array( 'type' => 'application/ld+json' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON with every < and > escaped. } } );