get( 'Version' ); wp_enqueue_style( 'renku-style', get_stylesheet_uri(), array(), $version ); // Singular post views render the same feed grid as the home page so the // lightbox can auto-open with the requested post sitting over the feed // (Instagram-desktop behavior on deep-linked / refreshed URLs). if ( is_home() || is_front_page() || is_archive() || is_singular( 'post' ) ) { wp_enqueue_script( 'renku-infinite-scroll', get_stylesheet_directory_uri() . '/assets/js/infinite-scroll.js', array(), $version, array( 'in_footer' => true, 'strategy' => 'defer' ) ); wp_enqueue_script( 'renku-lightbox', get_stylesheet_directory_uri() . '/assets/js/lightbox.js', array(), $version, array( 'in_footer' => true, 'strategy' => 'defer' ) ); wp_localize_script( 'renku-lightbox', 'renkuI18n', array( 'close' => __( 'Close', 'renku' ), 'previousImage' => __( 'Previous image', 'renku' ), 'nextImage' => __( 'Next image', 'renku' ), 'unmute' => __( 'Unmute', 'renku' ), 'mute' => __( 'Mute', 'renku' ), 'imagePosition' => __( 'Image position', 'renku' ), 'comments' => __( 'Comments', 'renku' ), 'likes' => __( 'Likes', 'renku' ), 'share' => __( 'Share', 'renku' ), 'linkCopied' => __( 'Link copied', 'renku' ), 'linkCopyFail' => __( 'Couldn’t copy link', 'renku' ), 'loadingComments' => __( 'Loading comments…', 'renku' ), /* translators: %d: image number in a multi-image post (1-based). */ 'imageNumber' => __( 'Image %d', 'renku' ), ) ); wp_enqueue_script( 'renku-bubble-shell', get_stylesheet_directory_uri() . '/assets/js/bubble-shell.js', array(), $version, array( 'in_footer' => true, 'strategy' => 'defer' ) ); wp_enqueue_script( 'renku-card-preview', get_stylesheet_directory_uri() . '/assets/js/card-preview.js', array(), $version, array( 'in_footer' => true, 'strategy' => 'defer' ) ); } } /** * On singular post views, emit a JSON payload describing the post so the * lightbox can auto-open over the feed grid on page load. Mirrors the * data-* attributes set by render_post_card() — keep these in sync. */ add_action( 'wp_head', __NAMESPACE__ . '\\emit_open_post_data' ); function emit_open_post_data() { if ( ! is_singular( 'post' ) ) { return; } $post_id = get_queried_object_id(); if ( ! $post_id ) { return; } $poster = get_post_meta( $post_id, '_renku_poster_url', true ); if ( ! $poster && has_post_thumbnail( $post_id ) ) { $poster = get_the_post_thumbnail_url( $post_id, 'large' ); } $content = get_post_field( 'post_content', $post_id ); $video_src = ''; if ( preg_match( '/]+src=["\']([^"\']+)["\']/', $content, $vm ) ) { $video_src = $vm[1]; } $image_urls = array(); if ( preg_match_all( '/]+src=["\']([^"\']+)["\']/', $content, $im_all ) ) { $image_urls = array_values( array_unique( $im_all[1] ) ); } if ( ! $poster && ! empty( $image_urls ) ) { $poster = $image_urls[0]; } $is_gallery = ( count( $image_urls ) >= 2 ) && ! $video_src; $author_id = (int) get_post_field( 'post_author', $post_id ); $data = array( 'postId' => (string) $post_id, 'url' => get_permalink( $post_id ), 'title' => get_the_title( $post_id ), 'author' => get_the_author_meta( 'display_name', $author_id ), 'authorUrl' => get_author_posts_url( $author_id ), 'avatar' => get_avatar_url( $author_id, array( 'size' => 96 ) ), 'poster' => $poster, 'date' => get_the_date( 'F j, Y', $post_id ), ); if ( $video_src ) { $data['video'] = $video_src; } if ( $is_gallery ) { $data['images'] = wp_json_encode( $image_urls ); } $media_w = (int) get_post_meta( $post_id, '_renku_media_w', true ); $media_h = (int) get_post_meta( $post_id, '_renku_media_h', true ); if ( $media_w && $media_h ) { $data['w'] = (string) $media_w; $data['h'] = (string) $media_h; } printf( "\n", wp_json_encode( $data ) ); } /** * Preload Inter Variable so it lands before first paint. Avoids the * brief system-font flash while Inter loads. */ add_action( 'wp_head', __NAMESPACE__ . '\\preload_inter', 1 ); function preload_inter() { $url = get_stylesheet_directory_uri() . '/assets/fonts/InterVariable.woff2'; printf( '' . "\n", esc_url( $url ) ); } /** * Inline the Phosphor icon sprite once per page so * works anywhere. See README for the icon system. */ add_action( 'wp_body_open', __NAMESPACE__ . '\\inject_icon_sprite' ); function inject_icon_sprite() { $path = get_theme_file_path( 'assets/icons/sprite.svg' ); if ( is_readable( $path ) ) { readfile( $path ); } } /** * Render a Renku icon by name (e.g. 'heart', 'comment', 'play'). * Icons are defined as in assets/icons/sprite.svg. */ function renku_icon( $name, $size = 20, $label = '' ) { $name = preg_replace( '/[^a-z0-9-]/', '', strtolower( $name ) ); $attrs = sprintf( 'class="renku-icon" width="%1$d" height="%1$d" aria-hidden="%2$s"%3$s', (int) $size, $label ? 'false' : 'true', $label ? ' role="img" aria-label="' . esc_attr( $label ) . '"' : '' ); return sprintf( '', $attrs, esc_attr( $name ) ); } /** * Register the dynamic post-card block. Runs per-post inside wp:post-template, * so it has correct loop context. */ add_action( 'init', __NAMESPACE__ . '\\register_post_card_block' ); function register_post_card_block() { register_block_type( 'renku/post-card', array( 'api_version' => 2, 'render_callback' => __NAMESPACE__ . '\\render_post_card', ) ); } function render_post_card() { // Track loop position ourselves. The Query block's post-template doesn't // swap the global $wp_query, so $wp_query->current_post isn't reliable // inside this render callback. A request-scoped counter is. static $position = 0; $post_id = get_the_ID(); // Hero treatment is reserved for the very first card of the very first // page. Infinite scroll fetches subsequent pages via ?query-1-page=N; // suppress hero on those so we only ever have one per feed. $paged_q = isset( $_GET['query-1-page'] ) ? (int) $_GET['query-1-page'] : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $is_hero = ( 0 === $position && 1 === $paged_q ); $position++; // Thumbnail: prefer VideoPress poster (cached in meta), fall back to featured image, // then to the first in the post content (covers the rare image-only posts). $poster = get_post_meta( $post_id, '_renku_poster_url', true ); if ( ! $poster && has_post_thumbnail( $post_id ) ) { $poster = get_the_post_thumbnail_url( $post_id, 'large' ); } // Video src (mp4) — extracted from the rewritten wp:video block content for local dev. $video_src = ''; $content = get_post_field( 'post_content', $post_id ); if ( preg_match( '/]+src=["\']([^"\']+)["\']/', $content, $vm ) ) { $video_src = $vm[1]; } // Collect every image URL in the post content so the lightbox can present // multi-image posts as a gallery. De-duped, in document order. $image_urls = array(); if ( preg_match_all( '/]+src=["\']([^"\']+)["\']/', $content, $im_all ) ) { $image_urls = array_values( array_unique( $im_all[1] ) ); } if ( ! $poster && ! empty( $image_urls ) ) { $poster = $image_urls[0]; } // Multi-image post: ≥2 distinct images AND no video. Video posts take // precedence on the corner badge — a clip is more salient than a count. $is_gallery = ( count( $image_urls ) >= 2 ) && ! $video_src; $author_id = (int) get_post_field( 'post_author', $post_id ); $author_name = get_the_author_meta( 'display_name', $author_id ); $author_url = get_author_posts_url( $author_id ); $avatar_url = get_avatar_url( $author_id, array( 'size' => 96 ) ); $comments = (int) get_comments_number( $post_id ); $title = get_the_title(); $permalink = get_permalink(); $media_w = (int) get_post_meta( $post_id, '_renku_media_w', true ); $media_h = (int) get_post_meta( $post_id, '_renku_media_h', true ); $date_short = get_the_date( 'M j' ); $date_iso = get_the_date( 'c' ); $date_long = get_the_date( 'F j, Y' ); $card_class = 'renku-card'; if ( $is_hero ) { $card_class .= ' renku-card--hero'; } // Cap the stagger delay so a long feed doesn't take forever to settle. // Use the pre-increment position so the first card is index 0. $stagger_index = min( $position - 1, 11 ); ob_start(); ?> data-video="" data-images="" data-w="" data-h="" >