auto: PR for feat/image-optimization #67

Merged
jaybe merged 5 commits from feat/image-optimization into main 2025-11-16 11:20:32 +00:00
4 changed files with 78 additions and 2 deletions
Showing only changes of commit ba2ba588f7 - Show all commits
+10
View File
@@ -95,6 +95,14 @@
- Exporter CSS 토큰 회귀 확대(:root --primary, body font, skip-link base/focus, focus outline, reduced-motion)
- 컴포넌트 i18n 회귀 스냅샷(en/ko 주요 라벨/버튼 키 존재)
- 섹션 a11y 자동 검증 템플릿(testUtils/sectionA11y) 도입 및 템플릿 테스트 추가
- 이미지 최적화 옵션 1차(TDD): hero 이미지에 `fetchpriority`/`srcset`/`sizes` 조건부 렌더링
- 테스트: `lib/exporter/image.responsive.options.test.ts` (속성 존재 검증)
- 구현: `lib/exporter/html.ts`(`ExportOptions`에 옵션 추가 및 `<img>` 렌더 반영)
- 브랜치: `feat/image-optimization` (로컬 커밋, 푸쉬 보류)
- 이미지 최적화 옵션 2차(TDD): `srcset`만 제공 시 기본 `sizes` 자동 적용(`imageAutoSizes`)
- 기본값: `(max-width: 640px) 100vw, 600px`
- 테스트: `image.responsive.options.test.ts` (auto on/off 케이스)
- 구현: `ExportOptions.imageAutoSizes` 추가 및 `renderHero` 적용
- 신규 섹션 추가: Benefits
- 스키마 확장(`benefits`): props.items:string[]
- Exporter 렌더 추가: `<section class="benefits" aria-labelledby>`, `<h2 id="benefits-heading">`, 리스트 렌더
@@ -373,6 +381,7 @@
- `verifyIntegrity` 결과에 `summary`(total/missing/integrityMismatches/sizeMismatches/bundleHashMismatch) 추가
- Export Options(폰트 프리커넥트/프리로드, 이미지 로딩/디코딩, robots 메타) UI/문서화
- 브랜치 푸쉬 완료: `feat/export-bundle-integrity`
- 이미지 최적화 옵션(1차): `fetchpriority`/`srcset`/`sizes` 추가 및 테스트 그린
### 하드닝
- ZIP 경로 가드: `../`, 절대경로(`/`), 역슬래시(`\`) 차단 (테스트 포함)
@@ -392,6 +401,7 @@
2) CI 무결성 검증 스텝 추가: `verifyIntegrity`로 빌드 산출물 검사(요약 리포트 로깅)
3) PR 라벨 자동화 및 조건부 머지(Gitea Actions/웹훅)
4) README에 사용 가이드 보완(verifyIntegrity 활용 예시)
5) 이미지 최적화 2차: `sizes` 기본 정책 프리셋 및 변환 파이프라인(AVIF/WebP) 설계 초안(TDD 스텁)
### CI/CD 계획(초안)
- PR 오픈 시: 라벨 자동 지정, 테스트/빌드, 무결성 검증 스텝 실행
+7
View File
@@ -63,6 +63,13 @@ The Builder can export a production-ready ZIP that contains:
- Image Options
- Loading: `lazy` (default) or `eager` for `<img loading>`
- Decoding: `async` (default) or `sync` for `<img decoding>`
- Fetch Priority: optional `high|low` for `<img fetchpriority>`
- Srcset/Sizes: optional responsive sources via `<img srcset>` and `<img sizes>`
- Auto Sizes: when enabled and only `srcset` is provided, a sensible default is applied to `<img sizes>`: `(max-width: 640px) 100vw, 600px`
- Sizes Preset: choose a preset for `<img sizes>` when `imageSizes` is not set.
- `mobile-first`: `(max-width: 640px) 100vw, 800px`
- `desktop-fixed`: `1200px`
- Precedence: explicit `imageSizes` > `imageSizesPreset` > `imageAutoSizes` > none
### site.webmanifest and robots.txt
+11 -1
View File
@@ -12,6 +12,8 @@ export type ExportOptions = {
imageFetchPriority?: 'high' | 'low'
imageSrcset?: string
imageSizes?: string
imageAutoSizes?: boolean
imageSizesPreset?: 'mobile-first' | 'desktop-fixed'
}
function renderFaq(section: { props: { items: Array<{ q: string; a: string }> } }) {
@@ -130,7 +132,15 @@ function renderHero(section: { props: { heading: string; subheading?: string; im
const decoding = opts?.imageDecoding ?? 'async'
const fetchpriority = opts?.imageFetchPriority ? ` fetchpriority="${escapeHtml(opts.imageFetchPriority)}"` : ''
const srcset = opts?.imageSrcset ? ` srcset="${escapeHtml(opts.imageSrcset)}"` : ''
const sizes = opts?.imageSizes ? ` sizes="${escapeHtml(opts.imageSizes)}"` : ''
const defaultSizes = '(max-width: 640px) 100vw, 600px'
const presetSizes = (() => {
if (!opts?.imageSizesPreset) return ''
if (opts.imageSizesPreset === 'mobile-first') return '(max-width: 640px) 100vw, 800px'
if (opts.imageSizesPreset === 'desktop-fixed') return '1200px'
return ''
})()
const sizesValue = opts?.imageSizes ?? (presetSizes || (opts?.imageSrcset && opts?.imageAutoSizes ? defaultSizes : ''))
const sizes = sizesValue ? ` sizes="${escapeHtml(sizesValue)}"` : ''
return `
<section class="hero" aria-labelledby="hero-heading">
<div class="container">
@@ -35,4 +35,53 @@ describe('image responsive/export options', () => {
expect(html).toMatch(/<img[^>]*srcset="https:\/\/example.com\/hero.jpg 1x, https:\/\/example.com\/hero@2x.jpg 2x"/)
expect(html).toMatch(/<img[^>]*sizes="\(max-width: 640px\) 100vw, 600px"/)
})
it('applies default sizes when srcset is provided and imageAutoSizes is true', () => {
const options: ExportOptions = {
imageSrcset: 'https://example.com/hero.jpg 1x, https://example.com/hero@2x.jpg 2x',
imageAutoSizes: true,
}
const { html } = exportPage(basePage, options)
expect(html).toMatch(/<img[^>]*srcset="https:\/\/example.com\/hero.jpg 1x, https:\/\/example.com\/hero@2x.jpg 2x"/)
expect(html).toMatch(/<img[^>]*sizes="\(max-width: 640px\) 100vw, 600px"/)
})
it('does not include sizes when srcset is provided but imageAutoSizes is false and sizes not provided', () => {
const options: ExportOptions = {
imageSrcset: 'https://example.com/hero.jpg 1x, https://example.com/hero@2x.jpg 2x',
imageAutoSizes: false,
}
const { html } = exportPage(basePage, options)
expect(html).toMatch(/<img[^>]*srcset="https:\/\/example.com\/hero.jpg 1x, https:\/\/example.com\/hero@2x.jpg 2x"/)
expect(html).not.toMatch(/<img[^>]*sizes="/)
})
it('applies preset sizes when imageSizesPreset is set (mobile-first)', () => {
const options: ExportOptions = {
imageSrcset: 'https://example.com/hero.jpg 1x, https://example.com/hero@2x.jpg 2x',
imageSizesPreset: 'mobile-first',
}
const { html } = exportPage(basePage, options)
expect(html).toMatch(/<img[^>]*sizes="\(max-width: 640px\) 100vw, 800px"/)
})
it('applies desktop-fixed preset when selected', () => {
const options: ExportOptions = {
imageSrcset: 'https://example.com/hero.jpg 1x, https://example.com/hero@2x.jpg 2x',
imageSizesPreset: 'desktop-fixed',
}
const { html } = exportPage(basePage, options)
expect(html).toMatch(/<img[^>]*sizes="1200px"/)
})
it('explicit imageSizes overrides presets and auto', () => {
const options: ExportOptions = {
imageSrcset: 'https://example.com/hero.jpg 1x, https://example.com/hero@2x.jpg 2x',
imageAutoSizes: true,
imageSizesPreset: 'desktop-fixed',
imageSizes: '(max-width: 768px) 100vw, 700px',
}
const { html } = exportPage(basePage, options)
expect(html).toMatch(/<img[^>]*sizes="\(max-width: 768px\) 100vw, 700px"/)
})
})