auto: PR for feat/image-optimization #67
+11
-1
@@ -95,7 +95,15 @@
|
|||||||
- Exporter CSS 토큰 회귀 확대(:root --primary, body font, skip-link base/focus, focus outline, reduced-motion)
|
- Exporter CSS 토큰 회귀 확대(:root --primary, body font, skip-link base/focus, focus outline, reduced-motion)
|
||||||
- 컴포넌트 i18n 회귀 스냅샷(en/ko 주요 라벨/버튼 키 존재)
|
- 컴포넌트 i18n 회귀 스냅샷(en/ko 주요 라벨/버튼 키 존재)
|
||||||
- 섹션 a11y 자동 검증 템플릿(testUtils/sectionA11y) 도입 및 템플릿 테스트 추가
|
- 섹션 a11y 자동 검증 템플릿(testUtils/sectionA11y) 도입 및 템플릿 테스트 추가
|
||||||
- 신규 섹션 추가: Benefits
|
- 이미지 최적화 옵션 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[]
|
- 스키마 확장(`benefits`): props.items:string[]
|
||||||
- Exporter 렌더 추가: `<section class="benefits" aria-labelledby>`, `<h2 id="benefits-heading">`, 리스트 렌더
|
- Exporter 렌더 추가: `<section class="benefits" aria-labelledby>`, `<h2 id="benefits-heading">`, 리스트 렌더
|
||||||
- 테스트: sections.benefits.test.ts (h2/aria-labelledby/리스트 아이템 검증)
|
- 테스트: sections.benefits.test.ts (h2/aria-labelledby/리스트 아이템 검증)
|
||||||
@@ -373,6 +381,7 @@
|
|||||||
- `verifyIntegrity` 결과에 `summary`(total/missing/integrityMismatches/sizeMismatches/bundleHashMismatch) 추가
|
- `verifyIntegrity` 결과에 `summary`(total/missing/integrityMismatches/sizeMismatches/bundleHashMismatch) 추가
|
||||||
- Export Options(폰트 프리커넥트/프리로드, 이미지 로딩/디코딩, robots 메타) UI/문서화
|
- Export Options(폰트 프리커넥트/프리로드, 이미지 로딩/디코딩, robots 메타) UI/문서화
|
||||||
- 브랜치 푸쉬 완료: `feat/export-bundle-integrity`
|
- 브랜치 푸쉬 완료: `feat/export-bundle-integrity`
|
||||||
|
- 이미지 최적화 옵션(1차): `fetchpriority`/`srcset`/`sizes` 추가 및 테스트 그린
|
||||||
|
|
||||||
### 하드닝
|
### 하드닝
|
||||||
- ZIP 경로 가드: `../`, 절대경로(`/`), 역슬래시(`\`) 차단 (테스트 포함)
|
- ZIP 경로 가드: `../`, 절대경로(`/`), 역슬래시(`\`) 차단 (테스트 포함)
|
||||||
@@ -392,6 +401,7 @@
|
|||||||
2) CI 무결성 검증 스텝 추가: `verifyIntegrity`로 빌드 산출물 검사(요약 리포트 로깅)
|
2) CI 무결성 검증 스텝 추가: `verifyIntegrity`로 빌드 산출물 검사(요약 리포트 로깅)
|
||||||
3) PR 라벨 자동화 및 조건부 머지(Gitea Actions/웹훅)
|
3) PR 라벨 자동화 및 조건부 머지(Gitea Actions/웹훅)
|
||||||
4) README에 사용 가이드 보완(verifyIntegrity 활용 예시)
|
4) README에 사용 가이드 보완(verifyIntegrity 활용 예시)
|
||||||
|
5) 이미지 최적화 2차: `sizes` 기본 정책 프리셋 및 변환 파이프라인(AVIF/WebP) 설계 초안(TDD 스텁)
|
||||||
|
|
||||||
### CI/CD 계획(초안)
|
### CI/CD 계획(초안)
|
||||||
- PR 오픈 시: 라벨 자동 지정, 테스트/빌드, 무결성 검증 스텝 실행
|
- PR 오픈 시: 라벨 자동 지정, 테스트/빌드, 무결성 검증 스텝 실행
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ The Builder can export a production-ready ZIP that contains:
|
|||||||
- Image Options
|
- Image Options
|
||||||
- Loading: `lazy` (default) or `eager` for `<img loading>`
|
- Loading: `lazy` (default) or `eager` for `<img loading>`
|
||||||
- Decoding: `async` (default) or `sync` for `<img decoding>`
|
- 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
|
### site.webmanifest and robots.txt
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -12,6 +12,8 @@ export type ExportOptions = {
|
|||||||
imageFetchPriority?: 'high' | 'low'
|
imageFetchPriority?: 'high' | 'low'
|
||||||
imageSrcset?: string
|
imageSrcset?: string
|
||||||
imageSizes?: string
|
imageSizes?: string
|
||||||
|
imageAutoSizes?: boolean
|
||||||
|
imageSizesPreset?: 'mobile-first' | 'desktop-fixed'
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFaq(section: { props: { items: Array<{ q: string; a: string }> } }) {
|
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 decoding = opts?.imageDecoding ?? 'async'
|
||||||
const fetchpriority = opts?.imageFetchPriority ? ` fetchpriority="${escapeHtml(opts.imageFetchPriority)}"` : ''
|
const fetchpriority = opts?.imageFetchPriority ? ` fetchpriority="${escapeHtml(opts.imageFetchPriority)}"` : ''
|
||||||
const srcset = opts?.imageSrcset ? ` srcset="${escapeHtml(opts.imageSrcset)}"` : ''
|
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 `
|
return `
|
||||||
<section class="hero" aria-labelledby="hero-heading">
|
<section class="hero" aria-labelledby="hero-heading">
|
||||||
<div class="container">
|
<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[^>]*srcset="https:\/\/example.com\/hero.jpg 1x, https:\/\/example.com\/hero@2x.jpg 2x"/)
|
||||||
expect(html).toMatch(/<img[^>]*sizes="\(max-width: 640px\) 100vw, 600px"/)
|
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"/)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user