���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC ‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!schema/main-image.php000066600000002216151733470320010533 0ustar00context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH; // The featured image. if ( $this->context->main_image_id ) { $generated_schema = $this->helpers->schema->image->generate_from_attachment_id( $image_id, $this->context->main_image_id ); $this->context->main_image_url = $generated_schema['url']; return $generated_schema; } // The first image in the content. if ( $this->context->main_image_url ) { return $this->helpers->schema->image->generate_from_url( $image_id, $this->context->main_image_url ); } return false; } } schema/website.php000066600000005323151733470320010173 0ustar00 'WebSite', '@id' => $this->context->site_url . Schema_IDs::WEBSITE_HASH, 'url' => $this->context->site_url, 'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->site_name ), 'description' => \get_bloginfo( 'description' ), ]; if ( $this->context->site_represents_reference ) { $data['publisher'] = $this->context->site_represents_reference; } $data = $this->add_alternate_name( $data ); $data = $this->internal_search_section( $data ); $data = $this->helpers->schema->language->add_piece_language( $data ); return $data; } /** * Returns an alternate name if one was specified in the Yoast SEO settings. * * @param array $data The website data array. * * @return array */ private function add_alternate_name( $data ) { if ( $this->context->alternate_site_name !== '' ) { $data['alternateName'] = $this->helpers->schema->html->smart_strip_tags( $this->context->alternate_site_name ); } return $data; } /** * Adds the internal search JSON LD code to the homepage if it's not disabled. * * @link https://developers.google.com/search/docs/data-types/sitelinks-searchbox * * @param array $data The website data array. * * @return array */ private function internal_search_section( $data ) { /** * Filter: 'disable_wpseo_json_ld_search' - Allow disabling of the json+ld output. * * @param bool $display_search Whether or not to display json+ld search on the frontend. */ if ( \apply_filters( 'disable_wpseo_json_ld_search', false ) ) { return $data; } /** * Filter: 'wpseo_json_ld_search_url' - Allows filtering of the search URL for Yoast SEO. * * @param string $search_url The search URL for this site with a `{search_term_string}` variable. */ $search_url = \apply_filters( 'wpseo_json_ld_search_url', $this->context->site_url . '?s={search_term_string}' ); $data['potentialAction'][] = [ '@type' => 'SearchAction', 'target' => [ '@type' => 'EntryPoint', 'urlTemplate' => $search_url, ], 'query-input' => [ '@type' => 'PropertyValueSpecification', 'valueRequired' => true, 'valueName' => 'search_term_string', ], ]; return $data; } } schema/breadcrumb.php000066600000012070151733470320010634 0ustar00context->indexable->object_type === 'unknown' ) { return false; } if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) { return false; } return true; } /** * Returns Schema breadcrumb data to allow recognition of page's position in the site hierarchy. * * @link https://developers.google.com/search/docs/data-types/breadcrumb * * @return bool|array Array on success, false on failure. */ public function generate() { $breadcrumbs = $this->context->presentation->breadcrumbs; $list_elements = []; // In case of pagination, replace the last breadcrumb, because it only contains "Page [number]" and has no URL. if ( ( $this->helpers->current_page->is_paged() || $this->context->indexable->number_of_pages > 1 ) && ( // Do not replace the last breadcrumb on static post pages. ! $this->helpers->current_page->is_static_posts_page() // Do not remove the last breadcrumb if only one exists (bugfix for custom paginated frontpages). && \count( $breadcrumbs ) > 1 ) ) { \array_pop( $breadcrumbs ); } // Only output breadcrumbs that are not hidden. $breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_hidden' ] ); \reset( $breadcrumbs ); /* * Check whether at least one of the breadcrumbs is broken. * If so, do not output anything. */ foreach ( $breadcrumbs as $breadcrumb ) { if ( $this->is_broken( $breadcrumb ) ) { return false; } } // Create the last breadcrumb. $last_breadcrumb = \array_pop( $breadcrumbs ); $breadcrumbs[] = $this->format_last_breadcrumb( $last_breadcrumb ); // If this is a static front page, prevent nested pages from creating a trail. if ( $this->helpers->current_page->is_home_static_page() ) { // Check if we're dealing with a nested page. if ( \count( $breadcrumbs ) > 1 ) { // Store the breadcrumbs home variable before dropping the parent page from the Schema. $breadcrumbs_home = $breadcrumbs[0]['text']; $breadcrumbs = [ \array_pop( $breadcrumbs ) ]; // Make the child page show the breadcrumbs home variable rather than its own title. $breadcrumbs[0]['text'] = $breadcrumbs_home; } } $breadcrumbs = \array_filter( $breadcrumbs, [ $this, 'not_empty_text' ] ); $breadcrumbs = \array_values( $breadcrumbs ); // Create intermediate breadcrumbs. foreach ( $breadcrumbs as $index => $breadcrumb ) { $list_elements[] = $this->create_breadcrumb( $index, $breadcrumb ); } return [ '@type' => 'BreadcrumbList', '@id' => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH, 'itemListElement' => $list_elements, ]; } /** * Returns a breadcrumb array. * * @param int $index The position in the list. * @param array $breadcrumb The position in the list. * * @return array A breadcrumb listItem. */ private function create_breadcrumb( $index, $breadcrumb ) { $crumb = [ '@type' => 'ListItem', 'position' => ( $index + 1 ), 'name' => $this->helpers->schema->html->smart_strip_tags( $breadcrumb['text'] ), ]; if ( ! empty( $breadcrumb['url'] ) ) { $crumb['item'] = $breadcrumb['url']; } return $crumb; } /** * Creates the last breadcrumb in the breadcrumb list, omitting the URL per Google's spec. * * @link https://developers.google.com/search/docs/data-types/breadcrumb * * @param array $breadcrumb The position in the list. * * @return array The last of the breadcrumbs. */ private function format_last_breadcrumb( $breadcrumb ) { unset( $breadcrumb['url'] ); return $breadcrumb; } /** * Tests if the breadcrumb is broken. * A breadcrumb is considered broken: * - when it is not an array. * - when it has no URL or text. * * @param array $breadcrumb The breadcrumb to test. * * @return bool `true` if the breadcrumb is broken. */ private function is_broken( $breadcrumb ) { // A breadcrumb is broken if it is not an array. if ( ! \is_array( $breadcrumb ) ) { return true; } // A breadcrumb is broken if it does not contain a URL or text. if ( ! \array_key_exists( 'url', $breadcrumb ) || ! \array_key_exists( 'text', $breadcrumb ) ) { return true; } return false; } /** * Checks whether the breadcrumb is not set to be hidden. * * @param array $breadcrumb The breadcrumb array. * * @return bool If the breadcrumb should not be hidden. */ private function not_hidden( $breadcrumb ) { return empty( $breadcrumb['hide_in_schema'] ); } /** * Checks whether the breadcrumb has a not empty text. * * @param array $breadcrumb The breadcrumb array. * * @return bool If the breadcrumb has a not empty text. */ private function not_empty_text( $breadcrumb ) { return ! empty( $breadcrumb['text'] ); } } schema/howto.php000066600000012211151733470320007663 0ustar00context->blocks['yoast/how-to-block'] ); } /** * Renders a list of questions, referencing them by ID. * * @return array Our Schema graph. */ public function generate() { $graph = []; foreach ( $this->context->blocks['yoast/how-to-block'] as $index => $block ) { $this->add_how_to( $graph, $block, $index ); } return $graph; } /** * Adds the duration of the task to the Schema. * * @param array $data Our How-To schema data. * @param array $attributes The block data attributes. * * @return void */ private function add_duration( &$data, $attributes ) { if ( empty( $attributes['hasDuration'] ) ) { return; } $days = empty( $attributes['days'] ) ? 0 : $attributes['days']; $hours = empty( $attributes['hours'] ) ? 0 : $attributes['hours']; $minutes = empty( $attributes['minutes'] ) ? 0 : $attributes['minutes']; if ( ( $days + $hours + $minutes ) > 0 ) { $data['totalTime'] = \esc_attr( 'P' . $days . 'DT' . $hours . 'H' . $minutes . 'M' ); } } /** * Adds the steps to our How-To output. * * @param array $data Our How-To schema data. * @param array $steps Our How-To block's steps. * * @return void */ private function add_steps( &$data, $steps ) { foreach ( $steps as $step ) { $schema_id = $this->context->canonical . '#' . \esc_attr( $step['id'] ); $schema_step = [ '@type' => 'HowToStep', 'url' => $schema_id, ]; if ( isset( $step['jsonText'] ) ) { $json_text = $this->helpers->schema->html->sanitize( $step['jsonText'] ); } if ( isset( $step['jsonName'] ) ) { $json_name = $this->helpers->schema->html->smart_strip_tags( $step['jsonName'] ); } if ( empty( $json_name ) ) { if ( empty( $step['text'] ) ) { continue; } $schema_step['text'] = ''; $this->add_step_image( $schema_step, $step ); // If there is no text and no image, don't output the step. if ( empty( $json_text ) && empty( $schema_step['image'] ) ) { continue; } if ( ! empty( $json_text ) ) { $schema_step['text'] = $json_text; } } elseif ( empty( $json_text ) ) { $schema_step['text'] = $json_name; } else { $schema_step['name'] = $json_name; $this->add_step_description( $schema_step, $json_text ); $this->add_step_image( $schema_step, $step ); } $data['step'][] = $schema_step; } } /** * Checks if we have a step description, if we do, add it. * * @param array $schema_step Our Schema output for the Step. * @param string $json_text The step text. * * @return void */ private function add_step_description( &$schema_step, $json_text ) { $schema_step['itemListElement'] = [ [ '@type' => 'HowToDirection', 'text' => $json_text, ], ]; } /** * Checks if we have a step image, if we do, add it. * * @param array $schema_step Our Schema output for the Step. * @param array $step The step block data. * * @return void */ private function add_step_image( &$schema_step, $step ) { if ( isset( $step['text'] ) && \is_array( $step['text'] ) ) { foreach ( $step['text'] as $line ) { if ( \is_array( $line ) && isset( $line['type'] ) && $line['type'] === 'img' ) { $schema_step['image'] = $this->get_image_schema( \esc_url( $line['props']['src'] ) ); } } } } /** * Generates the HowTo schema for a block. * * @param array $graph Our Schema data. * @param array $block The How-To block content. * @param int $index The index of the current block. * * @return void */ protected function add_how_to( &$graph, $block, $index ) { $data = [ '@type' => 'HowTo', '@id' => $this->context->canonical . '#howto-' . ( $index + 1 ), 'name' => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ), 'mainEntityOfPage' => [ '@id' => $this->context->main_schema_id ], 'description' => '', ]; if ( $this->context->has_article ) { $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id . Schema_IDs::ARTICLE_HASH ]; } if ( isset( $block['attrs']['jsonDescription'] ) ) { $data['description'] = $this->helpers->schema->html->sanitize( $block['attrs']['jsonDescription'] ); } $this->add_duration( $data, $block['attrs'] ); if ( isset( $block['attrs']['steps'] ) ) { $this->add_steps( $data, $block['attrs']['steps'] ); } $data = $this->helpers->schema->language->add_piece_language( $data ); $graph[] = $data; } /** * Generates the image schema from the attachment $url. * * @param string $url Attachment url. * * @return array Image schema. */ protected function get_image_schema( $url ) { $schema_id = $this->context->canonical . '#schema-image-' . \md5( $url ); return $this->helpers->schema->image->generate_from_url( $schema_id, $url ); } } schema/person.php000066600000023057151733470320010043 0ustar00site_represents_current_author() ) { return false; } return $this->context->site_represents === 'person'; } /** * Returns Person Schema data. * * @return bool|array Person data on success, false on failure. */ public function generate() { $user_id = $this->determine_user_id(); if ( ! $user_id ) { return false; } return $this->build_person_data( $user_id ); } /** * Determines a User ID for the Person data. * * @return bool|int User ID or false upon return. */ protected function determine_user_id() { /** * Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output. * * @param int|bool $user_id The user ID currently determined. */ $user_id = \apply_filters( 'wpseo_schema_person_user_id', $this->context->site_user_id ); // It should to be an integer higher than 0. if ( \is_int( $user_id ) && $user_id > 0 ) { return $user_id; } return false; } /** * Retrieve a list of social profile URLs for Person. * * @param string[] $same_as_urls Array of SameAs URLs. * @param int $user_id User ID. * * @return string[] A list of SameAs URLs. */ protected function get_social_profiles( $same_as_urls, $user_id ) { /** * Filter: 'wpseo_schema_person_social_profiles' - Allows filtering of social profiles per user. * * @param string[] $social_profiles The array of social profiles to retrieve. Each should be a user meta field * key. As they are retrieved using the WordPress function `get_the_author_meta`. * @param int $user_id The current user we're grabbing social profiles for. */ $social_profiles = \apply_filters( 'wpseo_schema_person_social_profiles', $this->social_profiles, $user_id ); // We can only handle an array. if ( ! \is_array( $social_profiles ) ) { return $same_as_urls; } foreach ( $social_profiles as $profile ) { // Skip non-string values. if ( ! \is_string( $profile ) ) { continue; } $social_url = $this->url_social_site( $profile, $user_id ); if ( $social_url ) { $same_as_urls[] = $social_url; } } return $same_as_urls; } /** * Builds our array of Schema Person data for a given user ID. * * @param int $user_id The user ID to use. * @param bool $add_hash Wether or not the person's image url hash should be added to the image id. * * @return array An array of Schema Person data. */ protected function build_person_data( $user_id, $add_hash = false ) { $user_data = \get_userdata( $user_id ); $data = [ '@type' => $this->type, '@id' => $this->helpers->schema->id->get_user_schema_id( $user_id, $this->context ), ]; // Safety check for the `get_userdata` WP function, which could return false. if ( $user_data === false ) { return $data; } $data['name'] = $this->helpers->schema->html->smart_strip_tags( $user_data->display_name ); $data = $this->add_image( $data, $user_data, $add_hash ); if ( ! empty( $user_data->description ) ) { $data['description'] = $this->helpers->schema->html->smart_strip_tags( $user_data->description ); } if ( \is_array( $this->context->schema_page_type ) && \in_array( 'ProfilePage', $this->context->schema_page_type, true ) ) { $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id, ]; } $data = $this->add_same_as_urls( $data, $user_data, $user_id ); /** * Filter: 'wpseo_schema_person_data' - Allows filtering of schema data per user. * * @param array $data The schema data we have for this person. * @param int $user_id The current user we're collecting schema data for. */ $data = \apply_filters( 'wpseo_schema_person_data', $data, $user_id ); return $data; } /** * Returns an ImageObject for the persons avatar. * * @param array $data The Person schema. * @param WP_User $user_data User data. * @param bool $add_hash Wether or not the person's image url hash should be added to the image id. * * @return array The Person schema. */ protected function add_image( $data, $user_data, $add_hash = false ) { $schema_id = $this->context->site_url . Schema_IDs::PERSON_LOGO_HASH; $data = $this->set_image_from_options( $data, $schema_id, $add_hash, $user_data ); if ( ! isset( $data['image'] ) ) { $data = $this->set_image_from_avatar( $data, $user_data, $schema_id, $add_hash ); } if ( \is_array( $this->type ) && \in_array( 'Organization', $this->type, true ) ) { $data_logo = ( $data['image']['@id'] ?? $schema_id ); $data['logo'] = [ '@id' => $data_logo ]; } return $data; } /** * Generate the person image from our settings. * * @param array $data The Person schema. * @param string $schema_id The string used in the `@id` for the schema. * @param bool $add_hash Whether or not the person's image url hash should be added to the image id. * @param WP_User|null $user_data User data. * * @return array The Person schema. */ protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) { if ( $this->context->site_represents !== 'person' ) { return $data; } if ( \is_array( $this->context->person_logo_meta ) ) { $data['image'] = $this->helpers->schema->image->generate_from_attachment_meta( $schema_id, $this->context->person_logo_meta, $data['name'], $add_hash ); } return $data; } /** * Generate the person logo from gravatar. * * @param array $data The Person schema. * @param WP_User $user_data User data. * @param string $schema_id The string used in the `@id` for the schema. * @param bool $add_hash Wether or not the person's image url hash should be added to the image id. * * @return array The Person schema. */ protected function set_image_from_avatar( $data, $user_data, $schema_id, $add_hash = false ) { // If we don't have an image in our settings, fall back to an avatar, if we're allowed to. $show_avatars = \get_option( 'show_avatars' ); if ( ! $show_avatars ) { return $data; } $url = \get_avatar_url( $user_data->user_email ); if ( empty( $url ) ) { return $data; } $data['image'] = $this->helpers->schema->image->simple_image_object( $schema_id, $url, $user_data->display_name, $add_hash ); return $data; } /** * Returns an author's social site URL. * * @param string $social_site The social site to retrieve the URL for. * @param int|false $user_id The user ID to use function outside of the loop. * * @return string */ protected function url_social_site( $social_site, $user_id = false ) { $url = \get_the_author_meta( $social_site, $user_id ); if ( ! empty( $url ) && $social_site === 'twitter' ) { $url = 'https://x.com/' . $url; } return $url; } /** * Checks the site is represented by the same person as this indexable. * * @param WP_User|null $user_data User data. * * @return bool True when the site is represented by the same person as this indexable. */ protected function site_represents_current_author( $user_data = null ) { // Can only be the case when the site represents a user. if ( $this->context->site_represents !== 'person' ) { return false; } // Article post from the same user as the site represents. if ( $this->context->indexable->object_type === 'post' && $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) && $this->context->schema_article_type !== 'None' ) { $user_id = ( $user_data instanceof WP_User && isset( $user_data->ID ) ) ? $user_data->ID : $this->context->indexable->author_id; return $this->context->site_user_id === $user_id; } // Author archive from the same user as the site represents. return $this->context->indexable->object_type === 'user' && $this->context->site_user_id === $this->context->indexable->object_id; } /** * Builds our SameAs array. * * @param array $data The Person schema data. * @param WP_User $user_data The user data object. * @param int $user_id The user ID to use. * * @return array The Person schema data. */ protected function add_same_as_urls( $data, $user_data, $user_id ) { $same_as_urls = []; // Add the "Website" field from WordPress' contact info. if ( ! empty( $user_data->user_url ) ) { $same_as_urls[] = $user_data->user_url; } // Add the social profiles. $same_as_urls = $this->get_social_profiles( $same_as_urls, $user_id ); if ( ! empty( $same_as_urls ) ) { $same_as_urls = \array_values( \array_unique( $same_as_urls ) ); $data['sameAs'] = $same_as_urls; } return $data; } } schema/author.php000066600000005736151733470320010043 0ustar00context->indexable->object_type === 'user' ) { return true; } if ( $this->context->indexable->object_type === 'post' && $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) && $this->context->schema_article_type !== 'None' ) { return true; } return false; } /** * Returns Person Schema data. * * @return bool|array Person data on success, false on failure. */ public function generate() { $user_id = $this->determine_user_id(); if ( ! $user_id ) { return false; } $data = $this->build_person_data( $user_id ); if ( $this->site_represents_current_author() === false ) { $data['@type'] = [ 'Person' ]; unset( $data['logo'] ); } // If this is an author page, the Person object is the main object, so we set it as such here. if ( $this->context->indexable->object_type === 'user' ) { $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id, ]; } // If this is a post and the author archives are enabled, set the author archive url as the author url. if ( $this->context->indexable->object_type === 'post' ) { if ( $this->helpers->options->get( 'disable-author' ) !== true ) { $data['url'] = $this->helpers->user->get_the_author_posts_url( $user_id ); } } return $data; } /** * Determines a User ID for the Person data. * * @return bool|int User ID or false upon return. */ protected function determine_user_id() { $user_id = 0; if ( $this->context->indexable->object_type === 'post' ) { $user_id = (int) $this->context->post->post_author; } if ( $this->context->indexable->object_type === 'user' ) { $user_id = $this->context->indexable->object_id; } /** * Filter: 'wpseo_schema_person_user_id' - Allows filtering of user ID used for person output. * * @param int|bool $user_id The user ID currently determined. */ $user_id = \apply_filters( 'wpseo_schema_person_user_id', $user_id ); if ( \is_int( $user_id ) && $user_id > 0 ) { return $user_id; } return false; } /** * An author should not have an image from options, this only applies to persons. * * @param array $data The Person schema. * @param string $schema_id The string used in the `@id` for the schema. * @param bool $add_hash Whether or not the person's image url hash should be added to the image id. * @param WP_User|null $user_data User data. * * @return array The Person schema. */ protected function set_image_from_options( $data, $schema_id, $add_hash = false, $user_data = null ) { if ( $this->site_represents_current_author( $user_data ) ) { return parent::set_image_from_options( $data, $schema_id, $add_hash, $user_data ); } return $data; } } schema/abstract-schema-piece.php000066600000001432151733470320012652 0ustar00context->indexable->object_type === 'unknown' ) { return false; } return ! ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ); } /** * Returns WebPage schema data. * * @return array> WebPage schema data. */ public function generate() { $data = [ '@type' => $this->context->schema_page_type, '@id' => $this->context->main_schema_id, 'url' => $this->context->canonical, 'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->title ), 'isPartOf' => [ '@id' => $this->context->site_url . Schema_IDs::WEBSITE_HASH, ], ]; if ( empty( $this->context->canonical ) && \is_search() ) { $data['url'] = $this->build_search_url(); } if ( $this->helpers->current_page->is_front_page() ) { if ( $this->context->site_represents_reference ) { $data['about'] = $this->context->site_represents_reference; } } $data = $this->add_image( $data ); if ( $this->context->indexable->object_type === 'post' ) { $data['datePublished'] = $this->helpers->date->format( $this->context->post->post_date_gmt ); if ( \strtotime( $this->context->post->post_modified_gmt ) > \strtotime( $this->context->post->post_date_gmt ) ) { $data['dateModified'] = $this->helpers->date->format( $this->context->post->post_modified_gmt ); } if ( $this->context->indexable->object_sub_type === 'post' ) { $data = $this->add_author( $data, $this->context->post ); } } if ( ! empty( $this->context->description ) ) { $data['description'] = $this->helpers->schema->html->smart_strip_tags( $this->context->description ); } if ( $this->add_breadcrumbs() ) { $data['breadcrumb'] = [ '@id' => $this->context->canonical . Schema_IDs::BREADCRUMB_HASH, ]; } if ( ! empty( $this->context->main_entity_of_page ) ) { $data['mainEntity'] = $this->context->main_entity_of_page; } $data = $this->helpers->schema->language->add_piece_language( $data ); $data = $this->add_potential_action( $data ); return $data; } /** * Adds an author property to the $data if the WebPage is not represented. * * @param array> $data The WebPage schema. * @param WP_Post $post The post the context is representing. * * @return array> The WebPage schema. */ public function add_author( $data, $post ) { if ( $this->context->site_represents === false ) { $data['author'] = [ '@id' => $this->helpers->schema->id->get_user_schema_id( $post->post_author, $this->context ) ]; } return $data; } /** * If we have an image, make it the primary image of the page. * * @param array> $data WebPage schema data. * * @return array> */ public function add_image( $data ) { if ( $this->context->has_image ) { $data['primaryImageOfPage'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ]; $data['image'] = [ '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH ]; $data['thumbnailUrl'] = $this->context->main_image_url; } return $data; } /** * Determine if we should add a breadcrumb attribute. * * @return bool */ private function add_breadcrumbs() { if ( $this->context->indexable->object_type === 'system-page' && $this->context->indexable->object_sub_type === '404' ) { return false; } return true; } /** * Adds the potential action property to the WebPage Schema piece. * * @param array> $data The WebPage data. * * @return array> The WebPage data with the potential action added. */ private function add_potential_action( $data ) { $url = $this->context->canonical; if ( $data['@type'] === 'CollectionPage' || ( \is_array( $data['@type'] ) && \in_array( 'CollectionPage', $data['@type'], true ) ) ) { return $data; } /** * Filter: 'wpseo_schema_webpage_potential_action_target' - Allows filtering of the schema WebPage potentialAction target. * * @param array $targets The URLs for the WebPage potentialAction target. */ $targets = \apply_filters( 'wpseo_schema_webpage_potential_action_target', [ $url ] ); $data['potentialAction'][] = [ '@type' => 'ReadAction', 'target' => $targets, ]; return $data; } /** * Creates the search URL for use when if there is no canonical. * * @return string Search URL. */ private function build_search_url() { return $this->context->site_url . '?s=' . \rawurlencode( \get_search_query() ); } } schema/organization.php000066600000005145151733470320011237 0ustar00context->site_represents === 'company'; } /** * Returns the Organization Schema data. * * @return array The Organization schema. */ public function generate() { $logo_schema_id = $this->context->site_url . Schema_IDs::ORGANIZATION_LOGO_HASH; if ( $this->context->company_logo_meta ) { $logo = $this->helpers->schema->image->generate_from_attachment_meta( $logo_schema_id, $this->context->company_logo_meta, $this->context->company_name ); } else { $logo = $this->helpers->schema->image->generate_from_attachment_id( $logo_schema_id, $this->context->company_logo_id, $this->context->company_name ); } $organization = [ '@type' => 'Organization', '@id' => $this->context->site_url . Schema_IDs::ORGANIZATION_HASH, 'name' => $this->helpers->schema->html->smart_strip_tags( $this->context->company_name ), ]; if ( ! empty( $this->context->company_alternate_name ) ) { $organization['alternateName'] = $this->context->company_alternate_name; } $organization['url'] = $this->context->site_url; $organization['logo'] = $logo; $organization['image'] = [ '@id' => $logo['@id'] ]; $same_as = \array_values( \array_unique( \array_filter( $this->fetch_social_profiles() ) ) ); if ( ! empty( $same_as ) ) { $organization['sameAs'] = $same_as; } if ( \is_array( $this->context->schema_page_type ) && \in_array( 'ProfilePage', $this->context->schema_page_type, true ) ) { $organization['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id, ]; } return $organization; } /** * Retrieve the social profiles to display in the organization schema. * * @return array An array of social profiles. */ private function fetch_social_profiles() { $profiles = $this->helpers->social_profiles->get_organization_social_profiles(); if ( isset( $profiles['other_social_urls'] ) ) { $other_social_urls = $profiles['other_social_urls']; unset( $profiles['other_social_urls'] ); $profiles = \array_merge( $profiles, $other_social_urls ); } /** * Filter: 'wpseo_schema_organization_social_profiles' - Allows filtering social profiles for the * represented organization. * * @param string[] $profiles */ $profiles = \apply_filters( 'wpseo_schema_organization_social_profiles', $profiles ); return $profiles; } } schema/faq.php000066600000005643151733470320007305 0ustar00context->blocks['yoast/faq-block'] ) ) { return false; } if ( ! \is_array( $this->context->schema_page_type ) ) { $this->context->schema_page_type = [ $this->context->schema_page_type ]; } $this->context->schema_page_type[] = 'FAQPage'; $this->context->main_entity_of_page = $this->generate_ids(); return true; } /** * Generate the IDs so we can link to them in the main entity. * * @return array */ private function generate_ids() { $ids = []; foreach ( $this->context->blocks['yoast/faq-block'] as $block ) { if ( isset( $block['attrs']['questions'] ) ) { foreach ( $block['attrs']['questions'] as $question ) { if ( empty( $question['jsonAnswer'] ) ) { continue; } $ids[] = [ '@id' => $this->context->canonical . '#' . \esc_attr( $question['id'] ) ]; } } } return $ids; } /** * Render a list of questions, referencing them by ID. * * @return array Our Schema graph. */ public function generate() { $graph = []; $questions = []; foreach ( $this->context->blocks['yoast/faq-block'] as $block ) { if ( isset( $block['attrs']['questions'] ) ) { $questions = \array_merge( $questions, $block['attrs']['questions'] ); } } foreach ( $questions as $index => $question ) { if ( ! isset( $question['jsonAnswer'] ) || empty( $question['jsonAnswer'] ) ) { continue; } $graph[] = $this->generate_question_block( $question, ( $index + 1 ) ); } return $graph; } /** * Generate a Question piece. * * @param array $question The question to generate schema for. * @param int $position The position of the question. * * @return array Schema.org Question piece. */ protected function generate_question_block( $question, $position ) { $url = $this->context->canonical . '#' . \esc_attr( $question['id'] ); $data = [ '@type' => 'Question', '@id' => $url, 'position' => $position, 'url' => $url, 'name' => $this->helpers->schema->html->smart_strip_tags( $question['jsonQuestion'] ), 'answerCount' => 1, 'acceptedAnswer' => $this->add_accepted_answer_property( $question ), ]; return $this->helpers->schema->language->add_piece_language( $data ); } /** * Adds the Questions `acceptedAnswer` property. * * @param array $question The question to add the acceptedAnswer to. * * @return array Schema.org Question piece. */ protected function add_accepted_answer_property( $question ) { $data = [ '@type' => 'Answer', 'text' => $this->helpers->schema->html->sanitize( $question['jsonAnswer'] ), ]; return $this->helpers->schema->language->add_piece_language( $data ); } } open-graph-locale-generator.php000066600000012767151733470320012564 0ustar00 'ca_ES', 'en' => 'en_US', 'el' => 'el_GR', 'et' => 'et_EE', 'ja' => 'ja_JP', 'sq' => 'sq_AL', 'uk' => 'uk_UA', 'vi' => 'vi_VN', 'zh' => 'zh_CN', ]; if ( isset( $fix_locales[ $locale ] ) ) { return $fix_locales[ $locale ]; } // Convert locales like "es" to "es_ES", in case that works for the given locale (sometimes it does). if ( \strlen( $locale ) === 2 ) { $locale = \strtolower( $locale ) . '_' . \strtoupper( $locale ); } // These are the locales FB supports. $fb_valid_fb_locales = [ 'af_ZA', // Afrikaans. 'ak_GH', // Akan. 'am_ET', // Amharic. 'ar_AR', // Arabic. 'as_IN', // Assamese. 'ay_BO', // Aymara. 'az_AZ', // Azerbaijani. 'be_BY', // Belarusian. 'bg_BG', // Bulgarian. 'bp_IN', // Bhojpuri. 'bn_IN', // Bengali. 'br_FR', // Breton. 'bs_BA', // Bosnian. 'ca_ES', // Catalan. 'cb_IQ', // Sorani Kurdish. 'ck_US', // Cherokee. 'co_FR', // Corsican. 'cs_CZ', // Czech. 'cx_PH', // Cebuano. 'cy_GB', // Welsh. 'da_DK', // Danish. 'de_DE', // German. 'el_GR', // Greek. 'en_GB', // English (UK). 'en_PI', // English (Pirate). 'en_UD', // English (Upside Down). 'en_US', // English (US). 'em_ZM', 'eo_EO', // Esperanto. 'es_ES', // Spanish (Spain). 'es_LA', // Spanish. 'es_MX', // Spanish (Mexico). 'et_EE', // Estonian. 'eu_ES', // Basque. 'fa_IR', // Persian. 'fb_LT', // Leet Speak. 'ff_NG', // Fulah. 'fi_FI', // Finnish. 'fo_FO', // Faroese. 'fr_CA', // French (Canada). 'fr_FR', // French (France). 'fy_NL', // Frisian. 'ga_IE', // Irish. 'gl_ES', // Galician. 'gn_PY', // Guarani. 'gu_IN', // Gujarati. 'gx_GR', // Classical Greek. 'ha_NG', // Hausa. 'he_IL', // Hebrew. 'hi_IN', // Hindi. 'hr_HR', // Croatian. 'hu_HU', // Hungarian. 'ht_HT', // Haitian Creole. 'hy_AM', // Armenian. 'id_ID', // Indonesian. 'ig_NG', // Igbo. 'is_IS', // Icelandic. 'it_IT', // Italian. 'ik_US', 'iu_CA', 'ja_JP', // Japanese. 'ja_KS', // Japanese (Kansai). 'jv_ID', // Javanese. 'ka_GE', // Georgian. 'kk_KZ', // Kazakh. 'km_KH', // Khmer. 'kn_IN', // Kannada. 'ko_KR', // Korean. 'ks_IN', // Kashmiri. 'ku_TR', // Kurdish (Kurmanji). 'ky_KG', // Kyrgyz. 'la_VA', // Latin. 'lg_UG', // Ganda. 'li_NL', // Limburgish. 'ln_CD', // Lingala. 'lo_LA', // Lao. 'lt_LT', // Lithuanian. 'lv_LV', // Latvian. 'mg_MG', // Malagasy. 'mi_NZ', // Maori. 'mk_MK', // Macedonian. 'ml_IN', // Malayalam. 'mn_MN', // Mongolian. 'mr_IN', // Marathi. 'ms_MY', // Malay. 'mt_MT', // Maltese. 'my_MM', // Burmese. 'nb_NO', // Norwegian (bokmal). 'nd_ZW', // Ndebele. 'ne_NP', // Nepali. 'nl_BE', // Dutch (Belgie). 'nl_NL', // Dutch. 'nn_NO', // Norwegian (nynorsk). 'nr_ZA', // Southern Ndebele. 'ns_ZA', // Northern Sotho. 'ny_MW', // Chewa. 'om_ET', // Oromo. 'or_IN', // Oriya. 'pa_IN', // Punjabi. 'pl_PL', // Polish. 'ps_AF', // Pashto. 'pt_BR', // Portuguese (Brazil). 'pt_PT', // Portuguese (Portugal). 'qc_GT', // Quiché. 'qu_PE', // Quechua. 'qr_GR', 'qz_MM', // Burmese (Zawgyi). 'rm_CH', // Romansh. 'ro_RO', // Romanian. 'ru_RU', // Russian. 'rw_RW', // Kinyarwanda. 'sa_IN', // Sanskrit. 'sc_IT', // Sardinian. 'se_NO', // Northern Sami. 'si_LK', // Sinhala. 'su_ID', // Sundanese. 'sk_SK', // Slovak. 'sl_SI', // Slovenian. 'sn_ZW', // Shona. 'so_SO', // Somali. 'sq_AL', // Albanian. 'sr_RS', // Serbian. 'ss_SZ', // Swazi. 'st_ZA', // Southern Sotho. 'sv_SE', // Swedish. 'sw_KE', // Swahili. 'sy_SY', // Syriac. 'sz_PL', // Silesian. 'ta_IN', // Tamil. 'te_IN', // Telugu. 'tg_TJ', // Tajik. 'th_TH', // Thai. 'tk_TM', // Turkmen. 'tl_PH', // Filipino. 'tl_ST', // Klingon. 'tn_BW', // Tswana. 'tr_TR', // Turkish. 'ts_ZA', // Tsonga. 'tt_RU', // Tatar. 'tz_MA', // Tamazight. 'uk_UA', // Ukrainian. 'ur_PK', // Urdu. 'uz_UZ', // Uzbek. 've_ZA', // Venda. 'vi_VN', // Vietnamese. 'wo_SN', // Wolof. 'xh_ZA', // Xhosa. 'yi_DE', // Yiddish. 'yo_NG', // Yoruba. 'zh_CN', // Simplified Chinese (China). 'zh_HK', // Traditional Chinese (Hong Kong). 'zh_TW', // Traditional Chinese (Taiwan). 'zu_ZA', // Zulu. 'zz_TR', // Zazaki. ]; // Check to see if the locale is a valid FB one, if not, use en_US as a fallback. if ( \in_array( $locale, $fb_valid_fb_locales, true ) ) { return $locale; } $locale = \strtolower( \substr( $locale, 0, 2 ) ) . '_' . \strtoupper( \substr( $locale, 0, 2 ) ); if ( ! \in_array( $locale, $fb_valid_fb_locales, true ) ) { return 'en_US'; } return $locale; } } breadcrumbs-generator.php000066600000031167151733470320011553 0ustar00repository = $repository; $this->options = $options; $this->current_page_helper = $current_page_helper; $this->post_type_helper = $post_type_helper; $this->url_helper = $url_helper; $this->pagination_helper = $pagination_helper; } /** * Generates the breadcrumbs. * * @param Meta_Tags_Context $context The meta tags context. * * @return array> An array of associative arrays that each have a 'text' and a 'url'. */ public function generate( Meta_Tags_Context $context ) { $static_ancestors = []; $breadcrumbs_home = $this->options->get( 'breadcrumbs-home' ); if ( $breadcrumbs_home !== '' && ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) { $front_page_id = $this->current_page_helper->get_front_page_id(); if ( $front_page_id === 0 ) { $home_page_ancestor = $this->repository->find_for_home_page(); if ( \is_a( $home_page_ancestor, Indexable::class ) ) { $static_ancestors[] = $home_page_ancestor; } } else { $static_ancestor = $this->repository->find_by_id_and_type( $front_page_id, 'post' ); if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) { $static_ancestors[] = $static_ancestor; } } } $page_for_posts = \get_option( 'page_for_posts' ); if ( $this->should_have_blog_crumb( $page_for_posts, $context ) ) { $static_ancestor = $this->repository->find_by_id_and_type( $page_for_posts, 'post' ); if ( \is_a( $static_ancestor, Indexable::class ) && $static_ancestor->post_status !== 'unindexed' ) { $static_ancestors[] = $static_ancestor; } } if ( $context->indexable->object_type === 'post' && $context->indexable->object_sub_type !== 'post' && $context->indexable->object_sub_type !== 'page' && $this->post_type_helper->has_archive( $context->indexable->object_sub_type ) ) { $static_ancestor = $this->repository->find_for_post_type_archive( $context->indexable->object_sub_type ); if ( \is_a( $static_ancestor, Indexable::class ) ) { $static_ancestors[] = $static_ancestor; } } if ( $context->indexable->object_type === 'term' ) { $parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type ); if ( $parent && $parent !== 'post' && $this->post_type_helper->has_archive( $parent ) ) { $static_ancestor = $this->repository->find_for_post_type_archive( $parent ); if ( \is_a( $static_ancestor, Indexable::class ) ) { $static_ancestors[] = $static_ancestor; } } } $indexables = []; if ( ! \in_array( $this->current_page_helper->get_page_type(), [ 'Home_Page', 'Static_Home_Page' ], true ) ) { // Get all ancestors of the indexable and append itself to get all indexables in the full crumb. $indexables = $this->repository->get_ancestors( $context->indexable ); } $indexables[] = $context->indexable; if ( ! empty( $static_ancestors ) ) { \array_unshift( $indexables, ...$static_ancestors ); } $indexables = \apply_filters( 'wpseo_breadcrumb_indexables', $indexables, $context ); $indexables = \is_array( $indexables ) ? $indexables : []; $indexables = \array_filter( $indexables, static function ( $indexable ) { return \is_a( $indexable, Indexable::class ); } ); $crumbs = \array_map( [ $this, 'get_post_type_crumb' ], $indexables ); if ( $breadcrumbs_home !== '' ) { $crumbs[0]['text'] = $breadcrumbs_home; } $crumbs = $this->add_paged_crumb( $crumbs, $context->indexable ); /** * Filter: 'wpseo_breadcrumb_links' - Allow the developer to filter the Yoast SEO breadcrumb links, add to them, change order, etc. * * @param array $crumbs The crumbs array. */ $filtered_crumbs = \apply_filters( 'wpseo_breadcrumb_links', $crumbs ); // Basic check to make sure the filtered crumbs are in an array. if ( ! \is_array( $filtered_crumbs ) ) { \_doing_it_wrong( 'Filter: \'wpseo_breadcrumb_links\'', 'The `wpseo_breadcrumb_links` filter should return a multi-dimensional array.', 'YoastSEO v20.0' ); } else { $crumbs = $filtered_crumbs; } $filter_callback = static function ( $link_info, $index ) use ( $crumbs ) { /** * Filter: 'wpseo_breadcrumb_single_link_info' - Allow developers to filter the Yoast SEO Breadcrumb link information. * * @param array $link_info The breadcrumb link information. * @param int $index The index of the breadcrumb in the list. * @param array $crumbs The complete list of breadcrumbs. */ return \apply_filters( 'wpseo_breadcrumb_single_link_info', $link_info, $index, $crumbs ); }; return \array_map( $filter_callback, $crumbs, \array_keys( $crumbs ) ); } /** * Returns the modified post crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return array The crumb. */ private function get_post_crumb( $crumb, $ancestor ) { $crumb['id'] = $ancestor->object_id; return $crumb; } /** * Adds the correct ID to the crumb array based on the ancestor provided. * * @param Indexable $ancestor The ancestor indexable. * * @return string[] */ private function get_post_type_crumb( Indexable $ancestor ) { $crumb = [ 'url' => $ancestor->permalink, 'text' => $ancestor->breadcrumb_title, ]; switch ( $ancestor->object_type ) { case 'post': $crumb = $this->get_post_crumb( $crumb, $ancestor ); break; case 'post-type-archive': $crumb = $this->get_post_type_archive_crumb( $crumb, $ancestor ); break; case 'term': $crumb = $this->get_term_crumb( $crumb, $ancestor ); break; case 'system-page': $crumb = $this->get_system_page_crumb( $crumb, $ancestor ); break; case 'user': $crumb = $this->get_user_crumb( $crumb, $ancestor ); break; case 'date-archive': $crumb = $this->get_date_archive_crumb( $crumb ); break; default: // Handle unknown object types (optional). break; } return $crumb; } /** * Returns the modified post type crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return string[] The crumb. */ private function get_post_type_archive_crumb( $crumb, $ancestor ) { $crumb['ptarchive'] = $ancestor->object_sub_type; return $crumb; } /** * Returns the modified term crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return array The crumb. */ private function get_term_crumb( $crumb, $ancestor ) { $crumb['term_id'] = $ancestor->object_id; $crumb['taxonomy'] = $ancestor->object_sub_type; return $crumb; } /** * Returns the modified system page crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return string[] The crumb. */ private function get_system_page_crumb( $crumb, $ancestor ) { if ( $ancestor->object_sub_type === 'search-result' ) { $crumb['text'] = $this->options->get( 'breadcrumbs-searchprefix' ) . ' ' . \esc_html( \get_search_query() ); $crumb['url'] = \get_search_link(); } elseif ( $ancestor->object_sub_type === '404' ) { $crumb['text'] = $this->options->get( 'breadcrumbs-404crumb' ); } return $crumb; } /** * Returns the modified user crumb. * * @param string[] $crumb The crumb. * @param Indexable $ancestor The indexable. * * @return string[] The crumb. */ private function get_user_crumb( $crumb, $ancestor ) { $display_name = \get_the_author_meta( 'display_name', $ancestor->object_id ); $crumb['text'] = $this->options->get( 'breadcrumbs-archiveprefix' ) . ' ' . $display_name; return $crumb; } /** * Returns the modified date archive crumb. * * @param string[] $crumb The crumb. * * @return string[] The crumb. */ protected function get_date_archive_crumb( $crumb ) { $home_url = $this->url_helper->home(); $prefix = $this->options->get( 'breadcrumbs-archiveprefix' ); if ( \is_day() ) { $day = \esc_html( \get_the_date() ); $crumb['url'] = $home_url . \get_the_date( 'Y/m/d' ) . '/'; $crumb['text'] = $prefix . ' ' . $day; } elseif ( \is_month() ) { $month = \esc_html( \trim( \single_month_title( ' ', false ) ) ); $crumb['url'] = $home_url . \get_the_date( 'Y/m' ) . '/'; $crumb['text'] = $prefix . ' ' . $month; } elseif ( \is_year() ) { $year = \get_the_date( 'Y' ); $crumb['url'] = $home_url . $year . '/'; $crumb['text'] = $prefix . ' ' . $year; } return $crumb; } /** * Returns whether or not a blog crumb should be added. * * @param int $page_for_posts The page for posts ID. * @param Meta_Tags_Context $context The meta tags context. * * @return bool Whether or not a blog crumb should be added. */ protected function should_have_blog_crumb( $page_for_posts, $context ) { // When there is no page configured as blog page. if ( \get_option( 'show_on_front' ) !== 'page' || ! $page_for_posts ) { return false; } if ( $context->indexable->object_type === 'term' ) { $parent = $this->get_taxonomy_post_type_parent( $context->indexable->object_sub_type ); return $parent === 'post'; } if ( $this->options->get( 'breadcrumbs-display-blog-page' ) !== true ) { return false; } // When the current page is the home page, searchpage or isn't a singular post. if ( \is_home() || \is_search() || ! \is_singular( 'post' ) ) { return false; } return true; } /** * Returns the post type parent of a given taxonomy. * * @param string $taxonomy The taxonomy. * * @return string|false The parent if it exists, false otherwise. */ protected function get_taxonomy_post_type_parent( $taxonomy ) { $parent = $this->options->get( 'taxonomy-' . $taxonomy . '-ptparent' ); if ( empty( $parent ) || (string) $parent === '0' ) { return false; } return $parent; } /** * Adds a crumb for the current page, if we're on an archive page or paginated post. * * @param string[] $crumbs The array of breadcrumbs. * @param Indexable $current_indexable The current indexable. * * @return string[] The breadcrumbs. */ protected function add_paged_crumb( array $crumbs, $current_indexable ) { $is_simple_page = $this->current_page_helper->is_simple_page(); // If we're not on a paged page do nothing. if ( ! $is_simple_page && ! $this->current_page_helper->is_paged() ) { return $crumbs; } // If we're not on a paginated post do nothing. if ( $is_simple_page && $current_indexable->number_of_pages === null ) { return $crumbs; } $current_page_number = $this->pagination_helper->get_current_page_number(); if ( $current_page_number <= 1 ) { return $crumbs; } $crumbs[] = [ 'text' => \sprintf( /* translators: %s expands to the current page number */ \__( 'Page %s', 'wordpress-seo' ), $current_page_number ), ]; return $crumbs; } } schema-generator.php000066600000032630151733470320010516 0ustar00helpers = $helpers; $this->schema_replace_vars_helper = $schema_replace_vars_helper; } /** * Returns a Schema graph array. * * @param Meta_Tags_Context $context The meta tags context. * * @return array The graph. */ public function generate( Meta_Tags_Context $context ) { $pieces = $this->get_graph_pieces( $context ); $this->schema_replace_vars_helper->register_replace_vars( $context ); foreach ( \array_keys( $context->blocks ) as $block_type ) { /** * Filter: 'wpseo_pre_schema_block_type_' - Allows hooking things to change graph output based on the blocks on the page. * * @param WP_Block_Parser_Block[] $blocks All the blocks of this block type. * @param Meta_Tags_Context $context A value object with context variables. */ \do_action( 'wpseo_pre_schema_block_type_' . $block_type, $context->blocks[ $block_type ], $context ); } // Do a loop before everything else to inject the context and helpers. foreach ( $pieces as $piece ) { if ( \is_a( $piece, Abstract_Schema_Piece::class ) ) { $piece->context = $context; $piece->helpers = $this->helpers; } } $pieces_to_generate = $this->filter_graph_pieces_to_generate( $pieces ); $graph = $this->generate_graph( $pieces_to_generate, $context ); $graph = $this->add_schema_blocks_graph_pieces( $graph, $context ); $graph = $this->finalize_graph( $graph, $context ); return [ '@context' => 'https://schema.org', '@graph' => $graph, ]; } /** * Filters out any graph pieces that should not be generated. * (Using the `wpseo_schema_needs_` series of filters). * * @param array $graph_pieces The current list of graph pieces that we want to generate. * * @return array The graph pieces to generate. */ protected function filter_graph_pieces_to_generate( $graph_pieces ) { $pieces_to_generate = []; foreach ( $graph_pieces as $piece ) { $identifier = \strtolower( \str_replace( 'Yoast\WP\SEO\Generators\Schema\\', '', \get_class( $piece ) ) ); if ( isset( $piece->identifier ) ) { $identifier = $piece->identifier; } /** * Filter: 'wpseo_schema_needs_' - Allows changing which graph pieces we output. * * @param bool $is_needed Whether or not to show a graph piece. */ $is_needed = \apply_filters( 'wpseo_schema_needs_' . $identifier, $piece->is_needed() ); if ( ! $is_needed ) { continue; } $pieces_to_generate[ $identifier ] = $piece; } return $pieces_to_generate; } /** * Generates the schema graph. * * @param array $graph_piece_generators The schema graph pieces to generate. * @param Meta_Tags_Context $context The meta tags context to use. * * @return array The generated schema graph. */ protected function generate_graph( $graph_piece_generators, $context ) { $graph = []; foreach ( $graph_piece_generators as $identifier => $graph_piece_generator ) { $graph_pieces = $graph_piece_generator->generate(); // If only a single graph piece was returned. if ( $graph_pieces !== false && \array_key_exists( '@type', $graph_pieces ) ) { $graph_pieces = [ $graph_pieces ]; } if ( ! \is_array( $graph_pieces ) ) { continue; } foreach ( $graph_pieces as $graph_piece ) { /** * Filter: 'wpseo_schema_' - Allows changing graph piece output. * This filter can be called with either an identifier or a block type (see `add_schema_blocks_graph_pieces()`). * * @param array $graph_piece The graph piece to filter. * @param Meta_Tags_Context $context A value object with context variables. * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables. * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables. */ $graph_piece = \apply_filters( 'wpseo_schema_' . $identifier, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators ); $graph_piece = $this->type_filter( $graph_piece, $identifier, $context, $graph_piece_generator, $graph_piece_generators ); $graph_piece = $this->validate_type( $graph_piece ); if ( \is_array( $graph_piece ) ) { $graph[] = $graph_piece; } } } /** * Filter: 'wpseo_schema_graph' - Allows changing graph output. * * @param array $graph The graph to filter. * @param Meta_Tags_Context $context A value object with context variables. */ $graph = \apply_filters( 'wpseo_schema_graph', $graph, $context ); return $graph; } /** * Adds schema graph pieces from Gutenberg blocks on the current page to * the given schema graph. * * Think of blocks like the Yoast FAQ block or the How To block. * * @param array $graph The current schema graph. * @param Meta_Tags_Context $context The meta tags context. * * @return array The graph with the schema blocks graph pieces added. */ protected function add_schema_blocks_graph_pieces( $graph, $context ) { foreach ( $context->blocks as $block_type => $blocks ) { foreach ( $blocks as $block ) { $block_type = \strtolower( $block['blockName'] ); /** * Filter: 'wpseo_schema_block_'. * This filter is documented in the `generate_graph()` function in this class. */ $graph = \apply_filters( 'wpseo_schema_block_' . $block_type, $graph, $block, $context ); if ( isset( $block['attrs']['yoast-schema'] ) ) { $graph[] = $this->schema_replace_vars_helper->replace( $block['attrs']['yoast-schema'], $context->presentation ); } } } return $graph; } /** * Finalizes the schema graph after all filtering is done. * * @param array $graph The current schema graph. * @param Meta_Tags_Context $context The meta tags context. * * @return array The schema graph. */ protected function finalize_graph( $graph, $context ) { $graph = $this->remove_empty_breadcrumb( $graph, $context ); return $graph; } /** * Removes the breadcrumb schema if empty. * * @param array $graph The current schema graph. * @param Meta_Tags_Context $context The meta tags context. * * @return array The schema graph with empty breadcrumbs taken out. */ protected function remove_empty_breadcrumb( $graph, $context ) { if ( $this->helpers->current_page->is_home_static_page() || $this->helpers->current_page->is_home_posts_page() ) { return $graph; } // Remove the breadcrumb piece, if it's empty. $index_to_remove = 0; foreach ( $graph as $key => $piece ) { if ( \in_array( 'BreadcrumbList', $this->get_type_from_piece( $piece ), true ) ) { if ( isset( $piece['itemListElement'] ) && \is_array( $piece['itemListElement'] ) && \count( $piece['itemListElement'] ) === 1 ) { $index_to_remove = $key; break; } } } // If the breadcrumb piece has been removed, we should remove its reference from the WebPage node. if ( $index_to_remove !== 0 ) { \array_splice( $graph, $index_to_remove, 1 ); // Get the type of the WebPage node. $webpage_types = \is_array( $context->schema_page_type ) ? $context->schema_page_type : [ $context->schema_page_type ]; foreach ( $graph as $key => $piece ) { if ( ! empty( \array_intersect( $webpage_types, $this->get_type_from_piece( $piece ) ) ) && isset( $piece['breadcrumb'] ) ) { unset( $piece['breadcrumb'] ); $graph[ $key ] = $piece; } } } return $graph; } /** * Adapts the WebPage graph piece for password-protected posts. * * It should only have certain whitelisted properties. * The type should always be WebPage. * * @param array $graph_piece The WebPage graph piece that should be adapted for password-protected posts. * * @return array The WebPage graph piece that has been adapted for password-protected posts. */ public function protected_webpage_schema( $graph_piece ) { $properties_to_show = \array_flip( [ '@type', '@id', 'url', 'name', 'isPartOf', 'inLanguage', 'datePublished', 'dateModified', 'breadcrumb', ] ); $graph_piece = \array_intersect_key( $graph_piece, $properties_to_show ); $graph_piece['@type'] = 'WebPage'; return $graph_piece; } /** * Gets all the graph pieces we need. * * @param Meta_Tags_Context $context The meta tags context. * * @return Abstract_Schema_Piece[] A filtered array of graph pieces. */ protected function get_graph_pieces( $context ) { if ( $context->indexable->object_type === 'post' && \post_password_required( $context->post ) ) { $schema_pieces = [ new Schema\WebPage(), new Schema\Website(), new Schema\Organization(), ]; \add_filter( 'wpseo_schema_webpage', [ $this, 'protected_webpage_schema' ], 1 ); } else { $schema_pieces = [ new Schema\Article(), new Schema\WebPage(), new Schema\Main_Image(), new Schema\Breadcrumb(), new Schema\Website(), new Schema\Organization(), new Schema\Person(), new Schema\Author(), new Schema\FAQ(), new Schema\HowTo(), ]; } /** * Filter: 'wpseo_schema_graph_pieces' - Allows adding pieces to the graph. * * @param array $pieces The schema pieces. * @param Meta_Tags_Context $context An object with context variables. */ return \apply_filters( 'wpseo_schema_graph_pieces', $schema_pieces, $context ); } /** * Allows filtering the graph piece by its schema type. * * Note: We removed the Abstract_Schema_Piece type-hint from the $graph_piece_generator argument, because * it caused conflicts with old code, Yoast SEO Video specifically. * * @param array $graph_piece The graph piece we're filtering. * @param string $identifier The identifier of the graph piece that is being filtered. * @param Meta_Tags_Context $context The meta tags context. * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables. * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables. * * @return array The filtered graph piece. */ private function type_filter( $graph_piece, $identifier, Meta_Tags_Context $context, $graph_piece_generator, array $graph_piece_generators ) { $types = $this->get_type_from_piece( $graph_piece ); foreach ( $types as $type ) { $type = \strtolower( $type ); // Prevent running the same filter twice. This makes sure we run f/i. for 'author' and for 'person'. if ( $type && $type !== $identifier ) { /** * Filter: 'wpseo_schema_' - Allows changing graph piece output by @type. * * @param array $graph_piece The graph piece to filter. * @param Meta_Tags_Context $context A value object with context variables. * @param Abstract_Schema_Piece $graph_piece_generator A value object with context variables. * @param Abstract_Schema_Piece[] $graph_piece_generators A value object with context variables. */ $graph_piece = \apply_filters( 'wpseo_schema_' . $type, $graph_piece, $context, $graph_piece_generator, $graph_piece_generators ); } } return $graph_piece; } /** * Retrieves the type from a graph piece. * * @param array $piece The graph piece. * * @return array An array of the piece's types. */ private function get_type_from_piece( $piece ) { if ( isset( $piece['@type'] ) ) { if ( \is_array( $piece['@type'] ) ) { // Return as-is, but remove unusable values, like sub-arrays, objects, null. return \array_filter( $piece['@type'], 'is_string' ); } return [ $piece['@type'] ]; } return []; } /** * Validates a graph piece's type. * * When the type is an array: * - Ensure the values are unique. * - Only 1 value? Use that value without the array wrapping. * * @param array $piece The graph piece. * * @return array The graph piece. */ private function validate_type( $piece ) { if ( ! isset( $piece['@type'] ) ) { // No type to validate. return $piece; } // If it is not an array, we can return immediately. if ( ! \is_array( $piece['@type'] ) ) { return $piece; } /* * Ensure the types are unique. * Use array_values to reset the indices (e.g. no 0, 2 because 1 was a duplicate). */ $piece['@type'] = \array_values( \array_unique( $piece['@type'] ) ); // Use the first value if there is only 1 type. if ( \count( $piece['@type'] ) === 1 ) { $piece['@type'] = \reset( $piece['@type'] ); } return $piece; } } twitter-image-generator.php000066600000004735151733470320012045 0ustar00image = $image; $this->url = $url; $this->twitter_image = $twitter_image; } /** * Retrieves the images for an indexable. * * @param Meta_Tags_Context $context The context. * * @return array> The images. */ public function generate( Meta_Tags_Context $context ) { $image_container = $this->get_image_container(); $this->add_from_indexable( $context->indexable, $image_container ); return $image_container->get_images(); } /** * Adds an image based on the given indexable. * * @param Indexable $indexable The indexable. * @param Images $image_container The image container. * * @return void */ protected function add_from_indexable( Indexable $indexable, Images $image_container ) { if ( $indexable->twitter_image_id ) { $image_container->add_image_by_id( $indexable->twitter_image_id ); return; } if ( $indexable->twitter_image ) { $image_container->add_image_by_url( $indexable->twitter_image ); } } /** * Retrieves an instance of the image container. * * @codeCoverageIgnore * * @return Images The image container. */ protected function get_image_container() { $image_container = new Images( $this->image, $this->url ); $image_container->image_size = $this->twitter_image->get_image_size(); $image_container->set_helpers( $this->twitter_image ); return $image_container; } }