@@ -47,27 +47,28 @@ public function get_blog_feed() {
4747
4848 // Transient expired, fetch new feed.
4949 if ( $ feed_data ['expires ' ] < \time () ) {
50- // Get the feed using the REST API.
51- $ response = \wp_remote_get ( \progress_planner ()->get_ui__branding ()->get_blog_feed_url () . '/wp-json/wp/v2/posts/?per_page=2 ' );
50+ // Get the feed URL - append /feed/ if not already present.
51+ $ feed_url = \progress_planner ()->get_ui__branding ()->get_blog_feed_url ();
52+ if ( ! \str_contains ( $ feed_url , '/feed ' ) && ! \str_ends_with ( $ feed_url , '.rss ' ) ) {
53+ $ feed_url = \trailingslashit ( $ feed_url ) . 'feed/ ' ;
54+ }
55+
56+ // Fetch the RSS feed using WordPress's built-in function.
57+ $ rss = \fetch_feed ( $ feed_url );
5258
53- if ( 200 !== \wp_remote_retrieve_response_code ( $ response ) ) {
54- // If we cant fetch the feed, we will try again later.
59+ if ( \is_wp_error ( $ rss ) ) {
60+ // If we can't fetch the feed, we will try again later.
5561 $ feed_data ['expires ' ] = \time () + 5 * MINUTE_IN_SECONDS ;
5662 } else {
57- $ feed = \json_decode ( \wp_remote_retrieve_body ( $ response ), true );
58-
59- foreach ( $ feed as $ key => $ post ) {
60- // Get the featured media.
61- $ featured_media_id = $ post ['featured_media ' ];
62- if ( $ featured_media_id ) {
63- $ response = \wp_remote_get ( \progress_planner ()->get_remote_server_root_url () . '/wp-json/wp/v2/media/ ' . $ featured_media_id );
64- if ( ! \is_wp_error ( $ response ) ) {
65- $ media = \json_decode ( \wp_remote_retrieve_body ( $ response ), true );
66-
67- $ post ['featured_media ' ] = $ media ;
68- }
63+ $ feed = [];
64+ $ max_items = $ rss ->get_item_quantity ( 2 );
65+ $ rss_items = $ rss ->get_items ( 0 , $ max_items );
66+
67+ foreach ( $ rss_items as $ item ) {
68+ $ post = $ this ->parse_rss_item ( $ item );
69+ if ( $ post ) {
70+ $ feed [] = $ post ;
6971 }
70- $ feed [ $ key ] = $ post ;
7172 }
7273
7374 $ feed_data ['feed ' ] = $ feed ;
@@ -81,6 +82,248 @@ public function get_blog_feed() {
8182 return $ feed_data ['feed ' ];
8283 }
8384
85+ /**
86+ * Parse an RSS feed item and convert it to the expected format.
87+ *
88+ * @param \SimplePie\Item|null $item The RSS feed item.
89+ * @return array|null The parsed item data or null if parsing fails.
90+ */
91+ private function parse_rss_item ( $ item ) {
92+ if ( ! $ item ) {
93+ return null ;
94+ }
95+
96+ $ post = [
97+ 'title ' => [
98+ 'rendered ' => $ item ->get_title (),
99+ ],
100+ 'link ' => $ item ->get_permalink (),
101+ 'date ' => $ item ->get_date ( 'c ' ),
102+ 'excerpt ' => [
103+ 'rendered ' => $ item ->get_description (),
104+ ],
105+ 'content ' => [
106+ 'rendered ' => $ item ->get_content (),
107+ ],
108+ 'featured_media ' => null ,
109+ ];
110+
111+ // Try to extract featured image from various sources.
112+ $ featured_image_url = $ this ->extract_featured_image ( $ item );
113+
114+ if ( $ featured_image_url ) {
115+ $ post ['featured_media ' ] = [
116+ 'source_url ' => $ featured_image_url ,
117+ 'media_details ' => [
118+ 'sizes ' => [
119+ 'medium ' => [
120+ 'source_url ' => $ featured_image_url ,
121+ ],
122+ 'medium_large ' => [
123+ 'source_url ' => $ featured_image_url ,
124+ ],
125+ ],
126+ ],
127+ ];
128+ }
129+
130+ return $ post ;
131+ }
132+
133+ /**
134+ * Extract featured image URL from an RSS feed item.
135+ *
136+ * @param \SimplePie\Item $item The RSS feed item.
137+ * @return string|null The featured image URL or null if not found.
138+ */
139+ private function extract_featured_image ( $ item ) {
140+ // Method 1: Try media:thumbnail namespace.
141+ $ media_thumbnail = $ item ->get_item_tags ( 'http://search.yahoo.com/mrss/ ' , 'thumbnail ' );
142+ if ( $ media_thumbnail && isset ( $ media_thumbnail [0 ]['attribs ' ]['' ]['url ' ] ) ) {
143+ return $ media_thumbnail [0 ]['attribs ' ]['' ]['url ' ];
144+ }
145+
146+ // Method 2: Try media:content.
147+ $ media_content = $ item ->get_item_tags ( 'http://search.yahoo.com/mrss/ ' , 'content ' );
148+ if ( $ media_content && isset ( $ media_content [0 ]['attribs ' ]['' ]['url ' ] ) ) {
149+ return $ media_content [0 ]['attribs ' ]['' ]['url ' ];
150+ }
151+
152+ // Method 3: Try enclosure.
153+ $ enclosure = $ item ->get_enclosure ();
154+ if ( $ enclosure ) {
155+ if ( $ enclosure ->get_thumbnail () ) {
156+ return $ enclosure ->get_thumbnail ();
157+ }
158+ if ( $ enclosure ->get_link () && \str_contains ( $ enclosure ->get_type (), 'image ' ) ) {
159+ return $ enclosure ->get_link ();
160+ }
161+ }
162+
163+ // Method 4: Try to extract from description HTML (prioritize featured images).
164+ $ description = $ item ->get_description ();
165+ if ( $ description ) {
166+ $ image_url = $ this ->extract_image_from_html ( $ description , true );
167+ if ( $ image_url ) {
168+ return $ image_url ;
169+ }
170+ }
171+
172+ // Method 5: Try to extract from content HTML (prioritize featured images).
173+ $ content = $ item ->get_content ();
174+ if ( $ content ) {
175+ $ image_url = $ this ->extract_image_from_html ( $ content , true );
176+ if ( $ image_url ) {
177+ return $ image_url ;
178+ }
179+ }
180+
181+ // Method 6: Try to get from REST API as fallback if feed is from a WordPress site.
182+ $ permalink = $ item ->get_permalink ();
183+ if ( $ permalink && \str_contains ( $ permalink , \progress_planner ()->get_ui__branding ()->get_blog_feed_url () ) ) {
184+ $ rest_api_image = $ this ->get_featured_image_from_rest_api ( $ permalink );
185+ if ( $ rest_api_image ) {
186+ return $ rest_api_image ;
187+ }
188+ }
189+
190+ // Method 7: Try to extract ANY image from content as last resort.
191+ if ( $ content ) {
192+ $ image_url = $ this ->extract_image_from_html ( $ content , false );
193+ if ( $ image_url ) {
194+ return $ image_url ;
195+ }
196+ }
197+
198+ // Method 8: Try to extract ANY image from description as last resort.
199+ if ( $ description ) {
200+ $ image_url = $ this ->extract_image_from_html ( $ description , false );
201+ if ( $ image_url ) {
202+ return $ image_url ;
203+ }
204+ }
205+
206+ return null ;
207+ }
208+
209+ /**
210+ * Extract image URL from HTML content.
211+ *
212+ * Checks for various HTML elements and attributes that can contain images:
213+ * - <img> tags (src, srcset, data-src)
214+ * - <picture> elements (source tags and img fallback)
215+ * - <figure> elements
216+ * - Background images in style attributes
217+ *
218+ * @param string $html The HTML content to parse.
219+ * @param bool $prioritize_featured Whether to prioritize featured images (wp-post-thumbnail, wp-post-image).
220+ * @return string|null The first image URL found, or null if none found.
221+ */
222+ private function extract_image_from_html ( $ html , $ prioritize_featured = false ) {
223+ if ( empty ( $ html ) ) {
224+ return null ;
225+ }
226+
227+ // If prioritizing featured images, check for WordPress featured image classes first.
228+ if ( $ prioritize_featured ) {
229+ // Check for wp-post-thumbnail or wp-post-image class.
230+ if ( \preg_match ( '/<img[^>]+class=[" \'][^" \']*(?:wp-post-thumbnail|wp-post-image)[^" \']*[" \'][^>]+src=[" \']([^" \']+)[" \'][^>]*>/i ' , $ html , $ matches ) ) {
231+ return $ matches [1 ];
232+ }
233+ if ( \preg_match ( '/<img[^>]+src=[" \']([^" \']+)[" \'][^>]+class=[" \'][^" \']*(?:wp-post-thumbnail|wp-post-image)[^" \']*[" \'][^>]*>/i ' , $ html , $ matches ) ) {
234+ return $ matches [1 ];
235+ }
236+ }
237+
238+ // Check for <picture> element with <source> tags.
239+ if ( \preg_match ( '/<picture[^>]*>.*?<source[^>]+srcset=[" \']([^" \'\s]+)[^" \']*[" \'][^>]*>.*?<\/picture>/is ' , $ html , $ matches ) ) {
240+ // Extract first URL from srcset (may contain multiple URLs with sizes).
241+ $ srcset = $ matches [1 ];
242+ if ( \preg_match ( '/^([^\s]+)/ ' , $ srcset , $ url_match ) ) {
243+ return $ url_match [1 ];
244+ }
245+ }
246+
247+ // Check for <img> with srcset attribute (responsive images).
248+ if ( \preg_match ( '/<img[^>]+srcset=[" \']([^" \'\s]+)[^" \']*[" \'][^>]*>/i ' , $ html , $ matches ) ) {
249+ // Extract first URL from srcset.
250+ $ srcset = $ matches [1 ];
251+ if ( \preg_match ( '/^([^\s]+)/ ' , $ srcset , $ url_match ) ) {
252+ return $ url_match [1 ];
253+ }
254+ }
255+
256+ // Check for lazy-loaded images (data-src).
257+ if ( \preg_match ( '/<img[^>]+data-src=[" \']([^" \']+)[" \'][^>]*>/i ' , $ html , $ matches ) ) {
258+ return $ matches [1 ];
259+ }
260+
261+ // Check for standard <img> src.
262+ if ( \preg_match ( '/<img[^>]+src=[" \']([^" \']+)[" \'][^>]*>/i ' , $ html , $ matches ) ) {
263+ return $ matches [1 ];
264+ }
265+
266+ // Check for background images in style attributes.
267+ if ( \preg_match ( '/style=[" \'][^" \']*background-image\s*:\s*url\([" \']?([^" \')]+)[" \']?\)[^" \']*[" \'][^>]*>/i ' , $ html , $ matches ) ) {
268+ return $ matches [1 ];
269+ }
270+
271+ // Check for <figure> with nested img.
272+ if ( \preg_match ( '/<figure[^>]*>.*?<img[^>]+src=[" \']([^" \']+)[" \'][^>]*>.*?<\/figure>/is ' , $ html , $ matches ) ) {
273+ return $ matches [1 ];
274+ }
275+
276+ return null ;
277+ }
278+
279+ /**
280+ * Get featured image from WordPress REST API using post permalink.
281+ *
282+ * @param string $permalink The post permalink.
283+ * @return string|null The featured image URL or null if not found.
284+ */
285+ private function get_featured_image_from_rest_api ( $ permalink ) {
286+ // Extract post slug from permalink.
287+ $ slug = \basename ( \untrailingslashit ( $ permalink ) );
288+ if ( ! $ slug ) {
289+ return null ;
290+ }
291+
292+ // Try to get post data from REST API.
293+ $ api_url = \trailingslashit ( \progress_planner ()->get_ui__branding ()->get_blog_feed_url () ) . 'wp-json/wp/v2/posts?slug= ' . $ slug ;
294+ $ response = \wp_remote_get ( $ api_url );
295+
296+ if ( \is_wp_error ( $ response ) || 200 !== \wp_remote_retrieve_response_code ( $ response ) ) {
297+ return null ;
298+ }
299+
300+ $ posts = \json_decode ( \wp_remote_retrieve_body ( $ response ), true );
301+ if ( empty ( $ posts ) || ! isset ( $ posts [0 ]['featured_media ' ] ) || ! $ posts [0 ]['featured_media ' ] ) {
302+ return null ;
303+ }
304+
305+ // Get the featured media.
306+ $ media_id = $ posts [0 ]['featured_media ' ];
307+ $ media_response = \wp_remote_get (
308+ \trailingslashit ( \progress_planner ()->get_ui__branding ()->get_blog_feed_url () ) . 'wp-json/wp/v2/media/ ' . $ media_id
309+ );
310+
311+ if ( \is_wp_error ( $ media_response ) || 200 !== \wp_remote_retrieve_response_code ( $ media_response ) ) {
312+ return null ;
313+ }
314+
315+ $ media = \json_decode ( \wp_remote_retrieve_body ( $ media_response ), true );
316+ if ( isset ( $ media ['media_details ' ]['sizes ' ]['medium_large ' ]['source_url ' ] ) ) {
317+ return $ media ['media_details ' ]['sizes ' ]['medium_large ' ]['source_url ' ];
318+ }
319+
320+ if ( isset ( $ media ['source_url ' ] ) ) {
321+ return $ media ['source_url ' ];
322+ }
323+
324+ return null ;
325+ }
326+
84327 /**
85328 * Get the cache key.
86329 *
0 commit comments