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

Yoast / wordpress-seo / b297a102e39b2007cbf436fb16441d0beb8d529d

17 Dec 2025 01:13PM UTC coverage: 41.433% (-11.1%) from 52.525%
b297a102e39b2007cbf436fb16441d0beb8d529d

Pull #22772

github

web-flow
Merge 7e4648015 into cdc37a449
Pull Request #22772: Add schema feature toggle and settings page

2611 of 9726 branches covered (26.85%)

Branch coverage included in aggregate %.

19 of 181 new or added lines in 17 files covered. (10.5%)

82 existing lines in 17 files now uncovered.

22742 of 51465 relevant lines covered (44.19%)

4.71 hits per line

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

78.41
/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() {
8✔
19
                if ( $this->context->indexable->object_type !== 'post' ) {
8✔
20
                        return false;
2✔
21
                }
22

23
                // If we cannot output an author, we shouldn't output an Article.
24
                if ( ! $this->helpers->schema->article->is_author_supported( $this->context->indexable->object_sub_type ) ) {
6✔
25
                        return false;
2✔
26
                }
27

28
                if ( $this->context->schema_article_type !== 'None' ) {
4✔
29
                        $this->context->has_article = true;
4✔
30
                        return true;
4✔
31
                }
32

UNCOV
33
                return false;
×
34
        }
35

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

55
                if ( \strtotime( $this->context->post->post_modified_gmt ) > \strtotime( $this->context->post->post_date_gmt ) ) {
14✔
56
                        $data['dateModified'] = $this->helpers->date->format( $this->context->post->post_modified_gmt );
14✔
57
                }
58

59
                $data['mainEntityOfPage'] = [ '@id' => $this->context->main_schema_id ];
14✔
60
                $data['wordCount']        = $this->word_count( $this->context->post->post_content, $this->context->post->post_title );
14✔
61

62
                if ( $this->context->post->comment_status === 'open' ) {
14✔
63
                        $data['commentCount'] = \intval( $this->context->post->comment_count, 10 );
10✔
64
                }
65

66
                if ( $this->context->site_represents_reference ) {
14✔
67
                        $data['publisher'] = $this->context->site_represents_reference;
2✔
68
                }
69

70
                $data = $this->add_image( $data );
14✔
71
                $data = $this->add_keywords( $data );
14✔
72
                $data = $this->add_sections( $data );
14✔
73
                $data = $this->helpers->schema->language->add_piece_language( $data );
14✔
74

75
                if ( \post_type_supports( $this->context->post->post_type, 'comments' ) && $this->context->post->comment_status === 'open' ) {
14✔
76
                        $data = $this->add_potential_action( $data );
8✔
77
                }
78

79
                return $data;
14✔
80
        }
81

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

97
                return $this->add_terms( $data, 'keywords', $taxonomy );
14✔
98
        }
99

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

115
                return $this->add_terms( $data, 'articleSection', $taxonomy );
14✔
116
        }
117

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

130
                if ( ! \is_array( $terms ) ) {
14✔
131
                        return $data;
2✔
132
                }
133

134
                $callback = static function ( $term ) {
12✔
135
                        // We are using the WordPress internal translation.
136
                        return $term->name !== \__( 'Uncategorized', 'default' );
10✔
137
                };
12✔
138
                $terms    = \array_filter( $terms, $callback );
12✔
139

140
                if ( empty( $terms ) ) {
12✔
141
                        return $data;
2✔
142
                }
143

144
                $data[ $key ] = \wp_list_pluck( $terms, 'name' );
10✔
145

146
                return $data;
10✔
147
        }
148

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

164
                return $data;
14✔
165
        }
166

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

182
                $data['potentialAction'][] = [
8✔
183
                        '@type'  => 'CommentAction',
8✔
184
                        'name'   => 'Comment',
8✔
185
                        'target' => $targets,
8✔
186
                ];
8✔
187

188
                return $data;
8✔
189
        }
190

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

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

206
                // Add space between tags that don't have it.
UNCOV
207
                $post_content = \preg_replace( '@><@', '> <', $post_content );
×
208

209
                // Strips all other tags.
UNCOV
210
                $post_content = \wp_strip_all_tags( $post_content );
×
211

212
                $characters = '';
×
213

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

221
                        $characters  = \implode( '', $alphabet );
×
222
                        $characters  = \preg_split( '//u', $characters, -1, \PREG_SPLIT_NO_EMPTY );
×
223
                        $characters  = \array_unique( $characters );
×
224
                        $characters  = \implode( '', $characters );
×
UNCOV
225
                        $characters .= \mb_strtoupper( $characters );
×
226
                }
227

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

UNCOV
231
                return \str_word_count( $post_content, 0, $characters );
×
232
        }
233
}
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