fix(seo): 봇 SEO HTML의 코어 app.css 404 참조 제거 (, )
SeoRenderer.getCssPath가 구 Vite manifest 구조만 파싱해 존재하지 않는 고정 경로 /build/assets/app.css 를 반환, 봇 렌더링 HTML(seo.blade)에서 CSS 404가 발생했다. 해당 파일은 실제 사용자 화면에서 미참조하는 Laravel 기본 빌드 산출물이므로, 담당자 답변 방향대로 참조 자체와 getCssPath 메서드를 제거했다. 봇 스타일은 템플릿 CSS($stylesheets)가 그대로 담당한다.
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
- 확장 기능이 화면을 그릴 때, 방문자의 접속 기기 종류처럼 요청마다 달라지는 정보를 화면 구성 데이터에 함께 실어 보낼 수 있는 통로를 마련했습니다. 쇼핑몰의 애플페이가 iOS 기기에서만 표시되도록 하는 데 이 통로가 쓰입니다.
|
||||
- 확장 기능이 특정 요청 경로에만 자기 처리 로직을 끼워 넣을 수 있는 표준 연결 방식을 마련했습니다. 각 확장은 자기가 개입할 대상 경로를 명시해 선언하고, 코어가 요청이 들어올 때 대상에 해당하는 확장 로직만 골라 실행합니다. 이를 통해 쿠키 동의 처리나 결제 입금통보의 발신 서버 확인 같은 기능이 필요한 경로에서만 정확히 동작하도록 정리했습니다.
|
||||
|
||||
### Fixed
|
||||
|
||||
- 검색엔진 노출용으로 만들어 내보내는 페이지에 실제로 존재하지 않는 스타일 파일 주소(`/build/assets/app.css`)가 포함되어, 브라우저 콘솔에 404 오류가 남던 문제를 수정했습니다. 이 주소는 화면 표시에 쓰이지 않는 파일을 가리키던 불필요한 참조였으며, 검색엔진 노출용 페이지의 스타일은 템플릿이 제공하는 파일이 그대로 담당하므로 화면에는 영향이 없었습니다. 이제 이 참조를 제거해 오류가 발생하지 않습니다. (#64 @bigmsg 님께서 제보해주셨습니다.)
|
||||
|
||||
## [7.0.4] - 2026-07-14
|
||||
|
||||
### Security
|
||||
|
||||
@@ -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 컨텍스트를 구성합니다.
|
||||
*
|
||||
|
||||
@@ -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 슬롯
|
||||
|
||||
@@ -27,8 +27,7 @@
|
||||
{{-- Twitter Card --}}
|
||||
{!! $twitterTags ?? '' !!}
|
||||
|
||||
{{-- CSS --}}
|
||||
<link rel="stylesheet" href="{{ $cssPath }}">
|
||||
{{-- CSS (템플릿 CSS만 — 코어 app.css 는 사용자 화면에서 미참조하는 Laravel 기본 빌드 파일이라 미포함) --}}
|
||||
@foreach($stylesheets as $stylesheet)
|
||||
<link rel="stylesheet" href="{{ $stylesheet }}">
|
||||
@endforeach
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Seo;
|
||||
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 운영 seo.blade.php 의 CSS 링크 렌더링 테스트.
|
||||
*
|
||||
* 봇 렌더링 HTML 에는 코어 Laravel 기본 에셋 빌드 산출물(app.css)에 대한
|
||||
* 고정 경로 `/build/assets/app.css` 링크가 포함되지 않아야 한다. 해당 파일은
|
||||
* 실제 사용자 화면(app.blade.php·admin.blade.php)에서 참조되지 않는 Laravel
|
||||
* 기본 빌드 파일이며, Vite manifest 가 해시 파일로만 생성하므로 고정 경로는
|
||||
* 항상 404 였다. 봇 스타일은 템플릿 CSS($stylesheets)가 담당한다.
|
||||
* (공개#64 — 봇 SEO HTML CSS 404 제거)
|
||||
*/
|
||||
class SeoBladeCssTest extends TestCase
|
||||
{
|
||||
/** 운영 seo.blade.php 가 받는 $viewData 형태(대표 값, cssPath 미포함) */
|
||||
private function viewData(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'locale' => 'ko',
|
||||
'title' => '베이직 오버핏 코튼 티셔츠',
|
||||
'titleSuffix' => ' | 코지홈',
|
||||
'description' => '사계절 입기 좋은 오버핏 기본 티셔츠',
|
||||
'keywords' => '티셔츠,오버핏',
|
||||
'canonicalUrl' => 'https://example.com/shop/products/1',
|
||||
'hreflangTags' => '',
|
||||
'ogTags' => '<meta property="og:type" content="product">',
|
||||
'twitterTags' => '<meta name="twitter:card" content="summary_large_image">',
|
||||
'jsonLd' => '{"@type":"Product","name":"베이직 오버핏 코튼 티셔츠"}',
|
||||
'bodyHtml' => '<div class="app-chrome"><nav><a href="/login">로그인</a></nav></div>',
|
||||
'googleAnalyticsId' => '',
|
||||
'googleVerification' => '',
|
||||
'naverVerification' => '',
|
||||
'stylesheets' => [
|
||||
'/api/templates/assets/sirsoft-basic/css/components.css',
|
||||
],
|
||||
'extraHeadTags' => '',
|
||||
'extraBodyEnd' => '',
|
||||
'generatorTag' => '<meta name="generator" content="GnuBoard7">',
|
||||
], $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(
|
||||
'<link rel="stylesheet" href="/api/templates/assets/sirsoft-basic/css/components.css">',
|
||||
$out,
|
||||
);
|
||||
}
|
||||
|
||||
public function test_cssPath_변수_없이도_예외없이_렌더된다(): void
|
||||
{
|
||||
// cssPath 를 뷰 데이터에서 완전히 제거해도 (undefined variable 등) 예외가 없어야 한다.
|
||||
$out = $this->renderSeo();
|
||||
|
||||
$this->assertStringContainsString('<title>베이직 오버핏 코튼 티셔츠 | 코지홈</title>', $out);
|
||||
}
|
||||
}
|
||||
@@ -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' => '',
|
||||
|
||||
@@ -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. 복잡한 표현식 + 조건부 렌더링 검증
|
||||
// ========================================================
|
||||
|
||||
@@ -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('<html><link rel="stylesheet"></html>');
|
||||
$viewMock->shouldReceive('render')->once()->andReturn('<html></html>');
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user