<?php /** * Plugin Name: Hausjournal Structured Data * Description: Yoast-Schema erweitern: Organization, BlogPosting, Kategorien, Autoren, Trust-Seiten. */ declare(strict_types=1); const HJ_SCHEMA_CATEGORY_ITEMLIST_LIMIT = 8; add_filter('wpseo_schema_article_type', 'hj_schema_article_type', 20, 2); add_filter('wpseo_schema_webpage', 'hj_schema_webpage', 11, 2); add_filter('wpseo_schema_webpage_type', 'hj_schema_webpage_type', 11, 1); add_filter('wpseo_schema_article', 'hj_schema_article', 11, 2); add_filter('wpseo_schema_graph', 'hj_schema_enhance_graph', 11, 2); /** * @param string|array<string> $type * @return string|array<string> */ function hj_schema_article_type($type, $indexable) { if (!is_object($indexable) || ($indexable->object_type ?? '') !== 'post' || ($indexable->object_sub_type ?? '') !== 'post') { return $type; } if ($type === 'None' || $type === 'Article') { return 'BlogPosting'; } if (is_array($type) && in_array('None', $type, true)) { return 'BlogPosting'; } return $type; } /** * @param array<string, mixed> $data * @return array<string, mixed> */ function hj_schema_webpage(array $data, $context): array { if (!is_object($context) || empty($context->indexable)) { return $data; } $object_type = (string) ($context->indexable->object_type ?? ''); $object_sub_type = (string) ($context->indexable->object_sub_type ?? ''); if ($object_type === 'post' && $object_sub_type === 'post' && !empty($context->canonical)) { $data['mainEntity'] = ['@id' => $context->canonical . '#article']; } if ($object_type === 'term' && $object_sub_type === 'category') { $description = hj_schema_resolve_description(); if ($description !== '') { $data['description'] = $description; } } if ($object_type === 'user') { $description = hj_schema_resolve_description(); if ($description !== '') { $data['description'] = $description; } } if ($object_type === 'post' && $object_sub_type === 'page') { $slug = (string) get_post_field('post_name', (int) $context->indexable->object_id); $org_id = hj_schema_organization_id(); if (in_array($slug, ['ueber-uns', 'redaktionsprozess'], true)) { $description = hj_schema_resolve_description(); if ($description !== '') { $data['description'] = $description; } $data['about'] = ['@id' => $org_id]; $data['publisher'] = ['@id' => $org_id]; } if ($slug === 'redaktionsprozess') { $data['specialty'] = 'Bau- und Heimwerkerjournalismus mit redaktioneller Qualitaetssicherung'; } } return $data; } /** * @param string|array<string> $type * @return string|array<string> */ function hj_schema_webpage_type($type) { if (is_page()) { $slug = (string) get_post_field('post_name', get_queried_object_id()); if (in_array($slug, ['ueber-uns', 'redaktionsprozess'], true)) { return ['WebPage', 'AboutPage']; } } if (is_author()) { return ['ProfilePage', 'WebPage']; } return $type; } /** * @param array<string, mixed> $data * @return array<string, mixed> */ function hj_schema_article(array $data, $context): array { if (empty($data) || !is_object($context) || empty($context->post) || !($context->post instanceof WP_Post)) { return $data; } if (empty($data['description'])) { $description = hj_schema_post_description($context->post); if ($description !== '') { $data['description'] = $description; } } return $data; } /** * @param array<int, array<string, mixed>> $graph * @return array<int, array<string, mixed>> */ function hj_schema_enhance_graph(array $graph, $context): array { $org_id = hj_schema_organization_id(); foreach ($graph as $index => $piece) { if (($piece['@type'] ?? '') === 'WebSite' && empty($piece['publisher'])) { $graph[$index]['publisher'] = ['@id' => $org_id]; } } if (is_category()) { return hj_schema_enhance_category_graph($graph, $context); } if (is_author()) { return hj_schema_enhance_author_graph($graph, $context); } return $graph; } /** * @param array<int, array<string, mixed>> $graph * @return array<int, array<string, mixed>> */ function hj_schema_enhance_category_graph(array $graph, $context): array { $term = get_queried_object(); if (!$term instanceof WP_Term || empty($context->canonical)) { return $graph; } $canonical = (string) $context->canonical; $image = hj_schema_category_image($term); if ($image !== null) { $graph = hj_schema_replace_primary_image($graph, $canonical, $image); } $description = hj_schema_resolve_description(); if ($description !== '') { $graph = hj_schema_set_piece_property($graph, $canonical, 'description', $description); } $item_list = hj_schema_build_category_item_list($term, $canonical); if ($item_list !== null) { $graph[] = $item_list; $graph = hj_schema_set_piece_property($graph, $canonical, 'mainEntity', ['@id' => $canonical . '#itemlist']); } return $graph; } /** * @param array<int, array<string, mixed>> $graph * @return array<int, array<string, mixed>> */ function hj_schema_enhance_author_graph(array $graph, $context): array { $author = get_queried_object(); if (!$author instanceof WP_User || empty($context->canonical)) { return $graph; } $canonical = (string) $context->canonical; $description = hj_schema_resolve_description(); if ($description !== '') { $graph = hj_schema_set_piece_property($graph, $canonical, 'description', $description); } $fachgebiet = hj_schema_clean_text((string) get_user_meta($author->ID, 'fachgebiet', true)); if ($fachgebiet !== '') { foreach ($graph as $index => $piece) { $type = $piece['@type'] ?? ''; if ($type === 'Person' || (is_array($type) && in_array('Person', $type, true))) { $graph[$index]['jobTitle'] = 'Autor'; $graph[$index]['knowsAbout'] = [$fachgebiet]; if ($description !== '' && empty($graph[$index]['description'])) { $graph[$index]['description'] = $description; } } } } return $graph; } function hj_schema_organization_id(): string { return trailingslashit(home_url('/')) . '#organization'; } function hj_schema_resolve_description(): string { if (function_exists('hj_seo_resolve_metadesc')) { return hj_schema_clean_text((string) hj_seo_resolve_metadesc()); } return ''; } function hj_schema_post_description(WP_Post $post): string { $description = ''; if (function_exists('YoastSEO')) { $presentation = YoastSEO()->meta->for_post($post->ID); if (isset($presentation->open_graph_description)) { $description = (string) $presentation->open_graph_description; } if ($description === '' && isset($presentation->meta_description)) { $description = (string) $presentation->meta_description; } } if ($description === '') { $description = get_the_excerpt($post); } return hj_schema_clean_text($description); } /** * @return array<string, mixed>|null */ function hj_schema_category_image(WP_Term $term): ?array { if (class_exists('WPSEO_Taxonomy_Meta')) { $meta = WPSEO_Taxonomy_Meta::get_term_meta((int) $term->term_id, 'category'); $og_image_id = (int) ($meta['wpseo_opengraph-image-id'] ?? 0); if ($og_image_id > 0) { $image = hj_schema_image_from_attachment($og_image_id, $term->name); if ($image !== null) { return $image; } } } $posts = get_posts([ 'cat' => (int) $term->term_id, 'posts_per_page' => 12, 'orderby' => 'date', 'order' => 'DESC', 'post_type' => 'post', 'post_status' => 'publish', 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ]); foreach ($posts as $post_id) { $post_id = (int) $post_id; if (!has_post_thumbnail($post_id)) { continue; } $image = hj_schema_image_from_attachment((int) get_post_thumbnail_id($post_id), $term->name); if ($image !== null) { return $image; } } return null; } /** * @return array<string, mixed>|null */ function hj_schema_image_from_attachment(int $attachment_id, string $caption): ?array { if ($attachment_id < 1) { return null; } $url = wp_get_attachment_image_url($attachment_id, 'full'); if (!is_string($url) || $url === '') { return null; } $meta = wp_get_attachment_metadata($attachment_id); $width = is_array($meta) ? (int) ($meta['width'] ?? 0) : 0; $height = is_array($meta) ? (int) ($meta['height'] ?? 0) : 0; return [ 'url' => $url, 'contentUrl' => $url, 'width' => $width > 0 ? $width : null, 'height' => $height > 0 ? $height : null, 'caption' => hj_schema_clean_text($caption), ]; } /** * @param array<int, array<string, mixed>> $graph * @param array<string, mixed> $image * @return array<int, array<string, mixed>> */ function hj_schema_replace_primary_image(array $graph, string $canonical, array $image): array { $image_id = $canonical . '#primaryimage'; foreach ($graph as $index => $piece) { $piece_id = (string) ($piece['@id'] ?? ''); if ($piece_id === $canonical) { $graph[$index]['thumbnailUrl'] = $image['url']; continue; } if ($piece_id !== $image_id) { continue; } $graph[$index]['url'] = $image['url']; $graph[$index]['contentUrl'] = $image['contentUrl']; if (!empty($image['width'])) { $graph[$index]['width'] = $image['width']; } if (!empty($image['height'])) { $graph[$index]['height'] = $image['height']; } if (!empty($image['caption'])) { $graph[$index]['caption'] = $image['caption']; } } return $graph; } /** * @param array<int, array<string, mixed>> $graph * @param mixed $value * @return array<int, array<string, mixed>> */ function hj_schema_set_piece_property(array $graph, string $piece_id, string $property, $value): array { foreach ($graph as $index => $piece) { if (($piece['@id'] ?? '') === $piece_id) { $graph[$index][$property] = $value; } } return $graph; } /** * @return array<string, mixed>|null */ function hj_schema_build_category_item_list(WP_Term $term, string $canonical): ?array { $posts = get_posts([ 'cat' => (int) $term->term_id, 'posts_per_page' => HJ_SCHEMA_CATEGORY_ITEMLIST_LIMIT, 'orderby' => 'date', 'order' => 'DESC', 'post_type' => 'post', 'post_status' => 'publish', 'no_found_rows' => true, ]); if ($posts === []) { return null; } $elements = []; $position = 1; foreach ($posts as $post) { if (!$post instanceof WP_Post) { continue; } $name = hj_schema_clean_text(get_the_title($post)); $url = get_permalink($post); if ($name === '' || !is_string($url) || $url === '') { continue; } $elements[] = [ '@type' => 'ListItem', 'position' => $position, 'name' => $name, 'url' => $url, ]; ++$position; } if ($elements === []) { return null; } return [ '@type' => 'ItemList', '@id' => $canonical . '#itemlist', 'name' => hj_schema_clean_text($term->name), 'itemListOrder' => 'https://schema.org/ItemListOrderDescending', 'numberOfItems' => count($elements), 'itemListElement' => $elements, ]; } function hj_schema_clean_text(string $text): string { if (function_exists('hj_seo_clean_text')) { return hj_seo_clean_text($text); } $charset = get_bloginfo('charset') ?: 'UTF-8'; $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, $charset); $text = wp_strip_all_tags($text, true); $text = preg_replace('/\s+/u', ' ', $text) ?? ''; return trim($text); } Wanddurchführung im Keller abdichten » So geht's richtig