Phase7-PerfSEO: 폰트 preload 옵션 및 이미지 loading/decoding UI 연동 TDD/구현 완료

This commit is contained in:
2025-11-16 04:10:52 +09:00
parent 689edeaf24
commit ed7f6189eb
5 changed files with 183 additions and 8 deletions
+17 -2
View File
@@ -2,7 +2,14 @@ import type { Page, FormField } from '@/lib/schema/page'
import type { AssetManager } from '@/lib/assets/assetManager'
export type Exported = { html: string; css: string; js: string }
export type ExportOptions = { assetManager?: AssetManager; fontPreconnect?: boolean; robotsMeta?: string }
export type ExportOptions = {
assetManager?: AssetManager
fontPreconnect?: boolean
fontPreload?: boolean
robotsMeta?: string
imageLoading?: 'lazy' | 'eager'
imageDecoding?: 'async' | 'sync'
}
function renderFaq(section: { props: { items: Array<{ q: string; a: string }> } }) {
const itemsRaw: Array<{ q: string; a: string }> = Array.isArray(section.props.items) ? section.props.items : []
@@ -116,6 +123,8 @@ function renderHero(section: { props: { heading: string; subheading?: string; im
const am = opts?.assetManager
const imageUrlRaw: string = section.props.imageUrl
const imageUrl = am ? am.rewriteUrl(imageUrlRaw) : imageUrlRaw
const loading = opts?.imageLoading ?? 'lazy'
const decoding = opts?.imageDecoding ?? 'async'
return `
<section class="hero" aria-labelledby="hero-heading">
<div class="container">
@@ -124,7 +133,7 @@ function renderHero(section: { props: { heading: string; subheading?: string; im
${subheading.length > 0 ? `<p>${escapeHtml(subheading)}</p>` : ''}
</div>
<div class="media">
<img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(heading)}" loading="lazy" decoding="async" width="1200" height="675" />
<img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(heading)}" loading="${escapeHtml(loading)}" decoding="${escapeHtml(decoding)}" width="1200" height="675" />
</div>
</div>
</section>
@@ -252,12 +261,18 @@ function buildHead(page: Page, opts?: ExportOptions) {
const custom = page.analytics?.customHeadHtml || ''
const preconnect = opts?.fontPreconnect !== false
const robotsMeta = (opts?.robotsMeta && opts.robotsMeta.trim().length > 0) ? `<meta name="robots" content="${escapeHtml(opts!.robotsMeta!)}" />` : ''
const fontPreload = opts?.fontPreload === true
const rawFamily = (page.theme?.fontFamily || '').trim()
const fontHref = rawFamily.length > 0 ? `https://fonts.googleapis.com/css2?family=${encodeURIComponent(rawFamily).replace(/%20/g, '+')}&display=swap` : ''
return `
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="canonical" href="/" />
${preconnect ? '<link rel="preconnect" href="https://fonts.googleapis.com" />' : ''}
${preconnect ? '<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />' : ''}
${fontPreload && fontHref ? `<link rel="preload" as="style" href="${fontHref}" />` : ''}
${fontPreload && fontHref ? `<link rel="stylesheet" href="${fontHref}" media="print" onload="this.media='all'" />` : ''}
${fontPreload && fontHref ? `<noscript><link rel="stylesheet" href="${fontHref}" /></noscript>` : ''}
<link rel="manifest" href="/site.webmanifest" />
<meta name="theme-color" content="${escapeHtml(page.theme.primaryColor)}" />
${robotsMeta}