• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / ca99786b69a25106723450181d6049c34982ff80

08 May 2025 09:25AM UTC coverage: 52.549% (-1.9%) from 54.437%
ca99786b69a25106723450181d6049c34982ff80

Pull #22170

github

web-flow
Merge be48a0f09 into c122b65f1
Pull Request #22170: Creates the popover component

8040 of 14189 branches covered (56.66%)

Branch coverage included in aggregate %.

29255 of 56783 relevant lines covered (51.52%)

41975.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.89
/src/generators/schema/article.php
1
<?php
2

3
namespace Yoast\WP\SEO\Generators\Schema;
4

5
use WP_User;
6
use Yoast\WP\SEO\Config\Schema_IDs;
7

8
/**
9
 * Returns schema Article data.
10
 */
11
class Article extends Abstract_Schema_Piece {
12

13
        /**
14
         * Determines whether or not a piece should be added to the graph.
15
         *
16
         * @return bool
17
         */
18
        public function is_needed() {
4✔
19
                if ( $this->context->indexable->object_type !== 'post' ) {
4✔
20
                        return false;
1✔
21
                }
22

23
                // If we cannot output a publisher, we shouldn't output an Article.
24
                if ( $this->context->site_represents === false ) {
3✔
25
                        return false;
1✔
26
                }
27

28
                // If we cannot output an author, we shouldn't output an Article.
29
                if ( ! $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) ) {
2✔
30
                        return false;
1✔
31
                }
32

33
                if ( $this->context->schema_article_type !== 'None' ) {
1✔
34
                        $this->context->has_article = true;
1✔
35
                        return true;
1✔
36
                }
37

38
                return false;
×
39
        }
40

41
        /**
42
         * Returns Article data.
43
         *
44
         * @return array Article data.
45
         */
46
        public function generate() {
7✔
47
                $author = \get_userdata( $this->context->post->post_author );
7✔
48
                $data   = [
7✔
49
                        '@type'            => $this->context->schema_article_type,
7✔
50
                        '@id'              => $this->context->canonical . Schema_IDs::ARTICLE_HASH,
7✔
51
                        'isPartOf'         => [ '@id' => $this->context->main_schema_id ],
7✔
52
                        'author'           => [
7✔
53
                                'name' => ( $author instanceof WP_User ) ? $this->helpers->schema->html->smart_strip_tags( $author->display_name ) : '',
7✔
54
                                '@id'  => $this->helpers->schema->id->get_user_schema_id( $this->context->post->post_author, $this->context ),
7✔
55
                        ],
7✔
56
                        'headline'         => $this->helpers->schema->html->smart_strip_tags( $this->helpers->post->get_post_title_with_fallback( $this->context->id ) ),
7✔
57
                        'datePublished'    => $this->helpers->date->format( $this->context->post->post_date_gmt ),
7✔
58
                ];
7✔
59

60
                if ( \strtotime( $this->context->post->post_modified_gmt ) > \strtotime( $this->context->post->post_date_gmt ) ) {
7✔
61
                        $data['dateModified'] = $this->helpers->date->format( $this->context->post->post_modified_gmt );
7✔
62
                }
63

64
                $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id ];
7✔
65
                $data['wordCount']        = $this->word_count( $this->context->post->post_content, $this->context->post->post_title );
7✔
66

67
                if ( $this->context->post->comment_status === 'open' ) {
7✔
68
                        $data['commentCount'] = \intval( $this->context->post->comment_count, 10 );
5✔
69
                }
70

71
                if ( $this->context->site_represents_reference ) {
7✔
72
                        $data['publisher'] = $this->context->site_represents_reference;
1✔
73
                }
74

75
                $data = $this->add_image( $data );
7✔
76
                $data = $this->add_keywords( $data );
7✔
77
                $data = $this->add_sections( $data );
7✔
78
                $data = $this->helpers->schema->language->add_piece_language( $data );
7✔
79

80
                if ( \post_type_supports( $this->context->post->post_type, 'comments' ) && $this->context->post->comment_status === 'open' ) {
7✔
81
                        $data = $this->add_potential_action( $data );
4✔
82
                }
83

84
                return $data;
7✔
85
        }
86

87
        /**
88
         * Adds tags as keywords, if tags are assigned.
89
         *
90
         * @param array $data Article data.
91
         *
92
         * @return array Article data.
93
         */
94
        private function add_keywords( $data ) {
7✔
95
                /**
96
                 * Filter: 'wpseo_schema_article_keywords_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data.
97
                 *
98
                 * @param string $taxonomy The chosen taxonomy.
99
                 */
100
                $taxonomy = \apply_filters( 'wpseo_schema_article_keywords_taxonomy', 'post_tag' );
7✔
101

102
                return $this->add_terms( $data, 'keywords', $taxonomy );
7✔
103
        }
104

105
        /**
106
         * Adds categories as sections, if categories are assigned.
107
         *
108
         * @param array $data Article data.
109
         *
110
         * @return array Article data.
111
         */
112
        private function add_sections( $data ) {
7✔
113
                /**
114
                 * Filter: 'wpseo_schema_article_sections_taxonomy' - Allow changing the taxonomy used to assign keywords to a post type Article data.
115
                 *
116
                 * @param string $taxonomy The chosen taxonomy.
117
                 */
118
                $taxonomy = \apply_filters( 'wpseo_schema_article_sections_taxonomy', 'category' );
7✔
119

120
                return $this->add_terms( $data, 'articleSection', $taxonomy );
7✔
121
        }
122

123
        /**
124
         * Adds a term or multiple terms, comma separated, to a field.
125
         *
126
         * @param array  $data     Article data.
127
         * @param string $key      The key in data to save the terms in.
128
         * @param string $taxonomy The taxonomy to retrieve the terms from.
129
         *
130
         * @return mixed Article data.
131
         */
132
        protected function add_terms( $data, $key, $taxonomy ) {
7✔
133
                $terms = \get_the_terms( $this->context->id, $taxonomy );
7✔
134

135
                if ( ! \is_array( $terms ) ) {
7✔
136
                        return $data;
1✔
137
                }
138

139
                $callback = static function ( $term ) {
6✔
140
                        // We are using the WordPress internal translation.
141
                        return $term->name !== \__( 'Uncategorized', 'default' );
5✔
142
                };
6✔
143
                $terms    = \array_filter( $terms, $callback );
6✔
144

145
                if ( empty( $terms ) ) {
6✔
146
                        return $data;
1✔
147
                }
148

149
                $data[ $key ] = \wp_list_pluck( $terms, 'name' );
5✔
150

151
                return $data;
5✔
152
        }
153

154
        /**
155
         * Adds an image node if the post has a featured image.
156
         *
157
         * @param array $data The Article data.
158
         *
159
         * @return array The Article data.
160
         */
161
        private function add_image( $data ) {
7✔
162
                if ( $this->context->main_image_url !== null ) {
7✔
163
                        $data['image']        = [
7✔
164
                                '@id' => $this->context->canonical . Schema_IDs::PRIMARY_IMAGE_HASH,
7✔
165
                        ];
7✔
166
                        $data['thumbnailUrl'] = $this->context->main_image_url;
7✔
167
                }
168

169
                return $data;
7✔
170
        }
171

172
        /**
173
         * Adds the potential action property to the Article Schema piece.
174
         *
175
         * @param array $data The Article data.
176
         *
177
         * @return array The Article data with the potential action added.
178
         */
179
        private function add_potential_action( $data ) {
4✔
180
                /**
181
                 * Filter: 'wpseo_schema_article_potential_action_target' - Allows filtering of the schema Article potentialAction target.
182
                 *
183
                 * @param array $targets The URLs for the Article potentialAction target.
184
                 */
185
                $targets = \apply_filters( 'wpseo_schema_article_potential_action_target', [ $this->context->canonical . '#respond' ] );
4✔
186

187
                $data['potentialAction'][] = [
4✔
188
                        '@type'  => 'CommentAction',
4✔
189
                        'name'   => 'Comment',
4✔
190
                        'target' => $targets,
4✔
191
                ];
4✔
192

193
                return $data;
4✔
194
        }
195

196
        /**
197
         * Does a simple word count but tries to be relatively smart about it.
198
         *
199
         * @param string $post_content The post content.
200
         * @param string $post_title   The post title.
201
         *
202
         * @return int The number of words in the content.
203
         */
204
        private function word_count( $post_content, $post_title = '' ) {
×
205
                // Add the title to our word count.
206
                $post_content = $post_title . ' ' . $post_content;
×
207

208
                // Strip pre/code blocks and their content.
209
                $post_content = \preg_replace( '@<(pre|code)[^>]*?>.*?</\\1>@si', '', $post_content );
×
210

211
                // Add space between tags that don't have it.
212
                $post_content = \preg_replace( '@><@', '> <', $post_content );
×
213

214
                // Strips all other tags.
215
                $post_content = \wp_strip_all_tags( $post_content );
×
216

217
                $characters = '';
×
218

219
                if ( \preg_match( '@[а-я]@ui', $post_content ) ) {
×
220
                        // Correct counting of the number of words in the Russian and Ukrainian languages.
221
                        $alphabet = [
×
222
                                'ru' => 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя',
×
223
                                'ua' => 'абвгґдеєжзиіїйклмнопрстуфхцчшщьюя',
×
224
                        ];
×
225

226
                        $characters  = \implode( '', $alphabet );
×
227
                        $characters  = \preg_split( '//u', $characters, -1, \PREG_SPLIT_NO_EMPTY );
×
228
                        $characters  = \array_unique( $characters );
×
229
                        $characters  = \implode( '', $characters );
×
230
                        $characters .= \mb_strtoupper( $characters );
×
231
                }
232

233
                // Remove characters from HTML entities.
234
                $post_content = \preg_replace( '@&[a-z0-9]+;@i', ' ', \htmlentities( $post_content ) );
×
235

236
                return \str_word_count( $post_content, 0, $characters );
×
237
        }
238
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc