-->`, swapped for `payload.parts[]` (listing pages, asked * with `parts=1`). Payload without parts (product · delivery · not_found · older aoo): grid = the whole html, the * other parts empty. A template page whose rendered content holds no token at all (Elementor replaced the content * at p9 and the owner removed every part widget) gets the whole html appended at the end of the_content. * In the Elementor editor (no route) parts/the shortcode show the shop index (preview_part()). * One

: the aoo HTML has its own h1 → theme title adapters (Hello, Astra, Flatsome, GeneratePress, * OceanWP, block themes) + generic CSS fallback (class-assets.php). * * SEO head output (title / description / canonical / robots / og / JSON-LD) lives in class-seo.php — it reads * state() / payload() / route() / localize() from here. * * @package AooShop */ defined( 'ABSPATH' ) || exit; class AooShop_Renderer { const TOKEN = ''; /** Part token prefix — full token ``. */ const PART_TOKEN = ''; } /** * Output of one part for a widget/shortcode: token on an aoo route (replaced at the end of the_content), * shop-index preview in the Elementor editor, '' elsewhere. * * @param string $part '' (whole page) | header | filters | grid | footer. */ public function render_part( $part = '' ) { return $this->shortcode( '' === $part ? array() : array( 'part' => $part ) ); } /** Elementor editor / its preview iframe (no aoo route there). */ public static function in_editor() { if ( ! current_user_can( 'edit_posts' ) ) { return false; // Visitors adding ?elementor-preview never get a preview. } if ( isset( $_GET['elementor-preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- context check only. return true; } if ( did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Plugin' ) && isset( \Elementor\Plugin::$instance ) ) { $el = \Elementor\Plugin::$instance; if ( isset( $el->editor ) && method_exists( $el->editor, 'is_edit_mode' ) && $el->editor->is_edit_mode() ) { return true; } if ( isset( $el->preview ) && method_exists( $el->preview, 'is_preview_mode' ) && $el->preview->is_preview_mode() ) { return true; } } return false; } /** * A part (or the whole page) of the shop index — for the editor, where there is no route. Shares the index * page cache entry with real visits. * * @param string $part '' | header | filters | grid | footer. */ public function preview_part( $part = '' ) { $result = $this->cache->get_page( array( 'type' => 'index', 'page' => 1 ) ); if ( is_wp_error( $result ) ) { return '

' . esc_html( sprintf( /* translators: %s: error message */ __( 'Cannot show the shop preview: %s', 'aoo-shop' ), $result->get_error_message() ) ) . '

'; } return AooShop_Kses::clean( $this->localize( self::part_of( (array) $result['payload'], $part ) ) ); } /** * Raw HTML of a part from a payload. No `parts` (or that part missing): grid = whole html, others ''. * * @param array $payload Page payload. * @param string $part '' (whole page) | header | filters | grid | footer. */ public static function part_of( array $payload, $part ) { $html = isset( $payload['html'] ) ? (string) $payload['html'] : ''; if ( '' === $part ) { return $html; } if ( isset( $payload['parts'] ) && is_array( $payload['parts'] ) ) { if ( isset( $payload['parts'][ $part ] ) && is_string( $payload['parts'][ $part ] ) ) { return $payload['parts'][ $part ]; } if ( isset( $payload['parts']['grid'] ) && is_string( $payload['parts']['grid'] ) ) { return ''; // aoo sent parts but not this one (e.g. footer reserved) — nothing. } } return 'grid' === $part ? $html : ''; } /** One part of the current page (links localized). */ public function part_html( $part ) { if ( 'error' === $this->state ) { return 'grid' === $part ? $this->error_html : ''; } if ( ! $this->payload ) { return ''; } return AooShop_Kses::clean( $this->localize( self::part_of( $this->payload, $part ) ) ); } public function replace_token( $content ) { if ( false !== strpos( $content, self::TOKEN ) ) { $content = str_replace( self::TOKEN, $this->html(), $content ); $found = true; } else { $found = false; } if ( false !== strpos( $content, self::PART_TOKEN ) ) { $map = array(); foreach ( self::PARTS as $part ) { $token = self::part_token( $part ); if ( false !== strpos( $content, $token ) ) { $map[ $token ] = $this->part_html( $part ); } } if ( $map ) { $content = strtr( $content, $map ); $found = true; } } if ( ! $found && $this->active() && in_the_loop() && is_main_query() && $this->template->is_template( get_the_ID() ) ) { // Template page whose content carries no token (Elementor replaced the content after ensure_shortcode() // at p1, and no part widget is on the page) — the shop still has to show. $content .= $this->html(); } return $content; } /** The page body (aoo HTML with links pointed at this site's routes, through the kses allowlist). */ public function html() { if ( 'error' === $this->state ) { return $this->error_html; } if ( ! $this->payload ) { return ''; } return AooShop_Kses::clean( $this->localize( (string) $this->payload['html'] ) ); } // ── URL localization ────────────────────────────────────────────── /** * aoo builds links from ITS copy of the WP config (base_url + prefixes, from ping). Map those onto the * routes this site serves right now — preview prefix, or a dev/staging host — so links always land here. */ private function url_map() { if ( null !== $this->url_map ) { return $this->url_map; } $remote = $this->settings->remote(); $wp = isset( $remote['wordpress'] ) && is_array( $remote['wordpress'] ) ? $remote['wordpress'] : array(); $conf = $this->settings->configured_paths(); $local = $this->settings->paths(); $home = untrailingslashit( home_url() ); $base = ! empty( $wp['base_url'] ) ? untrailingslashit( (string) $wp['base_url'] ) : $home; $map = array(); foreach ( AooShop_Settings::PATH_KEYS as $k ) { $rp = ! empty( $wp[ $k ] ) ? AooShop_Settings::sanitize_path( $wp[ $k ] ) : ''; if ( '' === $rp ) { $rp = $conf[ $k ]; // aoo older than this prefix (collection/budget) → assume ours. } $from = $base . $rp . '/'; $to = $home . $local[ $k ] . '/'; if ( $from !== $to ) { $map[ $from ] = $to; } } // Only route prefixes are mapped — never the bare host (image/upload URLs on the WP domain must stay). $this->url_map = (array) apply_filters( 'aooshop_url_map', $map, $base, $home ); return $this->url_map; } /** Rewrite aoo-built WP URLs inside HTML / a single URL (longest match first — strtr). */ public function localize( $text ) { $map = $this->url_map(); return $map ? strtr( (string) $text, $map ) : (string) $text; } // ── theme integration ───────────────────────────────────────────── public function body_class( $classes ) { if ( $this->active() ) { $route = $this->router->current(); $classes[] = 'aoo-shop'; $classes[] = 'aoo-route-' . sanitize_html_class( $route ? $route['type'] : 'unknown' ); $classes[] = 'aoo-status-' . sanitize_html_class( $this->state ); if ( $this->settings->is_preview() ) { $classes[] = 'aoo-preview'; } } return $classes; } /** Suppress the theme's page title so the aoo h1 is the only one. */ private function theme_adapters() { // Hello Elementor. add_filter( 'hello_elementor_page_title', '__return_false', 99 ); // Astra (title + single banner). add_filter( 'astra_the_title_enabled', '__return_false', 999 ); add_filter( 'astra_single_layout_one_banner_visibility', '__return_false', 999 ); add_filter( 'astra_banner_title_area_visibility', function () { return 'disabled'; }, 999 ); // Astra 4.x layout: normal-width container, unboxed, no sidebar — the boxed style puts the whole shop in // a white card on the site background (verified: astra_get_content_layout() → 'plain-container' gives // body.ast-plain-container; astra_is_content_style_boxed() / astra_is_sidebar_style_boxed() filters). add_filter( 'astra_get_content_layout', function () { return 'plain-container'; }, 999 ); add_filter( 'astra_is_content_layout_boxed', '__return_false', 999 ); // Astra microdata (body itemtype=WebPage, article itemtype=CreativeWork) would wrap the product — the // page's structured data is the aoo JSON-LD only. Read by Astra on `wp` p10 (we run on p1). add_filter( 'astra_schema_enabled', '__return_false', 999 ); add_filter( 'astra_is_sidebar_layout_boxed', '__return_false', 999 ); add_filter( 'astra_page_layout', function () { return 'no-sidebar'; }, 999 ); // Flatsome (default page template prints h1 when "default_title" is on). add_filter( 'theme_mod_default_title', '__return_zero', 999 ); // GeneratePress · OceanWP · Kadence. add_filter( 'generate_show_title', '__return_false', 999 ); add_filter( 'ocean_display_page_header', '__return_false', 999 ); add_filter( 'kadence_post_layout', array( $this, 'kadence_layout' ), 999 ); // Block themes: drop core/post-title of the template page. add_filter( 'render_block_core/post-title', array( $this, 'block_post_title' ), 10, 3 ); do_action( 'aooshop_theme_adapters', $this ); } public function kadence_layout( $layout ) { if ( is_array( $layout ) ) { $layout['title'] = 'hide'; } return $layout; } public function block_post_title( $content, $block, $instance = null ) { $post_id = ( $instance && isset( $instance->context['postId'] ) ) ? (int) $instance->context['postId'] : get_the_ID(); return $this->template->is_template( $post_id ) ? '' : $content; } }