*/ public static function labels(): array { return array( 'Article' => __( 'Article', 'elevenrank' ), 'BlogPosting' => __( 'Blog post', 'elevenrank' ), 'NewsArticle' => __( 'News article', 'elevenrank' ), 'Product' => __( 'Product', 'elevenrank' ), 'Recipe' => __( 'Recipe', 'elevenrank' ), 'Event' => __( 'Event', 'elevenrank' ), 'VideoObject' => __( 'Video', 'elevenrank' ), 'Review' => __( 'Review', 'elevenrank' ), ); } /** * The fields of a type. * * @since 1.0.0 * * @param string $type One of {@see self::TYPES}. * @return array> */ public static function fields( string $type ): array { switch ( $type ) { case 'Article': case 'BlogPosting': case 'NewsArticle': return self::article_fields(); case 'Product': return self::product_fields(); case 'Recipe': return self::recipe_fields(); case 'Event': return self::event_fields(); case 'VideoObject': return self::video_fields(); case 'Review': return self::review_fields(); } return array(); } /** * Everything the editor needs: each type with its label, a one-line summary of what Google asks * for, and its fields. * * @since 1.0.0 * * @return array> */ public static function describe(): array { $labels = self::labels(); $hints = self::hints(); $out = array(); foreach ( self::TYPES as $type ) { $out[] = array( 'type' => $type, 'label' => $labels[ $type ], 'hint' => $hints[ $type ], 'fields' => self::fields( $type ), ); } return $out; } /** * What Google asks for, in a sentence per type. * * @since 1.0.0 * * @return array */ private static function hints(): array { return array( 'Article' => __( 'Google asks for nothing in particular; the author, dates, headline and image help. They are taken from the post.', 'elevenrank' ), 'BlogPosting' => __( 'A kind of article, for blog posts. Google asks for nothing in particular; the author, dates, headline and image help.', 'elevenrank' ), 'NewsArticle' => __( 'A kind of article, for news. Google asks for nothing in particular; the author, dates, headline and image help.', 'elevenrank' ), 'Product' => __( 'Google needs a name and at least one of an offer (price and currency) or a rating (value and number of ratings). Only enter ratings that real customers gave.', 'elevenrank' ), 'Recipe' => __( 'Google needs a name and an image. Times, ingredients, instructions and the yield make the result richer.', 'elevenrank' ), 'Event' => __( 'Google needs a name, a start date and a place with an address (or an online address for an online event).', 'elevenrank' ), 'VideoObject' => __( 'Google needs a name, a thumbnail and the date the video was uploaded. The video address and the length help. Only describe a video that is on the page.', 'elevenrank' ), 'Review' => __( 'Google needs the name of the item you review, your rating and an author. Only review things you can review honestly; ratings written by a business about itself do not qualify.', 'elevenrank' ), ); } /** * Builds a field definition. * * @since 1.0.0 * * @param string $key Field key. * @param string $label Label. * @param string $kind Kind of value. * @param string $level `required`, `recommended` or `optional`. * @param array $extra Extra properties: `help`, `max`, `min`, `options`, `placeholder`. * @return array */ private static function field( string $key, string $label, string $kind, string $level, array $extra = array() ): array { return array_merge( array( 'key' => $key, 'label' => $label, 'kind' => $kind, 'level' => $level, ), $extra ); } /** * Fields of the article types. * * @since 1.0.0 * * @return array> */ private static function article_fields(): array { return array( self::field( 'headline', __( 'Headline', 'elevenrank' ), 'text', 'recommended', array( 'max' => 110, 'help' => __( 'Empty: the post title. Google shows at most 110 characters.', 'elevenrank' ), ) ), self::field( 'description', __( 'Summary', 'elevenrank' ), 'text', 'optional', array( 'max' => 300, 'help' => __( 'Empty: the meta description.', 'elevenrank' ), ) ), self::field( 'section', __( 'Section', 'elevenrank' ), 'text', 'optional', array( 'max' => 100, 'help' => __( 'Empty: the first category.', 'elevenrank' ), ) ), ); } /** * Fields of the Product type. * * @since 1.0.0 * * @return array> */ private static function product_fields(): array { return array( self::field( 'name', __( 'Name', 'elevenrank' ), 'text', 'required', array( 'max' => 150, 'help' => __( 'Empty: the post title.', 'elevenrank' ), ) ), self::field( 'description', __( 'Description', 'elevenrank' ), 'textarea', 'recommended', array( 'max' => 1000, 'help' => __( 'Empty: the meta description.', 'elevenrank' ), ) ), self::field( 'image', __( 'Image', 'elevenrank' ), 'image', 'recommended', array( 'help' => __( 'Empty: the featured image.', 'elevenrank' ) ) ), self::field( 'brand', __( 'Brand', 'elevenrank' ), 'text', 'recommended', array( 'max' => 100 ) ), self::field( 'sku', __( 'SKU', 'elevenrank' ), 'text', 'optional', array( 'max' => 100 ) ), self::field( 'gtin', __( 'GTIN, EAN or barcode', 'elevenrank' ), 'gtin', 'optional', array( 'help' => __( '8, 12, 13 or 14 digits.', 'elevenrank' ) ) ), self::field( 'mpn', __( 'Manufacturer part number', 'elevenrank' ), 'text', 'optional', array( 'max' => 70 ) ), self::field( 'price', __( 'Price', 'elevenrank' ), 'decimal', 'recommended', array( 'help' => __( 'Digits with a dot for decimals, for example 19.99.', 'elevenrank' ) ) ), self::field( 'currency', __( 'Currency', 'elevenrank' ), 'currency', 'recommended', array( 'help' => __( 'Three letters, for example USD or EUR. Needed together with the price.', 'elevenrank' ) ) ), self::field( 'availability', __( 'Availability', 'elevenrank' ), 'select', 'recommended', array( 'options' => self::availability_options(), ) ), self::field( 'price_valid_until', __( 'Price valid until', 'elevenrank' ), 'date', 'optional' ), self::field( 'rating_value', __( 'Average rating (1 to 5)', 'elevenrank' ), 'decimal', 'optional', array( 'help' => __( 'Only enter a rating that real customers gave and that is shown on the page.', 'elevenrank' ) ) ), self::field( 'rating_count', __( 'Number of ratings', 'elevenrank' ), 'integer', 'optional', array( 'min' => 1, 'help' => __( 'Needed together with the average rating.', 'elevenrank' ), ) ), ); } /** * Fields of the Recipe type. * * @since 1.0.0 * * @return array> */ private static function recipe_fields(): array { return array( self::field( 'name', __( 'Name', 'elevenrank' ), 'text', 'required', array( 'max' => 150, 'help' => __( 'Empty: the post title.', 'elevenrank' ), ) ), self::field( 'image', __( 'Image', 'elevenrank' ), 'image', 'required', array( 'help' => __( 'Empty: the featured image.', 'elevenrank' ) ) ), self::field( 'description', __( 'Description', 'elevenrank' ), 'textarea', 'recommended', array( 'max' => 1000, 'help' => __( 'Empty: the meta description.', 'elevenrank' ), ) ), self::field( 'prep_time', __( 'Preparation time (minutes)', 'elevenrank' ), 'integer', 'recommended', array( 'min' => 1 ) ), self::field( 'cook_time', __( 'Cooking time (minutes)', 'elevenrank' ), 'integer', 'recommended', array( 'min' => 1 ) ), self::field( 'total_time', __( 'Total time (minutes)', 'elevenrank' ), 'integer', 'recommended', array( 'min' => 1, 'help' => __( 'Empty: preparation plus cooking time, when both are given.', 'elevenrank' ), ) ), self::field( 'yield', __( 'Servings or yield', 'elevenrank' ), 'text', 'recommended', array( 'max' => 100, 'help' => __( 'For example “4 servings”.', 'elevenrank' ), ) ), self::field( 'category', __( 'Category', 'elevenrank' ), 'text', 'recommended', array( 'max' => 100, 'help' => __( 'For example “Dessert”.', 'elevenrank' ), ) ), self::field( 'cuisine', __( 'Cuisine', 'elevenrank' ), 'text', 'recommended', array( 'max' => 100 ) ), self::field( 'keywords', __( 'Keywords', 'elevenrank' ), 'text', 'optional', array( 'max' => 200, 'help' => __( 'Separate with commas.', 'elevenrank' ), ) ), self::field( 'ingredients', __( 'Ingredients', 'elevenrank' ), 'lines', 'recommended', array( 'help' => __( 'One ingredient per line.', 'elevenrank' ) ) ), self::field( 'instructions', __( 'Instructions', 'elevenrank' ), 'lines', 'recommended', array( 'help' => __( 'One step per line.', 'elevenrank' ) ) ), self::field( 'calories', __( 'Calories per serving', 'elevenrank' ), 'text', 'optional', array( 'max' => 40, 'help' => __( 'For example “270 calories”.', 'elevenrank' ), ) ), self::field( 'author_name', __( 'Author', 'elevenrank' ), 'text', 'recommended', array( 'max' => 100, 'help' => __( 'Empty: the post author.', 'elevenrank' ), ) ), self::field( 'rating_value', __( 'Average rating (1 to 5)', 'elevenrank' ), 'decimal', 'optional', array( 'help' => __( 'Only enter a rating that real readers gave and that is shown on the page.', 'elevenrank' ) ) ), self::field( 'rating_count', __( 'Number of ratings', 'elevenrank' ), 'integer', 'optional', array( 'min' => 1, 'help' => __( 'Needed together with the average rating.', 'elevenrank' ), ) ), ); } /** * Fields of the Event type. * * @since 1.0.0 * * @return array> */ private static function event_fields(): array { return array( self::field( 'name', __( 'Name', 'elevenrank' ), 'text', 'required', array( 'max' => 150, 'help' => __( 'Empty: the post title.', 'elevenrank' ), ) ), self::field( 'start', __( 'Starts', 'elevenrank' ), 'datetime', 'required', array( 'help' => __( 'In the time zone of this site.', 'elevenrank' ) ) ), self::field( 'end', __( 'Ends', 'elevenrank' ), 'datetime', 'recommended' ), self::field( 'attendance', __( 'Where it takes place', 'elevenrank' ), 'select', 'recommended', array( 'options' => array( 'offline' => __( 'At a place', 'elevenrank' ), 'online' => __( 'Online', 'elevenrank' ), 'mixed' => __( 'Both at a place and online', 'elevenrank' ), ), ) ), self::field( 'location_name', __( 'Venue name', 'elevenrank' ), 'text', 'recommended', array( 'max' => 150 ) ), self::field( 'street', __( 'Street address', 'elevenrank' ), 'text', 'recommended', array( 'max' => 150, 'help' => __( 'An event at a place needs an address: enter at least a street or a city.', 'elevenrank' ), ) ), self::field( 'city', __( 'City', 'elevenrank' ), 'text', 'recommended', array( 'max' => 100 ) ), self::field( 'region', __( 'State or region', 'elevenrank' ), 'text', 'optional', array( 'max' => 100 ) ), self::field( 'postal_code', __( 'Postal code', 'elevenrank' ), 'text', 'optional', array( 'max' => 20 ) ), self::field( 'country', __( 'Country (two letters)', 'elevenrank' ), 'country', 'optional', array( 'help' => __( 'For example US or DE.', 'elevenrank' ) ) ), self::field( 'online_url', __( 'Online address', 'elevenrank' ), 'url', 'optional', array( 'help' => __( 'Where people join an online event.', 'elevenrank' ) ) ), self::field( 'status', __( 'Status', 'elevenrank' ), 'select', 'recommended', array( 'options' => array( 'EventScheduled' => __( 'Scheduled', 'elevenrank' ), 'EventRescheduled' => __( 'Rescheduled', 'elevenrank' ), 'EventPostponed' => __( 'Postponed', 'elevenrank' ), 'EventCancelled' => __( 'Cancelled', 'elevenrank' ), 'EventMovedOnline' => __( 'Moved online', 'elevenrank' ), ), ) ), self::field( 'description', __( 'Description', 'elevenrank' ), 'textarea', 'recommended', array( 'max' => 1000, 'help' => __( 'Empty: the meta description.', 'elevenrank' ), ) ), self::field( 'image', __( 'Image', 'elevenrank' ), 'image', 'recommended', array( 'help' => __( 'Empty: the featured image.', 'elevenrank' ) ) ), self::field( 'organizer_name', __( 'Organizer', 'elevenrank' ), 'text', 'optional', array( 'max' => 100 ) ), self::field( 'organizer_url', __( 'Organizer website', 'elevenrank' ), 'url', 'optional' ), self::field( 'offer_price', __( 'Ticket price', 'elevenrank' ), 'decimal', 'recommended', array( 'help' => __( 'Use 0 for a free event.', 'elevenrank' ) ) ), self::field( 'offer_currency', __( 'Ticket currency', 'elevenrank' ), 'currency', 'recommended' ), self::field( 'offer_url', __( 'Tickets address', 'elevenrank' ), 'url', 'recommended', array( 'help' => __( 'Empty: this page.', 'elevenrank' ) ) ), self::field( 'offer_availability', __( 'Ticket availability', 'elevenrank' ), 'select', 'recommended', array( 'options' => self::availability_options() ) ), self::field( 'offer_valid_from', __( 'Tickets on sale from', 'elevenrank' ), 'datetime', 'optional' ), ); } /** * Fields of the VideoObject type. * * @since 1.0.0 * * @return array> */ private static function video_fields(): array { return array( self::field( 'name', __( 'Name', 'elevenrank' ), 'text', 'required', array( 'max' => 150, 'help' => __( 'Empty: the post title.', 'elevenrank' ), ) ), self::field( 'thumbnail', __( 'Thumbnail', 'elevenrank' ), 'image', 'required', array( 'help' => __( 'Empty: the featured image.', 'elevenrank' ) ) ), self::field( 'upload_date', __( 'Upload date', 'elevenrank' ), 'date', 'required', array( 'help' => __( 'The day the video was published. It is not guessed.', 'elevenrank' ) ) ), self::field( 'description', __( 'Description', 'elevenrank' ), 'textarea', 'recommended', array( 'max' => 1000, 'help' => __( 'Empty: the meta description.', 'elevenrank' ), ) ), self::field( 'content_url', __( 'Video file address', 'elevenrank' ), 'url', 'recommended', array( 'help' => __( 'The address of the video file itself.', 'elevenrank' ) ) ), self::field( 'embed_url', __( 'Player address', 'elevenrank' ), 'url', 'recommended', array( 'help' => __( 'The address of the embeddable player.', 'elevenrank' ) ) ), self::field( 'duration', __( 'Length (seconds)', 'elevenrank' ), 'integer', 'recommended', array( 'min' => 1 ) ), ); } /** * Fields of the Review type. * * @since 1.0.0 * * @return array> */ private static function review_fields(): array { return array( self::field( 'item_name', __( 'Name of the item you review', 'elevenrank' ), 'text', 'required', array( 'max' => 150 ) ), self::field( 'item_type', __( 'What kind of thing it is', 'elevenrank' ), 'select', 'required', array( 'options' => self::review_item_types(), ) ), self::field( 'item_url', __( 'Address of the item', 'elevenrank' ), 'url', 'optional' ), self::field( 'rating_value', __( 'Your rating', 'elevenrank' ), 'decimal', 'required', array( 'help' => __( 'A number from the lowest to the highest rating below.', 'elevenrank' ) ) ), self::field( 'best_rating', __( 'Highest rating', 'elevenrank' ), 'integer', 'recommended', array( 'min' => 1, 'max' => 100, 'help' => __( 'Empty: 5.', 'elevenrank' ), ) ), self::field( 'worst_rating', __( 'Lowest rating', 'elevenrank' ), 'integer', 'recommended', array( 'min' => 0, 'max' => 99, 'help' => __( 'Empty: 1.', 'elevenrank' ), ) ), self::field( 'review_body', __( 'Review text', 'elevenrank' ), 'textarea', 'optional', array( 'max' => 2000, 'help' => __( 'Empty: the excerpt of the post.', 'elevenrank' ), ) ), ); } /** * Availability values shared by products and tickets. * * @since 1.0.0 * * @return array */ private static function availability_options(): array { return array( 'InStock' => __( 'In stock', 'elevenrank' ), 'OutOfStock' => __( 'Out of stock', 'elevenrank' ), 'PreOrder' => __( 'Pre-order', 'elevenrank' ), 'BackOrder' => __( 'Back order', 'elevenrank' ), 'LimitedAvailability' => __( 'Limited availability', 'elevenrank' ), 'SoldOut' => __( 'Sold out', 'elevenrank' ), 'Discontinued' => __( 'Discontinued', 'elevenrank' ), ); } /** * The kinds of item a review can be about. Google lists more; Organization and LocalBusiness are * left out because reviews about yourself do not qualify, and HowTo because it is not built. * * @since 1.0.0 * * @return array */ private static function review_item_types(): array { return array( 'Product' => __( 'Product', 'elevenrank' ), 'Book' => __( 'Book', 'elevenrank' ), 'Course' => __( 'Course', 'elevenrank' ), 'Event' => __( 'Event', 'elevenrank' ), 'Game' => __( 'Game', 'elevenrank' ), 'Movie' => __( 'Movie', 'elevenrank' ), 'MusicRecording' => __( 'Song', 'elevenrank' ), 'MusicPlaylist' => __( 'Album or playlist', 'elevenrank' ), 'Recipe' => __( 'Recipe', 'elevenrank' ), 'SoftwareApplication' => __( 'Software or app', 'elevenrank' ), ); } }