PluginProbe
SEO Engine – Smart SEO with AI, Schema & Redirection for WordPress / trunk
SEO Engine – Smart SEO with AI, Schema & Redirection for WordPress vtrunk
0.9.4 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.2 0.8.1 0.7.9 0.8.0 0.7.7 0.7.8 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.5 All 88 releases
seo-engine / classes / modules / schema.php

schema.php in SEO Engine – Smart SEO with AI, Schema & Redirection for WordPress trunk, at classes/modules/schema.php

340 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class Meow_MWSEO_Modules_Schema
4 {
5 private $core = null;
6 private $schema_enabled = false;
7
8 public function __construct( $core )
9 {
10 $this->core = $core;
11 $this->init();
12 }
13
14 public function init()
15 {
16 $this->schema_enabled = $this->core->get_option( 'auto_schema_enabled', true );
17
18 if ( $this->schema_enabled ) {
19 if ( !is_admin() ) {
20 add_action( 'wp_head', array( $this, 'inject_schema_markup' ), 5 );
21 }
22 // Let the scoring system know we're generating schema
23 add_filter( 'seo_engine_has_schema', array( $this, 'confirm_schema_present' ), 10, 2 );
24 }
25 }
26
27 /**
28 * Confirm that schema is present for scoring system
29 */
30 public function confirm_schema_present( $has_schema, $post )
31 {
32 // If schema module is enabled and we have a valid post, we're generating schema
33 if ( $post && is_object( $post ) && isset( $post->ID ) ) {
34 return true;
35 }
36 return $has_schema;
37 }
38
39 /**
40 * Inject JSON-LD schema markup into wp_head
41 */
42 public function inject_schema_markup()
43 {
44 // Only run on singular posts
45 if ( !is_singular() ) {
46 return;
47 }
48
49 // Step back when another SEO plugin is handling the frontend output, so we don't
50 // emit a second JSON-LD block alongside theirs.
51 if ( !$this->core->should_render_frontend_meta() ) {
52 return;
53 }
54
55 global $post;
56 if ( !$post ) {
57 return;
58 }
59
60 $schema = $this->generate_schema( $post );
61 if ( $schema ) {
62 echo '<script type="application/ld+json" class="mwseo-schema">' . "\n";
63 echo wp_json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT );
64 echo "\n" . '</script>' . "\n";
65 }
66 }
67
68 /**
69 * Generate schema markup for a post
70 *
71 * @param WP_Post $post The post object
72 * @return array|null Schema array or null if not applicable
73 */
74 private function generate_schema( $post )
75 {
76 // Get schema type based on post type
77 $schema_by_post_type = $this->core->get_option( 'schema_by_post_type', array(
78 'post' => 'Article',
79 'page' => 'WebPage',
80 'product' => 'Product',
81 ) );
82
83 $post_type = get_post_type( $post );
84 $schema_type = isset( $schema_by_post_type[ $post_type ] ) ? $schema_by_post_type[ $post_type ] : 'Article';
85
86 // Generate appropriate schema based on type
87 switch ( $schema_type ) {
88 case 'Product':
89 return $this->generate_product_schema( $post );
90 case 'WebPage':
91 return $this->generate_webpage_schema( $post );
92 case 'Article':
93 default:
94 return $this->generate_article_schema( $post );
95 }
96 }
97
98 /**
99 * Generate Article schema
100 */
101 private function generate_article_schema( $post )
102 {
103 $schema = array(
104 '@context' => 'https://schema.org',
105 '@type' => 'Article',
106 );
107
108 // Headline (required)
109 $schema['headline'] = get_the_title( $post );
110
111 // Description
112 $excerpt = $this->core->build_excerpt( $post );
113 if ( !empty( $excerpt ) ) {
114 $schema['description'] = wp_strip_all_tags( $excerpt );
115 }
116
117 // DatePublished (required)
118 $schema['datePublished'] = get_the_date( 'c', $post );
119
120 // DateModified (required)
121 $schema['dateModified'] = get_the_modified_date( 'c', $post );
122
123 // Author (required)
124 $author = get_the_author_meta( 'display_name', $post->post_author );
125 if ( !empty( $author ) ) {
126 $schema['author'] = array(
127 '@type' => 'Person',
128 'name' => $author,
129 'url' => get_author_posts_url( $post->post_author )
130 );
131 }
132
133 // Publisher (required for Article)
134 $site_name = get_bloginfo( 'name' );
135 $site_logo = get_site_icon_url();
136
137 $publisher = array(
138 '@type' => 'Organization',
139 'name' => $site_name,
140 'url' => home_url(),
141 );
142
143 if ( !empty( $site_logo ) ) {
144 $publisher['logo'] = array(
145 '@type' => 'ImageObject',
146 'url' => $site_logo,
147 );
148 }
149
150 $schema['publisher'] = $publisher;
151
152 // Image (recommended)
153 $featured_image_id = get_post_thumbnail_id( $post );
154 if ( $featured_image_id ) {
155 $image_url = get_the_post_thumbnail_url( $post, 'large' );
156 if ( $image_url ) {
157 $schema['image'] = $image_url;
158 }
159 }
160
161 // MainEntityOfPage
162 $schema['mainEntityOfPage'] = array(
163 '@type' => 'WebPage',
164 '@id' => get_permalink( $post ),
165 );
166
167 return apply_filters( 'mwseo_schema_article', $schema, $post );
168 }
169
170 /**
171 * Generate WebPage schema
172 */
173 private function generate_webpage_schema( $post )
174 {
175 $schema = array(
176 '@context' => 'https://schema.org',
177 '@type' => 'WebPage',
178 );
179
180 // Name (title)
181 $schema['name'] = get_the_title( $post );
182
183 // Description
184 $excerpt = $this->core->build_excerpt( $post );
185 if ( !empty( $excerpt ) ) {
186 $schema['description'] = wp_strip_all_tags( $excerpt );
187 }
188
189 // URL
190 $schema['url'] = get_permalink( $post );
191
192 // DatePublished
193 $schema['datePublished'] = get_the_date( 'c', $post );
194
195 // DateModified
196 $schema['dateModified'] = get_the_modified_date( 'c', $post );
197
198 // Image
199 $featured_image_id = get_post_thumbnail_id( $post );
200 if ( $featured_image_id ) {
201 $image_url = get_the_post_thumbnail_url( $post, 'large' );
202 if ( $image_url ) {
203 $schema['image'] = $image_url;
204 }
205 }
206
207 return apply_filters( 'mwseo_schema_webpage', $schema, $post );
208 }
209
210 /**
211 * Generate Product schema (for WooCommerce or similar)
212 */
213 private function generate_product_schema( $post )
214 {
215 // Check if WooCommerce is active and this is a product
216 if ( !function_exists( 'wc_get_product' ) ) {
217 return $this->generate_article_schema( $post );
218 }
219
220 $product = wc_get_product( $post->ID );
221 if ( !$product ) {
222 return $this->generate_article_schema( $post );
223 }
224
225 $schema = array(
226 '@context' => 'https://schema.org',
227 '@type' => 'Product',
228 );
229
230 // Name (required)
231 $schema['name'] = $product->get_name();
232
233 // Description
234 $description = $product->get_short_description();
235 if ( empty( $description ) ) {
236 $description = $product->get_description();
237 }
238 if ( !empty( $description ) ) {
239 $schema['description'] = wp_strip_all_tags( $description );
240 }
241
242 // Image
243 $image_id = $product->get_image_id();
244 if ( $image_id ) {
245 $image_url = wp_get_attachment_url( $image_id );
246 if ( $image_url ) {
247 $schema['image'] = $image_url;
248 }
249 }
250
251 // SKU
252 $sku = $product->get_sku();
253 if ( !empty( $sku ) ) {
254 $schema['sku'] = $sku;
255 }
256
257 // Offers (required for Product)
258 $schema['offers'] = array(
259 '@type' => 'Offer',
260 'url' => get_permalink( $post ),
261 'priceCurrency' => get_woocommerce_currency(),
262 'price' => $product->get_price(),
263 'availability' => $product->is_in_stock() ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
264 );
265
266 // AggregateRating (required by Google for review snippets)
267 $average_rating = $product->get_average_rating();
268 $review_count = $product->get_review_count();
269 if ( $average_rating > 0 && $review_count > 0 ) {
270 $schema['aggregateRating'] = array(
271 '@type' => 'AggregateRating',
272 'ratingValue' => floatval( $average_rating ),
273 'reviewCount' => intval( $review_count ),
274 'bestRating' => 5,
275 'worstRating' => 1,
276 );
277 }
278
279 // Reviews (required by Google for review snippets)
280 $reviews = $this->get_product_reviews( $post->ID );
281 if ( !empty( $reviews ) ) {
282 $schema['review'] = $reviews;
283 }
284
285 return apply_filters( 'mwseo_schema_product', $schema, $post, $product );
286 }
287
288 private function get_product_reviews( $product_id, $limit = 10 )
289 {
290 $reviews = array();
291
292 $comments = get_comments( array(
293 'post_id' => $product_id,
294 'status' => 'approve',
295 'type' => 'review',
296 'number' => $limit,
297 'orderby' => 'comment_date_gmt',
298 'order' => 'DESC',
299 ) );
300
301 if ( empty( $comments ) ) {
302 return $reviews;
303 }
304
305 foreach ( $comments as $comment ) {
306 $rating = get_comment_meta( $comment->comment_ID, 'rating', true );
307
308 // Skip reviews without a rating
309 if ( empty( $rating ) ) {
310 continue;
311 }
312
313 $review = array(
314 '@type' => 'Review',
315 'reviewRating' => array(
316 '@type' => 'Rating',
317 'ratingValue' => intval( $rating ),
318 'bestRating' => 5,
319 'worstRating' => 1,
320 ),
321 'author' => array(
322 '@type' => 'Person',
323 'name' => $comment->comment_author,
324 ),
325 'datePublished' => get_comment_date( 'c', $comment ),
326 );
327
328 // Add review body if available
329 $review_body = wp_strip_all_tags( $comment->comment_content );
330 if ( !empty( $review_body ) ) {
331 $review['reviewBody'] = $review_body;
332 }
333
334 $reviews[] = $review;
335 }
336
337 return $reviews;
338 }
339 }
340