'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();
}
}