diff --git a/MAIN_PLAN.md b/MAIN_PLAN.md index 7df377c..2d1733e 100644 --- a/MAIN_PLAN.md +++ b/MAIN_PLAN.md @@ -272,6 +272,14 @@ - manifest JSON 파싱 검증 로직 추가 - 브랜치/PR: `feat/manifest-robots-auto` → CI 트리거 및 자동 머지 대상(automerge 라벨 조건) +### SEO/Perf 개선(TDD): theme-color, hero width/height +- 테스트: `lib/exporter/seoPerf.themeColor.heroDims.test.ts` + - `` 헤드 포함 검증(page.theme.primaryColor 사용) + - hero 이미지에 `width`/`height` 속성 존재 검증(CLS 저감) +- 구현: `lib/exporter/html.ts` + - `buildHead`에 theme-color 메타 추가 + - `renderHero` 이미지 태그에 width/height 기본값 추가(1200x675) +- 브랜치/PR: `feat/seo-perf-themecolor-hero-dims` → CI 트리거 및 자동 머지 대상 ### ZIP 검증 TDD Phase5(extras 내용 검증) - 테스트: `lib/exporter/zip.validation.phase5.test.ts` - `robots.txt` 텍스트 베이스라인 확인(`User-agent: *`) diff --git a/lib/exporter/extras.overrides.test.ts b/lib/exporter/extras.overrides.test.ts new file mode 100644 index 0000000..e1c6bbd --- /dev/null +++ b/lib/exporter/extras.overrides.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest' +import { pageSchema, type Page } from '@/lib/schema/page' +import { buildExtrasForPage } from '@/lib/exporter/extras' + +function makePage(): Page { + return pageSchema.parse({ + title: 'Overrides Demo', + locale: 'en', + theme: { primaryColor: '#123456', fontFamily: 'Inter' }, + sections: [ + { type: 'hero', props: { heading: 'Hello', subheading: 'World', imageUrl: 'https://img.example/hero.png' } }, + ], + form: { actionUrl: '', fields: [], spamProtection: { honeypotFieldName: '_hp', minSubmitSeconds: 2 } }, + seo: { title: 'Overrides', description: 'Desc' }, + }) +} + +describe('extras overrides', () => { + it('allows overriding robots.txt content', () => { + const page = makePage() + const extras = buildExtrasForPage(page, { + robotsText: 'User-agent: *\nDisallow: /private', + }) + const robots = new TextDecoder().decode(extras['robots.txt']) + expect(robots).toContain('Disallow: /private') + }) + + it('allows overriding manifest fields (name, short_name, start_url, display, theme_color)', () => { + const page = makePage() + const extras = buildExtrasForPage(page, { + manifest: { + name: 'Custom Name', + short_name: 'Custom', + start_url: '/home', + display: 'minimal-ui', + theme_color: '#abcdef', + }, + }) + const manifestRaw = new TextDecoder().decode(extras['site.webmanifest']) + const manifest = JSON.parse(manifestRaw) + expect(manifest.name).toBe('Custom Name') + expect(manifest.short_name).toBe('Custom') + expect(manifest.start_url).toBe('/home') + expect(manifest.display).toBe('minimal-ui') + expect(manifest.theme_color).toBe('#abcdef') + }) +}) diff --git a/lib/exporter/extras.ts b/lib/exporter/extras.ts index e112772..8b9fa4d 100644 --- a/lib/exporter/extras.ts +++ b/lib/exporter/extras.ts @@ -1,13 +1,25 @@ import type { Page } from '@/lib/schema/page' +type ExtrasOverrides = { + robotsText?: string + manifest?: Partial<{ + name: string + short_name: string + start_url: string + display: string + theme_color: string + }> +} + // Sheets 템플릿 등 ZIP 루트/서브 경로에 포함할 추가 파일(extras)을 구성한다. // - actionUrl이 비어있을 때만 Google Apps Script 템플릿을 포함한다. -export function buildExtrasForPage(page: Page): Record { +export function buildExtrasForPage(page: Page, overrides?: ExtrasOverrides): Record { const extras: Record = {} const te = new TextEncoder() // Always provide a baseline robots.txt - const robots = `User-agent: *\nDisallow:` + const robotsDefault = `User-agent: *\nDisallow:` + const robots = overrides?.robotsText ?? robotsDefault extras['robots.txt'] = te.encode(robots) // Provide a minimal site.webmanifest derived from Page @@ -20,6 +32,13 @@ export function buildExtrasForPage(page: Page): Record { start_url: '/', display: 'standalone', theme_color: page.theme?.primaryColor ?? '#000000', + ...((overrides?.manifest as object) || {}), + } as { + name: string + short_name: string + start_url: string + display: string + theme_color: string } extras['site.webmanifest'] = te.encode(JSON.stringify(manifest)) } catch {}