@@ -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,93 @@ 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
796+ * [width, height].
797+ *
798+ * @param array $image_dimensions
799+ * Dimensions keyed by breakpoint, subkeyed by multiplier. Example:
800+ * @code
801+ * [
802+ * 'mobile' => ['1x' => [400, 300], '2x' => [800, 600]],
803+ * 'sm' => ['1x' => [640, 480], '2x' => [1280, 960]],
804+ * 'md' => ['1x' => [768, 576], '2x' => [1536, 1152]],
805+ * 'lg' => ['1x' => [1024, 768], '2x' => [2048, 1536]],
806+ * ]
807+ * @endcode
808+ * @param string $id
809+ * The placeholder image ID or seed.
810+ * @param string $id_type
811+ * The type of the ID, either 'id' or 'seed'.
812+ * @param string $alt
813+ * Alt text.
814+ *
815+ * @return array
816+ * A render array for a <picture> element.
817+ */
818+ private function buildResponsiveImage (array $ image_dimensions , string $ id = '' , string $ id_type = 'id ' , string $ alt = '' ): array {
819+ $ breakpoint_media_queries = [
820+ '2xl ' => 'all and (min-width: 1536px) ' ,
821+ 'xl ' => 'all and (min-width: 1280px) ' ,
822+ 'lg ' => 'all and (min-width: 1024px) ' ,
823+ 'md ' => 'all and (min-width: 768px) ' ,
824+ 'sm ' => 'all and (min-width: 640px) ' ,
825+ 'mobile ' => '' ,
826+ ];
827+
828+ $ sources = [];
829+ // Iterate from largest to smallest so the browser picks the first match.
830+ foreach ($ breakpoint_media_queries as $ breakpoint => $ media_query ) {
831+ if (!isset ($ image_dimensions [$ breakpoint ])) {
832+ continue ;
833+ }
834+
835+ $ srcset_parts = [];
836+ foreach ($ image_dimensions [$ breakpoint ] as $ multiplier => $ dimensions ) {
837+ [$ width , $ height ] = $ dimensions ;
838+ $ url = $ this ->getPlaceholderImage ($ width , $ height , $ id , $ id_type );
839+ $ srcset_parts [] = "$ url {$ multiplier }" ;
840+ }
841+
842+ $ source = [
843+ '#type ' => 'html_tag ' ,
844+ '#tag ' => 'source ' ,
845+ '#attributes ' => [
846+ 'srcset ' => implode (', ' , $ srcset_parts ),
847+ ],
848+ ];
849+ if (!empty ($ media_query )) {
850+ $ source ['#attributes ' ]['media ' ] = $ media_query ;
851+ }
852+ $ sources [] = $ source ;
853+ }
854+
855+ // Fallback <img> using the mobile 1x dimensions.
856+ $ fallback_breakpoint = isset ($ image_dimensions ['mobile ' ]) ? 'mobile ' : array_key_first ($ image_dimensions );
857+ $ fallback_dimensions = $ image_dimensions [$ fallback_breakpoint ]['1x ' ];
858+ [$ fallback_width , $ fallback_height ] = $ fallback_dimensions ;
859+
860+ $ sources [] = [
861+ '#type ' => 'html_tag ' ,
862+ '#tag ' => 'img ' ,
863+ '#attributes ' => [
864+ 'src ' => $ this ->getPlaceholderImage ($ fallback_width , $ fallback_height , $ id , $ id_type ),
865+ 'alt ' => $ alt ,
866+ 'width ' => $ fallback_width ,
867+ 'height ' => $ fallback_height ,
868+ ],
869+ ];
870+
871+ return [
872+ '#type ' => 'html_tag ' ,
873+ '#tag ' => 'picture ' ,
874+ 'sources ' => $ sources ,
875+ ];
876+ }
877+
775878 /**
776879 * Build text with HTML.
777880 *
0 commit comments