diff --git a/CHANGELOG.md b/CHANGELOG.md index 060f0f97..39a4202f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - 확장 기능이 화면을 그릴 때, 방문자의 접속 기기 종류처럼 요청마다 달라지는 정보를 화면 구성 데이터에 함께 실어 보낼 수 있는 통로를 마련했습니다. 쇼핑몰의 애플페이가 iOS 기기에서만 표시되도록 하는 데 이 통로가 쓰입니다. - 확장 기능이 특정 요청 경로에만 자기 처리 로직을 끼워 넣을 수 있는 표준 연결 방식을 마련했습니다. 각 확장은 자기가 개입할 대상 경로를 명시해 선언하고, 코어가 요청이 들어올 때 대상에 해당하는 확장 로직만 골라 실행합니다. 이를 통해 쿠키 동의 처리나 결제 입금통보의 발신 서버 확인 같은 기능이 필요한 경로에서만 정확히 동작하도록 정리했습니다. +### Fixed + +- 검색엔진 노출용으로 만들어 내보내는 페이지에 실제로 존재하지 않는 스타일 파일 주소(`/build/assets/app.css`)가 포함되어, 브라우저 콘솔에 404 오류가 남던 문제를 수정했습니다. 이 주소는 화면 표시에 쓰이지 않는 파일을 가리키던 불필요한 참조였으며, 검색엔진 노출용 페이지의 스타일은 템플릿이 제공하는 파일이 그대로 담당하므로 화면에는 영향이 없었습니다. 이제 이 참조를 제거해 오류가 발생하지 않습니다. (#64 @bigmsg 님께서 제보해주셨습니다.) + ## [7.0.4] - 2026-07-14 ### Security diff --git a/app/Seo/SeoRenderer.php b/app/Seo/SeoRenderer.php index cf2ab421..65f78ddb 100644 --- a/app/Seo/SeoRenderer.php +++ b/app/Seo/SeoRenderer.php @@ -464,7 +464,6 @@ class SeoRenderer implements SeoRendererInterface 'googleAnalyticsId' => $meta['googleAnalyticsId'], 'googleVerification' => $meta['googleVerification'], 'naverVerification' => $meta['naverVerification'], - 'cssPath' => $this->getCssPath(), 'stylesheets' => $allStylesheets, 'extraHeadTags' => '', 'extraBodyEnd' => '', @@ -732,28 +731,6 @@ class SeoRenderer implements SeoRendererInterface return $tags; } - /** - * Vite 빌드 CSS 경로를 반환합니다. - * - * @return string CSS 경로 - */ - private function getCssPath(): string - { - $manifestPath = public_path('build/manifest.json'); - if (file_exists($manifestPath)) { - $manifest = json_decode(file_get_contents($manifestPath), true); - foreach ($manifest as $entry) { - if (isset($entry['css'])) { - foreach ($entry['css'] as $css) { - return '/build/'.$css; - } - } - } - } - - return '/build/assets/app.css'; - } - /** * SEO 렌더링용 _global 컨텍스트를 구성합니다. * diff --git a/resources/views/seo-preview.blade.php b/resources/views/seo-preview.blade.php index 2a05bb3e..df6019c9 100644 --- a/resources/views/seo-preview.blade.php +++ b/resources/views/seo-preview.blade.php @@ -4,7 +4,7 @@ 검색엔진 탭·레이아웃·모듈/코어 SEO 환경설정이 "설정/정의한 것의 산출물" 만 렌더한다. 운영 seo.blade.php 와 달리 다음은 SEO 설정 산출물이 아니므로 미포함: - body 컴포넌트 마크업($bodyHtml) — 페이지 본문(검색엔진 탭과 무관) - - CSS($cssPath/$stylesheets) — 에셋 + - CSS($stylesheets) — 에셋 - 시스템 기본 메타(charset/viewport/generatorTag) — SEO 설정과 무관 - Google Analytics 스크립트 — 추적(검색 노출 산출물 아님) - extraHeadTags/extraBodyEnd — 훅 주입 비-SEO 슬롯 diff --git a/resources/views/seo.blade.php b/resources/views/seo.blade.php index 957c135d..926345c3 100644 --- a/resources/views/seo.blade.php +++ b/resources/views/seo.blade.php @@ -27,8 +27,7 @@ {{-- Twitter Card --}} {!! $twitterTags ?? '' !!} - {{-- CSS --}} - + {{-- CSS (템플릿 CSS만 — 코어 app.css 는 사용자 화면에서 미참조하는 Laravel 기본 빌드 파일이라 미포함) --}} @foreach($stylesheets as $stylesheet) @endforeach diff --git a/tests/Feature/Seo/SeoBladeCssTest.php b/tests/Feature/Seo/SeoBladeCssTest.php new file mode 100644 index 00000000..db485ac0 --- /dev/null +++ b/tests/Feature/Seo/SeoBladeCssTest.php @@ -0,0 +1,77 @@ + 'ko', + 'title' => '베이직 오버핏 코튼 티셔츠', + 'titleSuffix' => ' | 코지홈', + 'description' => '사계절 입기 좋은 오버핏 기본 티셔츠', + 'keywords' => '티셔츠,오버핏', + 'canonicalUrl' => 'https://example.com/shop/products/1', + 'hreflangTags' => '', + 'ogTags' => '', + 'twitterTags' => '', + 'jsonLd' => '{"@type":"Product","name":"베이직 오버핏 코튼 티셔츠"}', + 'bodyHtml' => '
', + 'googleAnalyticsId' => '', + 'googleVerification' => '', + 'naverVerification' => '', + 'stylesheets' => [ + '/api/templates/assets/sirsoft-basic/css/components.css', + ], + 'extraHeadTags' => '', + 'extraBodyEnd' => '', + 'generatorTag' => '', + ], $overrides); + } + + private function renderSeo(array $overrides = []): string + { + return View::make('seo', $this->viewData($overrides))->render(); + } + + public function test_봇_HTML에_코어_app_css_고정경로_링크가_없다(): void + { + $out = $this->renderSeo(); + + $this->assertStringNotContainsString('/build/assets/app.css', $out); + $this->assertStringNotContainsString('/build/', $out); + } + + public function test_템플릿_stylesheets는_정상_링크된다(): void + { + $out = $this->renderSeo(); + + $this->assertStringContainsString( + '', + $out, + ); + } + + public function test_cssPath_변수_없이도_예외없이_렌더된다(): void + { + // cssPath 를 뷰 데이터에서 완전히 제거해도 (undefined variable 등) 예외가 없어야 한다. + $out = $this->renderSeo(); + + $this->assertStringContainsString('베이직 오버핏 코튼 티셔츠 | 코지홈', $out); + } +} diff --git a/tests/Feature/Seo/SeoPreviewBladeTest.php b/tests/Feature/Seo/SeoPreviewBladeTest.php index a593070a..68d9ff13 100644 --- a/tests/Feature/Seo/SeoPreviewBladeTest.php +++ b/tests/Feature/Seo/SeoPreviewBladeTest.php @@ -33,7 +33,6 @@ class SeoPreviewBladeTest extends TestCase 'googleAnalyticsId' => 'G-XXXX', 'googleVerification' => 'gverify123', 'naverVerification' => 'nverify456', - 'cssPath' => '/build/assets/app.css', 'stylesheets' => ['/api/templates/assets/sirsoft-basic/css/components.css', 'https://cdnjs.cloudflare.com/font-awesome.css'], 'extraHeadTags' => '', 'extraBodyEnd' => '', diff --git a/tests/Unit/Seo/RealWorldRenderingTest.php b/tests/Unit/Seo/RealWorldRenderingTest.php index 8f71dcd8..aef5dbf4 100644 --- a/tests/Unit/Seo/RealWorldRenderingTest.php +++ b/tests/Unit/Seo/RealWorldRenderingTest.php @@ -770,27 +770,6 @@ class RealWorldRenderingTest extends TestCase } } - /** - * SeoRenderer.getCssPath()가 코어 Tailwind CSS를 반환해야 합니다. - * (코어 app.css가 아닌 Tailwind를 포함한 빌드 CSS) - */ - public function test_css_path_includes_tailwind(): void - { - // getCssPath()는 Vite manifest에서 CSS를 찾으므로, - // manifest가 없으면 fallback '/build/assets/app.css'를 반환합니다. - // 실제로는 Tailwind 클래스가 포함된 CSS여야 합니다. - $renderer = app(\App\Seo\SeoRenderer::class); - - // Reflection으로 getCssPath() 호출 - $method = new \ReflectionMethod($renderer, 'getCssPath'); - $method->setAccessible(true); - $cssPath = $method->invoke($renderer); - - $this->assertIsString($cssPath); - // CSS 경로가 존재해야 함 - $this->assertNotEmpty($cssPath); - } - // ======================================================== // 9. 복잡한 표현식 + 조건부 렌더링 검증 // ======================================================== diff --git a/tests/Unit/Seo/SeoRendererTest.php b/tests/Unit/Seo/SeoRendererTest.php index b24a5f07..654fe8c8 100644 --- a/tests/Unit/Seo/SeoRendererTest.php +++ b/tests/Unit/Seo/SeoRendererTest.php @@ -455,9 +455,16 @@ class SeoRendererTest extends TestCase } /** - * CSS 경로가 뷰 데이터에 포함됩니다. + * 코어 Laravel 에셋 빌드 산출물(app.css)은 뷰 데이터에 주입되지 않습니다. + * + * seo.blade.php 가 참조하던 코어 app.css 는 실제 사용자 화면 + * (app.blade.php·admin.blade.php)에서 쓰이지 않는 Laravel 기본 빌드 파일이며, + * Vite manifest 구조상 해시 파일로만 생성되어 고정 경로 `/build/assets/app.css` + * 는 404 였다. 봇 렌더링에 필요한 스타일은 템플릿 CSS($stylesheets)가 담당하므로 + * 코어 app.css 참조를 제거했다. 이 테스트는 cssPath 잔재가 재유입되지 않도록 가드한다. + * (공개#64 — 봇 SEO HTML CSS 404 제거) */ - public function test_render_includes_css_path(): void + public function test_render_does_not_inject_core_app_css_path(): void { $request = Request::create('/about'); @@ -486,11 +493,12 @@ class SeoRendererTest extends TestCase ->andReturn(''); $viewMock = Mockery::mock(\Illuminate\View\View::class); - $viewMock->shouldReceive('render')->once()->andReturn(''); + $viewMock->shouldReceive('render')->once()->andReturn(''); View::shouldReceive('make') ->with('seo', Mockery::on(function ($data) { - return isset($data['cssPath']) && is_string($data['cssPath']); + // cssPath 키가 더 이상 뷰 데이터에 존재하지 않아야 한다. + return ! array_key_exists('cssPath', $data); })) ->once() ->andReturn($viewMock);