@@ -619,8 +619,24 @@ protected function getTextStyles(): array {
619619 protected function getHeroImage (): array {
620620 $ url = Url::fromRoute ('<front> ' );
621621
622+ $ image_dimensions = [
623+ 'mobile ' => [
624+ '1x ' => [768 , 576 ],
625+ '2x ' => [1536 , 1152 ],
626+ ],
627+ 'md ' => [
628+ '1x ' => [1024 , 580 ],
629+ '2x ' => [1536 , 1160 ],
630+ ],
631+ 'lg ' => [
632+ '1x ' => [1440 , 800 ],
633+ '2x ' => [2880 , 1600 ],
634+ ],
635+ ];
636+ $ image = $ this ->buildResponsiveImage ($ image_dimensions , 'image_item ' , 'seed ' );
637+
622638 return $ this ->buildElementHeroImage (
623- $ this -> buildImage ( $ this -> getPlaceholderImage ( 1600 , 400 )) ,
639+ $ image ,
624640 $ this ->getRandomTitle (),
625641 $ this ->getRandomTitle (),
626642 Link::fromTextAndUrl ('Learn more ' , $ url ),
@@ -772,6 +788,92 @@ protected function buildImage(string $url) {
772788 ];
773789 }
774790
791+ /**
792+ * Build a responsive image using a <picture> element with breakpoint sources.
793+ *
794+ * Uses server_theme breakpoints to produce <source> elements per breakpoint.
795+ * Each breakpoint key maps to srcset multipliers (1x, 2x) with [width, height].
796+ *
797+ * @param array $image_dimensions
798+ * Dimensions keyed by breakpoint, subkeyed by multiplier. Example:
799+ * @code
800+ * [
801+ * 'mobile' => ['1x' => [400, 300], '2x' => [800, 600]],
802+ * 'sm' => ['1x' => [640, 480], '2x' => [1280, 960]],
803+ * 'md' => ['1x' => [768, 576], '2x' => [1536, 1152]],
804+ * 'lg' => ['1x' => [1024, 768], '2x' => [2048, 1536]],
805+ * ]
806+ * @endcode
807+ * @param string $id
808+ * The placeholder image ID or seed.
809+ * @param string $id_type
810+ * The type of the ID, either 'id' or 'seed'.
811+ * @param string $alt
812+ * Alt text.
813+ *
814+ * @return array
815+ * A render array for a <picture> element.
816+ */
817+ private function buildResponsiveImage (array $ image_dimensions , string $ id = '' , string $ id_type = 'id ' , string $ alt = '' ): array {
818+ $ breakpoint_media_queries = [
819+ '2xl ' => 'all and (min-width: 1536px) ' ,
820+ 'xl ' => 'all and (min-width: 1280px) ' ,
821+ 'lg ' => 'all and (min-width: 1024px) ' ,
822+ 'md ' => 'all and (min-width: 768px) ' ,
823+ 'sm ' => 'all and (min-width: 640px) ' ,
824+ 'mobile ' => '' ,
825+ ];
826+
827+ $ sources = [];
828+ // Iterate from largest to smallest so the browser picks the first match.
829+ foreach ($ breakpoint_media_queries as $ breakpoint => $ media_query ) {
830+ if (!isset ($ image_dimensions [$ breakpoint ])) {
831+ continue ;
832+ }
833+
834+ $ srcset_parts = [];
835+ foreach ($ image_dimensions [$ breakpoint ] as $ multiplier => $ dimensions ) {
836+ [$ width , $ height ] = $ dimensions ;
837+ $ url = $ this ->getPlaceholderImage ($ width , $ height , $ id , $ id_type );
838+ $ srcset_parts [] = "$ url {$ multiplier }" ;
839+ }
840+
841+ $ source = [
842+ '#type ' => 'html_tag ' ,
843+ '#tag ' => 'source ' ,
844+ '#attributes ' => [
845+ 'srcset ' => implode (', ' , $ srcset_parts ),
846+ ],
847+ ];
848+ if (!empty ($ media_query )) {
849+ $ source ['#attributes ' ]['media ' ] = $ media_query ;
850+ }
851+ $ sources [] = $ source ;
852+ }
853+
854+ // Fallback <img> using the mobile 1x dimensions.
855+ $ fallback_breakpoint = isset ($ image_dimensions ['mobile ' ]) ? 'mobile ' : array_key_first ($ image_dimensions );
856+ $ fallback_dimensions = $ image_dimensions [$ fallback_breakpoint ]['1x ' ];
857+ [$ fallback_width , $ fallback_height ] = $ fallback_dimensions ;
858+
859+ $ sources [] = [
860+ '#type ' => 'html_tag ' ,
861+ '#tag ' => 'img ' ,
862+ '#attributes ' => [
863+ 'src ' => $ this ->getPlaceholderImage ($ fallback_width , $ fallback_height , $ id , $ id_type ),
864+ 'alt ' => $ alt ,
865+ 'width ' => $ fallback_width ,
866+ 'height ' => $ fallback_height ,
867+ ],
868+ ];
869+
870+ return [
871+ '#type ' => 'html_tag ' ,
872+ '#tag ' => 'picture ' ,
873+ 'sources ' => $ sources ,
874+ ];
875+ }
876+
775877 /**
776878 * Build text with HTML.
777879 *
0 commit comments