PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 13.7.2 13.8.3 All 506 releases
jetpack / modules / seo-tools / class-jetpack-seo-posts.php

class-jetpack-seo-posts.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at modules/seo-tools/class-jetpack-seo-posts.php

280 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
280