'dynamirss', 'title' => __( 'DynamiRSS', 'dynamirss' ), 'icon' => 'rss', ) ); return $categories; } public function register_blocks(): void { foreach ( self::BLOCK_DIRS as $rel ) { $dir = DYNAMIRSS_DIR . 'build/' . $rel; if ( file_exists( $dir . '/block.json' ) ) { register_block_type( $dir ); } } } public function register_assets(): void { $css = DYNAMIRSS_DIR . 'assets/frontend.css'; $js = DYNAMIRSS_DIR . 'assets/frontend.js'; wp_register_style( 'dynamirss-frontend', DYNAMIRSS_URL . 'assets/frontend.css', array(), file_exists( $css ) ? (string) filemtime( $css ) : DYNAMIRSS_VERSION ); wp_register_script( 'dynamirss-frontend', DYNAMIRSS_URL . 'assets/frontend.js', array(), file_exists( $js ) ? (string) filemtime( $js ) : DYNAMIRSS_VERSION, true ); wp_localize_script( 'dynamirss-frontend', 'DynamiRSSFrontend', array( 'restUrl' => esc_url_raw( rest_url( DynamiRSS_Rest_Controller::API_NAMESPACE . '/' ) ), 'i18n' => array( 'loadError' => __( 'Could not load articles.', 'dynamirss' ), 'networkError' => __( 'Could not reach the server. Please try again.', 'dynamirss' ), ), ) ); } /** * Render one card per article by instantiating each field block in the * template fresh, with the article injected as block context. * * @param array[] $articles * @param array[] $template_blocks Parsed blocks (parse_blocks() shape). */ public static function render_articles( array $articles, array $template_blocks ): string { if ( empty( $template_blocks ) ) { return ''; } $html = ''; foreach ( $articles as $article ) { $card = ''; foreach ( $template_blocks as $parsed_block ) { if ( empty( $parsed_block['blockName'] ) ) { continue; // skip whitespace-only text nodes between block comments } $wp_block = new WP_Block( $parsed_block, array( self::CONTEXT_KEY => $article ) ); $card .= $wp_block->render(); } $html .= '
' . $card . '
'; } return $html; } /** * Cache a per-article template (the article-template block's innerBlocks) * under a short hash so the REST live-filter endpoint can re-render with * the exact same template without trusting client-supplied block markup. * Only ever written here, during a real page render — never from a * REST request — so get_cached_template() can't be used to smuggle * arbitrary block markup in from an anonymous visitor. */ public static function cache_template( array $template_blocks ): string { $markup = serialize_blocks( $template_blocks ); $hash = substr( md5( $markup ), 0, 16 ); set_transient( 'dynamirss_tpl_' . $hash, $markup, DAY_IN_SECONDS ); return $hash; } /** @return array[] Parsed blocks, or the default template if the hash is unknown/expired. */ public static function get_cached_template( string $hash ): array { $markup = $hash ? get_transient( 'dynamirss_tpl_' . $hash ) : false; if ( ! $markup ) { return self::default_template_blocks(); } return array_values( array_filter( parse_blocks( $markup ), static fn( $b ) => ! empty( $b['blockName'] ) ) ); } /** Fallback template used when a query block has an empty article-template (e.g. right after insertion). */ public static function default_template_blocks(): array { $markup = "\n" . "\n" . "\n" . "\n" . "\n" . ""; return array_values( array_filter( parse_blocks( $markup ), static fn( $b ) => ! empty( $b['blockName'] ) ) ); } /** Find the article-template block's own innerBlocks among a query block's innerBlocks. */ public static function extract_template_blocks( array $query_inner_blocks ): array { foreach ( $query_inner_blocks as $inner ) { if ( ( $inner['blockName'] ?? '' ) === 'dynamirss/article-template' ) { return array_values( array_filter( $inner['innerBlocks'] ?? array(), static fn( $b ) => ! empty( $b['blockName'] ) ) ); } } return array(); } }