PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/seo-tools/class-jetpack-seo-posts.php +279 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,279 @@
1 +<?php
2 +/**
3 + * Class containing utility static methods for managing SEO options for Posts and Pages.
4 + *
5 + * @package automattic/jetpack
6 + */
7 +
8 +/**
9 + * Provides static utility methods for managing SEO options for Posts and Pages.
10 + */
11 +class Jetpack_SEO_Posts {
12 + /**
13 + * Key of the post meta values that will be used to store post custom data.
14 + */
15 + const DESCRIPTION_META_KEY = 'advanced_seo_description';
16 + const HTML_TITLE_META_KEY = 'jetpack_seo_html_title';
17 + const NOINDEX_META_KEY = 'jetpack_seo_noindex';
18 + const SCHEMA_TYPE_META_KEY = 'jetpack_seo_schema_type';
19 + const POST_META_KEYS_ARRAY = array(
20 + self::DESCRIPTION_META_KEY,
21 + self::HTML_TITLE_META_KEY,
22 + self::NOINDEX_META_KEY,
23 + self::SCHEMA_TYPE_META_KEY,
24 + );
25 +
26 + /**
27 + * Allowed Schema.org types that can be stored in the per-post schema-type
28 + * meta. Empty string means "no override" — Schema_Builder picks a sensible
29 + * default for the post. Single source of truth for the meta enum, the
30 + * block-editor panel options, and Schema_Builder.
31 + */
32 + const ALLOWED_SCHEMA_TYPES = array( '', 'article', 'faq' );
33 +
34 + /**
35 + * Build meta description for post SEO.
36 + *
37 + * @param WP_Post|null $post Source of data for custom description.
38 + *
39 + * @return string Post description or empty string.
40 + */
41 + public static function get_post_description( $post = null ) {
42 + $post = get_post( $post );
43 + if ( ! ( $post instanceof WP_Post ) ) {
44 + return '';
45 + }
46 +
47 + // Check the post being described, not the global one.
48 + if ( post_password_required( $post ) || ! is_singular() ) {
49 + return '';
50 + }
51 +
52 + // Business users can overwrite the description.
53 + $custom_description = self::get_post_custom_description( $post );
54 +
55 + if ( ! empty( $custom_description ) ) {
56 + return $custom_description;
57 + }
58 +
59 + if ( ! empty( $post->post_excerpt ) ) {
60 + return $post->post_excerpt;
61 + }
62 +
63 + // The fall-through reads raw post_content, which never passes the `the_content` paywall.
64 + $content = $post->post_content;
65 + if ( \Automattic\Jetpack\SEO\Content_Gate::is_gated( $post ) ) {
66 + $content = \Automattic\Jetpack\SEO\Content_Gate::public_teaser( $post );
67 + if ( '' === $content ) {
68 + return '';
69 + }
70 + }
71 +
72 + // Remove content within wp:query blocks and return.
73 + return Jetpack_SEO_Utils::remove_query_blocks( $content );
74 + }
75 +
76 + /**
77 + * Returns post's custom meta description if it is set, and if
78 + * SEO tools are enabled for current blog.
79 + *
80 + * @param WP_Post|null $post Source of data for custom description.
81 + *
82 + * @return string Custom description or empty string
83 + */
84 + public static function get_post_custom_description( $post = null ) {
85 + $post = get_post( $post );
86 + if ( ! ( $post instanceof WP_Post ) ) {
87 + return '';
88 + }
89 +
90 + $custom_description = get_post_meta( $post->ID, self::DESCRIPTION_META_KEY, true );
91 +
92 + if ( empty( $custom_description ) || ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
93 + return '';
94 + }
95 +
96 + return $custom_description;
97 + }
98 +
99 + /**
100 + * Gets a custom HTML title for a post if one is set, and if
101 + * SEO tools are enabled for the current blog.
102 + *
103 + * @param WP_Post|null $post Source of data for the custom HTML title.
104 + *
105 + * @return string Custom HTML title or an empty string if not set.
106 + */
107 + public static function get_post_custom_html_title( $post = null ) {
108 + $post = get_post( $post );
109 + if ( ! ( $post instanceof WP_Post ) ) {
110 + return '';
111 + }
112 +
113 + $custom_html_title = get_post_meta( $post->ID, self::HTML_TITLE_META_KEY, true );
114 +
115 + if ( empty( $custom_html_title ) || ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
116 + return '';
117 + }
118 +
119 + return $custom_html_title;
120 + }
121 +
122 + /**
123 + * Gets the `jetpack_seo_noindex` setting for a post, if
124 + * SEO tools are enabled for the current blog.
125 + *
126 + * @param WP_Post|null $post Provided post or defaults to the global post.
127 + *
128 + * @return bool True if post should be marked as noindex, false otherwise.
129 + */
130 + public static function get_post_noindex_setting( $post = null ) {
131 + $post = get_post( $post );
132 + if ( ! ( $post instanceof WP_Post ) ) {
133 + return false;
134 + }
135 +
136 + $mark_as_noindex = get_post_meta( $post->ID, self::NOINDEX_META_KEY, true );
137 +
138 + if ( empty( $mark_as_noindex ) || ! Jetpack_SEO_Utils::is_enabled_jetpack_seo() ) {
139 + return false;
140 + }
141 +
142 + return (bool) $mark_as_noindex;
143 + }
144 +
145 + /**
146 + * Filter callback for `jetpack_sitemap_skip_post`; if a post has `jetpack_seo_noindex` set to true,
147 + * then exclude that post from the Jetpack sitemap.
148 + *
149 + * @param bool $skip Whether to skip the post in the sitemap.
150 + * @param WP_Post $post The post to check.
151 + *
152 + * @return bool
153 + */
154 + public static function exclude_noindex_posts_from_jetpack_sitemap( $skip, $post ) {
155 + $exclude = self::get_post_noindex_setting( $post );
156 + if ( $exclude ) {
157 + $skip = true;
158 + }
159 + return $skip;
160 + }
161 +
162 + /**
163 + * Registers the SEO post meta keys for use in the REST API:
164 + * - self::DESCRIPTION_META_KEY
165 + * - self::HTML_TITLE_META_KEY
166 + * - self::NOINDEX_META_KEY
167 + * - self::SCHEMA_TYPE_META_KEY
168 + */
169 + public static function register_post_meta() {
170 + $description_args = array(
171 + 'type' => 'string',
172 + 'description' => __( 'Custom post description to be used in HTML <meta /> tag.', 'jetpack' ),
173 + 'single' => true,
174 + 'default' => '',
175 + 'show_in_rest' => array(
176 + 'name' => self::DESCRIPTION_META_KEY,
177 + ),
178 + );
179 +
180 + $html_title_args = array(
181 + 'type' => 'string',
182 + 'description' => __( 'Custom title to be used in HTML <title /> tag.', 'jetpack' ),
183 + 'single' => true,
184 + 'default' => '',
185 + 'show_in_rest' => array(
186 + 'name' => self::HTML_TITLE_META_KEY,
187 + ),
188 + );
189 +
190 + $noindex_args = array(
191 + 'type' => 'boolean',
192 + 'description' => __( 'Whether to hide the post from search engines and the Jetpack sitemap.', 'jetpack' ),
193 + 'single' => true,
194 + 'default' => false,
195 + 'show_in_rest' => array(
196 + 'name' => self::NOINDEX_META_KEY,
197 + ),
198 + );
199 +
200 + $schema_type_args = array(
201 + 'type' => 'string',
202 + 'description' => __( 'Schema.org type to emit as JSON-LD for this post.', 'jetpack' ),
203 + 'single' => true,
204 + 'default' => '',
205 + 'sanitize_callback' => array( __CLASS__, 'sanitize_schema_type' ),
206 + 'show_in_rest' => array(
207 + 'name' => self::SCHEMA_TYPE_META_KEY,
208 + // Enum so core REST rejects an unknown schema type with a proper
209 + // rest_invalid_param error; the sanitize_callback is the
210 + // defense-in-depth fallback for non-REST writes.
211 + 'schema' => array(
212 + 'type' => 'string',
213 + 'enum' => self::ALLOWED_SCHEMA_TYPES,
214 + ),
215 + ),
216 + );
217 +
218 + register_meta( 'post', self::DESCRIPTION_META_KEY, $description_args );
219 + register_meta( 'post', self::HTML_TITLE_META_KEY, $html_title_args );
220 + register_meta( 'post', self::NOINDEX_META_KEY, $noindex_args );
221 + register_meta( 'post', self::SCHEMA_TYPE_META_KEY, $schema_type_args );
222 + }
223 +
224 + /**
225 + * Sanitize a schema type to the allowed list. Unknown values become ''
226 + * (no override) rather than erroring, so a non-REST write can't store junk.
227 + *
228 + * @param string $value The submitted value.
229 + * @return string A value from self::ALLOWED_SCHEMA_TYPES.
230 + */
231 + public static function sanitize_schema_type( $value ) {
232 + $value = is_string( $value ) ? sanitize_key( $value ) : '';
233 + return in_array( $value, self::ALLOWED_SCHEMA_TYPES, true ) ? $value : '';
234 + }
235 +
236 + /**
237 + * Get the per-post schema-type override, if any.
238 + *
239 + * @param WP_Post|int|null $post Post or post ID.
240 + * @return string A value from self::ALLOWED_SCHEMA_TYPES ('' = no override).
241 + */
242 + public static function get_post_schema_type( $post = null ) {
243 + $post = get_post( $post );
244 + if ( ! ( $post instanceof WP_Post ) ) {
245 + return '';
246 + }
247 + return self::sanitize_schema_type( (string) get_post_meta( $post->ID, self::SCHEMA_TYPE_META_KEY, true ) );
248 + }
249 +
250 + /**
251 + * Factual per-post SEO field coverage — presence/state only, never a score.
252 + *
253 + * Single source of truth shared by the Content tab, the edit.php columns,
254 + * and the Overview coverage card so the three never drift. Reports whether
255 + * each field has been *set*, independent of whether SEO tools are currently
256 + * active (this is an authoring/audit view, not front-end emission).
257 + *
258 + * @param WP_Post|int|null $post Post or post ID.
259 + * @return array{has_custom_title:bool,has_description:bool,has_schema_type:bool,noindex:bool}
260 + */
261 + public static function get_post_seo_coverage( $post = null ) {
262 + $post = get_post( $post );
263 + if ( ! ( $post instanceof WP_Post ) ) {
264 + return array(
265 + 'has_custom_title' => false,
266 + 'has_description' => false,
267 + 'has_schema_type' => false,
268 + 'noindex' => false,
269 + );
270 + }
271 +
272 + return array(
273 + 'has_custom_title' => '' !== (string) get_post_meta( $post->ID, self::HTML_TITLE_META_KEY, true ),
274 + 'has_description' => '' !== (string) get_post_meta( $post->ID, self::DESCRIPTION_META_KEY, true ),
275 + 'has_schema_type' => '' !== self::get_post_schema_type( $post ),
276 + 'noindex' => (bool) get_post_meta( $post->ID, self::NOINDEX_META_KEY, true ),
277 + );
278 + }
279 +}