홀리스 크릭 가족 치과 · 수리 보고서

증거물 A-2 근거: 커밋 31bc0c1

커밋 시각 · 31bc0c19328caad2ee56059d5b5c5db7c2e96313

휴대폰이 원본 크기 이미지를 내려받았습니다

저장소 자체는 비공개입니다. 이 페이지에 커밋을 기록된 그대로 옮겼습니다.

커밋 메시지

이미지를 자리 크기에 맞춰 srcset과 지연 로딩으로 제공

Fix A-2: serve images at the size of their slot, with srcset and lazy loading

Root cause: the child theme's home template printed the original uploads
(3600x2400 hero, 3000x2000 waiting room) with bare <img src>, and the team
loop did the same for every 1600x1600 portrait: no srcset, no sizes, no
width/height (layout shift), no lazy loading, no alt text.

Fix: go back to wp_get_attachment_image() at the size of each slot, so
WordPress picks from the sizes it generated at upload. The hero loads
eagerly with fetchpriority="high"; everything below the fold loads lazily.

Proof: tests/phpunit/repair/ImagesTest.php.

Exhibit: A-2

변경 내용

git에 기록된 텍스트 파일만 보여 줍니다. +로 시작하는 줄은 추가, −로 시작하는 줄은 삭제입니다.

바뀐 파일

---
 site/wp-content/plugins/hc-team-directory/hc-team-directory.php | 13 ++++-
 site/wp-content/themes/hollis-child/front-page.php              | 24 ++++++++-
 tests/phpunit/repair/ImagesTest.php                             | 79 +++++++++++++++++++++++++++++
 3 files changed, 112 insertions(+), 4 deletions(-)
diff --git a/site/wp-content/plugins/hc-team-directory/hc-team-directory.php b/site/wp-content/plugins/hc-team-directory/hc-team-directory.php
index e572635..9ddb84f 100644
--- a/site/wp-content/plugins/hc-team-directory/hc-team-directory.php
+++ b/site/wp-content/plugins/hc-team-directory/hc-team-directory.php
@@ -107,7 +107,16 @@ function hc_team_directory_render( $limit = -1, $full = true ) {
 		$role_ko  = $wpdb->get_var( "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = $id AND meta_key = '_hollis_ko_role'" );
 		$bio_ko   = $wpdb->get_var( "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = $id AND meta_key = '_hollis_ko_content'" );
 		$photo_id = $wpdb->get_var( "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = $id AND meta_key = '_thumbnail_id'" );
-		$photo    = $photo_id ? wp_get_original_image_url( (int) $photo_id ) : '';
+		$photo    = $photo_id ? wp_get_attachment_image(
+			(int) $photo_id,
+			'medium',
+			false,
+			array(
+				'loading' => 'lazy',
+				'sizes'   => '(max-width: 600px) 45vw, 260px',
+				'alt'     => '',
+			)
+		) : '';
 		$langs    = $wpdb->get_col( "SELECT t.slug FROM {$wpdb->terms} t JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id JOIN {$wpdb->term_relationships} tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy = 'hc_language' AND tr.object_id = $id" );
 		$services = $wpdb->get_col( "SELECT p.ID FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} m ON m.post_id = p.ID WHERE p.post_type = 'hc_service' AND p.post_status = 'publish' AND m.meta_key = 'hc_provider' AND m.meta_value = '$id' ORDER BY p.menu_order" );
 		$ko       = function_exists( 'hollis_lang_is_ko' ) && hollis_lang_is_ko();
@@ -122,7 +131,7 @@ function hc_team_directory_render( $limit = -1, $full = true ) {
 
 		$html .= '<li class="team-card">';
 		if ( $photo ) {
-			$html .= '<div class="team-photo"><img src="' . esc_url( $photo ) . '"></div>';
+			$html .= '<div class="team-photo">' . $photo . '</div>';
 		}
 		$html .= '<h3 class="team-name">' . esc_html( $member->post_title ) . '</h3>';
 		$html .= '<p class="team-role">' . esc_html( $ko && $role_ko ? $role_ko : $role ) . '</p>';
diff --git a/site/wp-content/themes/hollis-child/front-page.php b/site/wp-content/themes/hollis-child/front-page.php
index 83019df..79997a4 100644
--- a/site/wp-content/themes/hollis-child/front-page.php
+++ b/site/wp-content/themes/hollis-child/front-page.php
@@ -34,7 +34,18 @@ $hollis_office = (int) get_theme_mod( 'hollis_office_image' );
 			?>
 		</div>
 		<div class="hero-media">
-			<img src="<?php echo esc_url( wp_get_original_image_url( $hollis_hero ) ); ?>">
+			<?php
+			echo hollis_child_image( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_get_attachment_image() output.
+				$hollis_hero,
+				'hollis-hero',
+				array(
+					'loading'       => 'eager',
+					'fetchpriority' => 'high',
+					'sizes'         => '(max-width: 900px) 100vw, 560px',
+					'alt'           => __( 'Illustration of the Hollis Creek office: a white clapboard building by a creek, with a bench and two maple trees.', 'hollis-child' ),
+				)
+			);
+			?>
 		</div>
 	</div>
 </section>
@@ -77,7 +88,16 @@ $hollis_office = (int) get_theme_mod( 'hollis_office_image' );
 			<p><a href="<?php echo esc_url( home_url( '/insurance/' ) ); ?>"><?php esc_html_e( 'Insurance and payment', 'hollis-child' ); ?></a></p>
 		</div>
 		<div class="visit-media">
-			<img src="<?php echo esc_url( wp_get_original_image_url( $hollis_office ) ); ?>">
+			<?php
+			echo hollis_child_image( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_get_attachment_image() output.
+				$hollis_office,
+				'large',
+				array(
+					'sizes' => '(max-width: 900px) 100vw, 520px',
+					'alt'   => __( 'Illustration of the waiting room: two armchairs, a plant and a children\'s corner with books.', 'hollis-child' ),
+				)
+			);
+			?>
 		</div>
 	</div>
 </section>
diff --git a/tests/phpunit/repair/ImagesTest.php b/tests/phpunit/repair/ImagesTest.php
new file mode 100644
index 0000000..6c6047b
--- /dev/null
+++ b/tests/phpunit/repair/ImagesTest.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Exhibit A-2: the home page and team photos are served at the size of their slot.
+ *
+ * @package Hollis\Tests
+ */
+
+/**
+ * Images.
+ */
+class ImagesTest extends WP_UnitTestCase {
+
+	/**
+	 * Attachment used for every image slot.
+	 *
+	 * @var int
+	 */
+	private $image;
+
+	public function set_up() {
+		parent::set_up();
+		$this->image = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/33772.jpg' );
+		set_theme_mod( 'hollis_hero_image', $this->image );
+		set_theme_mod( 'hollis_office_image', $this->image );
+		$member = self::factory()->post->create(
+			array(
+				'post_type'  => 'hc_team',
+				'post_title' => 'Test Member',
+			)
+		);
+		set_post_thumbnail( $member, $this->image );
+	}
+
+	/**
+	 * Renders the home page template.
+	 *
+	 * @return string
+	 */
+	private function home_html() {
+		$this->go_to( home_url( '/' ) );
+		ob_start();
+		include get_stylesheet_directory() . '/front-page.php';
+		return ob_get_clean();
+	}
+
+	/**
+	 * All <img> tags in the markup.
+	 *
+	 * @param string $html Markup.
+	 * @return string[]
+	 */
+	private function images( $html ) {
+		preg_match_all( '/<img\b[^>]*>/', $html, $m );
+		return $m[0];
+	}
+
+	public function test_hero_has_srcset_dimensions_and_high_priority_not_the_original_upload() {
+		$imgs = $this->images( $this->home_html() );
+		$this->assertNotEmpty( $imgs );
+		$hero = $imgs[0];
+		$this->assertMatchesRegularExpression( '/\ssrcset="[^"]+\s\d+w/', $hero );
+		$this->assertMatchesRegularExpression( '/\ssizes="/', $hero );
+		$this->assertMatchesRegularExpression( '/\swidth="\d+"/', $hero );
+		$this->assertMatchesRegularExpression( '/\sheight="\d+"/', $hero );
+		$this->assertStringContainsString( 'fetchpriority="high"', $hero );
+		$this->assertStringNotContainsString( 'loading="lazy"', $hero );
+		$this->assertStringNotContainsString( wp_get_original_image_url( $this->image ) . '"', $hero );
+	}
+
+	public function test_images_below_the_fold_load_lazily_with_srcset_and_alt() {
+		$imgs = array_slice( $this->images( $this->home_html() ), 1 );
+		$this->assertNotEmpty( $imgs );
+		foreach ( $imgs as $img ) {
+			$this->assertStringContainsString( 'loading="lazy"', $img );
+			$this->assertMatchesRegularExpression( '/\ssrcset="/', $img );
+			$this->assertMatchesRegularExpression( '/\salt="/', $img );
+		}
+	}
+}

증명 테스트

tests/phpunit/repair/ImagesTest.php

tests/phpunit/repair/ImagesTest.php, 커밋된 그대로.

<?php
/**
 * Exhibit A-2: the home page and team photos are served at the size of their slot.
 *
 * @package Hollis\Tests
 */

/**
 * Images.
 */
class ImagesTest extends WP_UnitTestCase {

	/**
	 * Attachment used for every image slot.
	 *
	 * @var int
	 */
	private $image;

	public function set_up() {
		parent::set_up();
		hc_booking_install_table();
		$this->image = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/test-image-large.jpg' /* 3000x2250, like a camera upload */ );
		set_theme_mod( 'hollis_hero_image', $this->image );
		set_theme_mod( 'hollis_office_image', $this->image );
		$member = self::factory()->post->create(
			array(
				'post_type'  => 'hc_team',
				'post_title' => 'Test Member',
			)
		);
		set_post_thumbnail( $member, $this->image );
	}

	/**
	 * Renders the home page template.
	 *
	 * @return string
	 */
	private function home_html() {
		$this->go_to( home_url( '/' ) );
		ob_start();
		include get_stylesheet_directory() . '/front-page.php';
		return ob_get_clean();
	}

	/**
	 * All <img> tags in the markup.
	 *
	 * @param string $html Markup.
	 * @return string[]
	 */
	private function images( $html ) {
		preg_match_all( '/<img\b[^>]*>/', $html, $m );
		return $m[0];
	}

	public function test_hero_has_srcset_dimensions_and_high_priority_not_the_original_upload() {
		$imgs = $this->images( $this->home_html() );
		$this->assertNotEmpty( $imgs );
		$hero = $imgs[0];
		$this->assertMatchesRegularExpression( '/\ssrcset="[^"]+\s\d+w/', $hero );
		$this->assertMatchesRegularExpression( '/\ssizes="/', $hero );
		$this->assertMatchesRegularExpression( '/\swidth="\d+"/', $hero );
		$this->assertMatchesRegularExpression( '/\sheight="\d+"/', $hero );
		$this->assertStringContainsString( 'fetchpriority="high"', $hero );
		$this->assertStringNotContainsString( 'loading="lazy"', $hero );
		$this->assertStringNotContainsString( wp_get_original_image_url( $this->image ) . '"', $hero );
	}

	public function test_images_below_the_fold_load_lazily_with_srcset_and_alt() {
		$imgs = array_slice( $this->images( $this->home_html() ), 1 );
		$this->assertNotEmpty( $imgs );
		foreach ( $imgs as $img ) {
			$this->assertStringContainsString( 'loading="lazy"', $img );
			$this->assertMatchesRegularExpression( '/\ssrcset="/', $img );
			$this->assertMatchesRegularExpression( '/\salt="/', $img );
		}
	}
}

증거물 A-2로 돌아가기