helpers->getPostPageBuilderName( $postId ); } /** * Checks if a post is built with a specific page builder using AIOSEO's existing integrations. * * @since 4.8.8 * * @param int $postId The post ID. * @param string $builderName The page builder name. * @return bool Whether the post is built with the specified builder. */ private function isBuiltWith( $postId, $builderName ) { if ( ! isset( aioseo()->standalone->pageBuilderIntegrations[ $builderName ] ) ) { return false; } return aioseo()->standalone->pageBuilderIntegrations[ $builderName ]->isBuiltWith( $postId ); } /** * Gets the properly rendered content for a post, ensuring it works in both Action Scheduler and normal contexts. * * @since 4.8.8 * * @param \WP_Post $post The post object. * @param bool $short Whether to return a short version (like excerpt/description) instead of full content. * @return string */ public function getRenderedContent( $post, $short = false ) { // Ensure we have a full WP_Post object if ( ! is_a( $post, 'WP_Post' ) || ! isset( $post->post_content ) ) { $post = get_post( $post->ID ); if ( ! is_a( $post, 'WP_Post' ) ) { return ''; } } // If short mode is requested, get the full content first and then shorten it if ( $short ) { // Get the full rendered content first $fullContent = $this->getRenderedContent( $post, false ); // Then shorten it return $this->shortenContent( $fullContent ); } // Check if we can use AIOSEO's existing page builder processing $pageBuilder = $this->getPageBuilderName( $post->ID ); if ( ! empty( $pageBuilder ) && isset( aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ] ) ) { $processedContent = $this->processPageBuilderContent( $post, $pageBuilder ); // If the page builder processed the content successfully, return it if ( ! $this->containsUnrenderedShortcodes( $processedContent ) ) { return $processedContent; } } // Special handling for Thrive Architect if ( 'thrive-architect' === $pageBuilder ) { $thriveContent = $this->processThriveArchitectContent( $post ); if ( ! empty( $thriveContent ) && ! $this->containsUnrenderedShortcodes( $thriveContent ) ) { return $thriveContent; } } // Special handling for Elementor if ( 'elementor' === $pageBuilder ) { $elementorContent = $this->processElementorContent( $post ); if ( ! empty( $elementorContent ) && ! $this->containsUnrenderedShortcodes( $elementorContent ) ) { return $elementorContent; } } // If we're in a proper WordPress context and no page builder was detected, use standard approach if ( ! wp_doing_cron() && ! wp_doing_ajax() && ! is_admin() ) { return apply_filters( 'the_content', $post->post_content ); } // For Action Scheduler and other non-frontend contexts, we need to simulate a frontend request return $this->simulateFrontendRequest( $post ); } /** * Shortens the full rendered content to create a description. * * @since 4.8.8 * * @param string $content The full rendered content. * @return string */ private function shortenContent( $content ) { // If content is empty, return empty if ( empty( $content ) ) { return ''; } // Strip HTML tags and get plain text $plainContent = wp_strip_all_tags( $content ); // Clean up whitespace $plainContent = preg_replace( '/\s+/', ' ', $plainContent ); $plainContent = trim( $plainContent ); // If content is empty after cleaning, return empty if ( empty( $plainContent ) ) { return ''; } // If content is short enough, return as-is if ( strlen( $plainContent ) <= 160 ) { return $plainContent; } // Truncate at word boundary and add ellipsis $shortened = substr( $plainContent, 0, 160 ); $lastSpace = strrpos( $shortened, ' ' ); if ( false !== $lastSpace && 100 < $lastSpace ) { $shortened = substr( $shortened, 0, $lastSpace ); } return $shortened . '...'; } /** * Processes content using the appropriate page builder integration. * * @since 4.8.8 * * @param \WP_Post $post The post object. * @param string $pageBuilder The page builder name. * @return string */ private function processPageBuilderContent( $post, $pageBuilder ) { $integration = aioseo()->standalone->pageBuilderIntegrations[ $pageBuilder ]; // For WPBakery, we need to ensure shortcodes are mapped before processing if ( 'wpbakery' === $pageBuilder ) { if ( method_exists( '\WPBMap', 'addAllMappedShortcodes' ) ) { \WPBMap::addAllMappedShortcodes(); } } // Process the content using the page builder's method $processedContent = $integration->processContent( $post->ID, $post->post_content ); // If the page builder didn't process the content (returned raw), try applying the_content filter if ( $processedContent === $post->post_content || $this->containsUnrenderedShortcodes( $processedContent ) ) { // Set up the page builder context first $this->setupPageBuilderContext( $post ); // Then apply the content filter $processedContent = apply_filters( 'the_content', $post->post_content ); } return $processedContent; } /** * Simulates a frontend request to properly render content with page builders. * * @since 4.8.8 * * @param \WP_Post $post The post object. * @return string */ private function simulateFrontendRequest( $post ) { // Try using WordPress's internal request handling first $content = $this->renderContentViaInternalRequest( $post ); // If that didn't work or returned raw shortcodes, try the manual approach if ( $this->containsUnrenderedShortcodes( $content ) ) { $content = $this->renderContentManually( $post ); } // If still not working, try making an actual HTTP request if ( $this->containsUnrenderedShortcodes( $content ) ) { $content = $this->renderContentViaHttpRequest( $post ); } return $content; } /** * Renders content using WordPress's internal request handling. * * @since 4.8.8 * * @param \WP_Post $post The post object. * @return string */ private function renderContentViaInternalRequest( $post ) { // Store original state $originalPost = $GLOBALS['post'] ?? null; $originalQuery = $GLOBALS['wp_query'] ?? null; try { // Set up the post $GLOBALS['post'] = $post; setup_postdata( $post ); // Create a proper query $query = new \WP_Query( [ 'p' => $post->ID, 'post_type' => $post->post_type, 'post_status' => 'publish' ] ); $GLOBALS['wp_query'] = $query; $GLOBALS['wp_the_query'] = $query; // Set up page builder context $this->setupPageBuilderContext( $post ); // Process shortcodes using AIOSEO's helper which handles Divi properly $content = aioseo()->helpers->doShortcodes( $post->post_content, [], $post->ID ); // Apply the content filters $content = apply_filters( 'the_content', $content ); } finally { // Restore original state $GLOBALS['post'] = $originalPost; $GLOBALS['wp_query'] = $originalQuery; if ( $originalPost ) { setup_postdata( $originalPost ); } } return $content; } /** * Renders content manually with extensive context setup. * * @since 4.8.8 * * @param \WP_Post $post The post object. * @return string */ private function renderContentManually( $post ) { // Store all current global state $originalGlobals = [ 'post' => $GLOBALS['post'] ?? null, 'wp_query' => $GLOBALS['wp_query'] ?? null, 'wp_the_query' => $GLOBALS['wp_the_query'] ?? null, 'wp' => $GLOBALS['wp'] ?? null, ]; // Store Divi-specific globals if they exist $originalDiviGlobals = []; $diviGlobals = [ 'et_pb_current_template', 'et_pb_rendering_column', 'et_pb_rendering_specialty_section', 'et_pb_rendering_row', 'et_pb_rendering_section', ]; foreach ( $diviGlobals as $global ) { $originalDiviGlobals[ $global ] = $GLOBALS[ $global ] ?? null; } try { // Set up the post context $GLOBALS['post'] = $post; setup_postdata( $post ); // Create a proper WP_Query object $query = new \WP_Query(); $query->init(); $query->is_single = true; $query->is_singular = true; $query->is_page = ( 'page' === $post->post_type ); $query->is_home = false; $query->is_front_page = ( 'page' === $post->post_type && get_option( 'page_on_front' ) === $post->ID ); // @phpstan-ignore-line $query->queried_object = $post; $query->queried_object_id = $post->ID; $query->post_count = 1; $query->found_posts = 1; $query->max_num_pages = 1; // Set the global query objects $GLOBALS['wp_query'] = $query; $GLOBALS['wp_the_query'] = $query; // Set up page builder context $this->setupPageBuilderContext( $post ); // Process shortcodes using AIOSEO's helper which handles Divi properly $content = aioseo()->helpers->doShortcodes( $post->post_content, [], $post->ID ); // Apply the content filters $content = apply_filters( 'the_content', $content ); } finally { // Restore all original global state foreach ( $originalGlobals as $key => $value ) { $GLOBALS[ $key ] = $value; } foreach ( $originalDiviGlobals as $key => $value ) { $GLOBALS[ $key ] = $value; } // Restore post data if we had an original post if ( $originalGlobals['post'] ) { setup_postdata( $originalGlobals['post'] ); } } return $content; } /** * Renders content by making an HTTP request to the frontend. * * @since 4.8.8 * * @param \WP_Post $post The post object. * @return string */ private function renderContentViaHttpRequest( $post ) { $url = get_permalink( $post->ID ); if ( ! $url ) { return $post->post_content; } // Add a parameter to indicate this is for LLMS generation $url = add_query_arg( 'aioseo_llms_generation', '1', $url ); // Make the HTTP request $response = wp_remote_get( $url, [ 'timeout' => 30, 'user-agent' => 'AIOSEO-LLMS-Generator/1.0', 'headers' => [ 'X-AIOSEO-LLMS-Generation' => '1', ], ] ); if ( is_wp_error( $response ) ) { return $post->post_content; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return $post->post_content; } // Extract the content from the HTML response $content = $this->extractContentFromHtml( $body ); return $content ?: $post->post_content; } /** * Extracts the main content from HTML response. * * @since 4.8.8 * * @param string $html The HTML content. * @return string */ private function extractContentFromHtml( $html ) { // Remove CSS and JavaScript first $html = preg_replace( '/]*>.*?<\/style>/is', '', $html ); $html = preg_replace( '/]*>.*?<\/script>/is', '', $html ); $html = preg_replace( '/]*rel=["\']stylesheet["\'][^>]*>/i', '', $html ); // Try to find the main content area $patterns = [ '/]*>(.*?)<\/main>/is', '/]*>(.*?)<\/article>/is', '/]*class="[^"]*entry-content[^"]*"[^>]*>(.*?)<\/div>/is', '/]*class="[^"]*post-content[^"]*"[^>]*>(.*?)<\/div>/is', '/]*class="[^"]*content[^"]*"[^>]*>(.*?)<\/div>/is', '/]*class="[^"]*et_pb_section[^"]*"[^>]*>(.*?)<\/div>/is', ]; foreach ( $patterns as $pattern ) { if ( preg_match( $pattern, $html, $matches ) ) { $content = $matches[1]; // Clean up the content $content = $this->cleanHtmlContent( $content ); if ( ! empty( $content ) ) { return $content; } } } // Fallback: return the entire body content return $this->cleanHtmlContent( $html ); } /** * Cleans HTML content for better markdown conversion. * * @since 4.8.8 * * @param string $content The HTML content to clean. * @return string */ private function cleanHtmlContent( $content ) { // Remove unwanted elements $content = preg_replace( '/]*>.*?<\/style>/is', '', $content ); $content = preg_replace( '/]*>.*?<\/script>/is', '', $content ); $content = preg_replace( '/]*>/i', '', $content ); $content = preg_replace( '/]*>/i', '', $content ); $content = preg_replace( '/]*>.*?<\/noscript>/is', '', $content ); // Remove author bio sections that are duplicated $content = preg_replace( '/]*class="[^"]*aioseo-author-bio[^"]*"[^>]*>.*?<\/div>/is', '', $content ); // Remove empty paragraphs and divs $content = preg_replace( '/]*>\s*<\/p>/i', '', $content ); $content = preg_replace( '/]*>\s*<\/div>/i', '', $content ); // Clean up whitespace $content = preg_replace( '/\s+/', ' ', $content ); $content = trim( $content ); // Strip tags but keep essential formatting $content = strip_tags( $content, '