공개 제보 https://github.com/gnuboard/g7/issues/122 대응 — 초기 부트스트랩 리소스(다국어 병합·컴포넌트 정의·라우트·확장 번들·템플릿 dist)를 캐시 버전 디렉토리(public/build/ext/{v}/)에 실파일로 게시해 웹서버가 rewrite 전에 직접 서빙한다. 부트 임계 경로의 PHP 왕복을 제거하고(실측 TTFB 131~144ms → 1~7ms), 미게시·부분게시·GC 직후에는 fetch·태그·번들 3계층이 종전 API 로 즉시 폴백한다. - 게시: 원자적 tmp→rename→manifest(존재=완료), 캐시 락 단일 실행, 인라인 GC (현재+직전 1개), incrementExtensionCacheVersion 단일 지점 terminating 트리거 + blade 자가 치유 + 설치기 태스크(best_effort) + 일일 cleanup 스케줄, sudo 업데이트 대비 소유권 정상화(normalizeOwnership)·prune/백업 제외 - 프론트(engine-v1.61.0): blade 주입 cache_version 1급 시드(이중 부트 로드 제거), fetchStaticFirst 즉시 폴백, ComponentRegistry 버전 키드 매니페스트, ModuleAssetLoader 번들 정적→레거시 폴백, asset-url-recovery staticToLegacy 역변환 - 폴백 API 품질: lang/components/routes ETag+304 + 환경 분기 Cache-Control, 열화 라우트 스냅샷 공개 캐시 금지(서버측 캐시 회피와 대칭), 게시 .htaccess mod_deflate + nginx gzip 스니펫(압축 전송량 회귀 방지) - SEO 정합: 봇 HTML 은 GC 대상 정적 URL 미사용(allowStatic:false), props $switch 봇측 해석 구현(engine-v1.56.0 패리티), 패리티 룰 expression-dialect 그룹 신설, 상주 allow 헤더 제거로 잠금 복원, _comment* 접두 주석 키 분류 - 검증: 전 수정 red→green 4단계, Playwright 라이브 21건, Chrome MCP 24축+M1~M3, 봇 curl 3축, 캐시 저장소(file/redis/database) 축 판정, 히스토리·공개이슈·커밋 이력 전수 재조사 반영 - 코어 7.0.10, engine-v1.61.0. kill-switch: G7_STATIC_CACHE=false
79 lines
3.1 KiB
PHP
79 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Seo;
|
|
|
|
use App\Seo\SeoRenderer;
|
|
use App\Services\ExtensionStaticCacheService;
|
|
use App\Support\AssetUrl;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\File;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* SEO 봇 HTML 의 템플릿 CSS URL 은 정적 게시(bake) 경로를 우회해야 한다 (#122).
|
|
*
|
|
* 봇 HTML 은 SeoCacheManager(`seo.page.*` — 키에 cache_version 미포함, TTL 수시간)에
|
|
* 캐시되는데, 정적 게시 디렉토리는 GC 가 현재+직전 1개만 보존한다. 캐시된 HTML 이
|
|
* GC 된 `/build/ext/{v}/…` 를 참조하면 봇 화면의 CSS 가 404 이고, SEO HTML 에는
|
|
* asset-url-recovery 파샬이 포함되지 않아 자가 복구 경로도 없다. 따라서 SEO 렌더는
|
|
* 항상 무버전 API URL(정적 게이트 우회 = 종전 동작)을 사용한다.
|
|
*/
|
|
class SeoRendererStaticAssetBypassTest extends TestCase
|
|
{
|
|
private const VERSION = 424242;
|
|
|
|
/** 테스트 전용 public 루트 (실 게시 트리 격리) */
|
|
private string $isolatedPublicPath;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// 실 게시 트리(public/build/ext) 오염 방지 (ExtensionStaticCacheServiceTest 와 동일 근거)
|
|
$this->isolatedPublicPath = storage_path('framework/testing/public-seobypass-'.getmypid());
|
|
File::ensureDirectoryExists($this->isolatedPublicPath);
|
|
$this->app->usePublicPath($this->isolatedPublicPath);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
File::deleteDirectory($this->isolatedPublicPath);
|
|
AssetUrl::resetStaticExtBaseMemo();
|
|
ExtensionStaticCacheService::resetPublishScheduleForTesting();
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
/**
|
|
* 정적 게이트가 열린 상태에서도 SEO CSS URL 은 `/build/ext/` 를 참조하지 않는다.
|
|
*
|
|
* @effects seo_html_never_references_gc_target_static_path
|
|
*/
|
|
public function test_seo_css_url_은_정적_게시_경로를_우회한다(): void
|
|
{
|
|
Cache::put('g7:core:ext.cache_version', self::VERSION);
|
|
app()['env'] = 'production';
|
|
|
|
$dir = public_path('build/ext/'.self::VERSION);
|
|
File::ensureDirectoryExists($dir.'/templates/sirsoft-basic/assets/css');
|
|
File::put($dir.'/manifest.json', '{}');
|
|
File::put($dir.'/templates/sirsoft-basic/assets/css/components.css', 'x');
|
|
AssetUrl::resetStaticExtBaseMemo();
|
|
|
|
// 전제 고정 — 블레이드 경로(기본값)는 정적 게이트를 통과하는 상태다.
|
|
// 이 단언이 깨지면 아래 우회 단언은 게이트 미통과로 인한 공허한 통과가 된다.
|
|
$this->assertSame(
|
|
'/build/ext/'.self::VERSION.'/templates/sirsoft-basic/assets/css/components.css',
|
|
AssetUrl::templateAsset('sirsoft-basic', 'css/components.css')
|
|
);
|
|
|
|
$method = new \ReflectionMethod(SeoRenderer::class, 'getTemplateCssUrls');
|
|
$urls = $method->invoke(app(SeoRenderer::class), 'sirsoft-basic');
|
|
|
|
$this->assertNotEmpty($urls);
|
|
foreach ($urls as $url) {
|
|
$this->assertStringNotContainsString('/build/ext/', $url);
|
|
}
|
|
}
|
|
}
|