fix(images): masters do hero no tamanho de exibição e TTL de 4h no otimizador

This commit is contained in:
Francisco Solla 2026-10-01 20:04:22 -03:00
parent 9a049bbbb4
commit 16f959752a
9 changed files with 94 additions and 7 deletions

View file

@ -7,6 +7,7 @@ const nextConfig = {
distDir: process.env.NEXT_DIST_DIR || '.next',
...(process.env.NEXT_OUTPUT_STANDALONE === '1' ? { output: 'standalone' } : {}),
images: {
minimumCacheTTL: 14400,
remotePatterns: [
new URL(`${allowedImageHost}/**`),
// YouTube video thumbnails (campaign home content board, S2) come from

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -5,31 +5,32 @@ const SUPPORT_URL = 'https://apoiar.me/jorgesolla'
const allyImages = [
{
id: 'rui',
src: '/RUI%20-%202-2.avif',
src: '/RUI - 2-2.avif',
width: 177,
height: 265,
sizes: '(min-width: 1366px) calc(6.14vw + 93px), 177px',
sizes: '(min-width: 1920px) 211px, (min-width: 1366px) calc(6.14vw + 93px), 177px',
},
{
id: 'wagner',
src: '/WAGNER%20-%202-9%20final.avif',
src: '/WAGNER - 2-9 final.avif',
width: 177,
height: 265,
sizes: '(min-width: 1366px) calc(6.14vw + 93px), 177px',
sizes: '(min-width: 1920px) 211px, (min-width: 1366px) calc(6.14vw + 93px), 177px',
},
{
id: 'jeronimo',
src: '/Jeronimo.avif',
width: 177,
height: 265,
sizes: '(min-width: 1366px) calc(6.14vw + 93px), 177px',
sizes: '(min-width: 1920px) 211px, (min-width: 1366px) calc(6.14vw + 93px), 177px',
},
{
id: 'lula',
src: '/Lula.avif',
width: 216,
height: 213,
sizes: '(min-width: 1366px) calc(8.3vw + 125px), (min-width: 1024px) 238px, 216px',
sizes:
'(min-width: 1920px) 284px, (min-width: 1366px) calc(8.3vw + 124.6px), (min-width: 1024px) 238px, 216px',
},
]
@ -71,7 +72,7 @@ export const CampaignHero = () => (
alt="Jorge Solla, candidato a deputado federal pela Bahia"
width={495}
height={742}
sizes="(min-width: 1720px) 747px, (min-width: 1280px) 595px, (min-width: 1024px) 495px, 449px"
sizes="(min-width: 1920px) 747px, (min-width: 1024px) calc(28.16vw + 206.7px), 449px"
priority
className="campaign-hero-portrait-image absolute h-auto max-w-none object-contain"
/>

View file

@ -31,6 +31,19 @@ describe('CampaignHero', () => {
expect(novidadesLink.getAttribute('data-cta')).toBe('secondary')
})
it('não percent-encoda os caminhos das imagens', () => {
const { container } = render(<CampaignHero />)
const sources = Array.from(container.querySelectorAll('img')).map((image) =>
image.getAttribute('src'),
)
expect(sources.length).toBeGreaterThan(0)
for (const source of sources) {
expect(source).not.toContain('%')
}
})
it('expõe as provas e as fotos com nomes acessíveis', () => {
render(<CampaignHero />)

View file

@ -0,0 +1,72 @@
// @vitest-environment node
import { existsSync, readFileSync, readdirSync } from 'node:fs'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import sharp from 'sharp'
import { describe, expect, it } from 'vitest'
const MAX_WIDTH = 1600
const MAX_HEIGHT = 2400
const LOCAL_IMAGE_REF = /src[=:]\s*['"](\/[^'"`]+\.(?:avif|webp|png|jpe?g))['"]/g
const publicFile = (src: string) =>
fileURLToPath(new URL(`../../public${decodeURIComponent(src)}`, import.meta.url))
const campaignHeroSource = readFileSync(
fileURLToPath(new URL('../../src/components/CampaignHero.tsx', import.meta.url)),
'utf8',
)
const localHeroAssets = [
...new Set([...campaignHeroSource.matchAll(LOCAL_IMAGE_REF)].map(([, src]) => src)),
]
const collectSourceImageRefs = () => {
const refs = new Set<string>()
const srcDir = fileURLToPath(new URL('../../src', import.meta.url))
for (const entry of readdirSync(srcDir, { recursive: true, withFileTypes: true })) {
if (!entry.isFile() || !/\.(?:ts|tsx)$/.test(entry.name)) continue
const code = readFileSync(join(entry.parentPath, entry.name), 'utf8')
for (const [, src] of code.matchAll(LOCAL_IMAGE_REF)) refs.add(src)
}
return refs
}
describe('entrega das imagens do hero (perf do /_next/image)', () => {
it('usa masters no tamanho máximo de exibição, sem forçar decode de 3840px', async () => {
expect(localHeroAssets.length).toBeGreaterThanOrEqual(5)
for (const src of localHeroAssets) {
const { width, height } = await sharp(publicFile(src)).metadata()
expect(width, src).toBeLessThanOrEqual(MAX_WIDTH)
expect(height, src).toBeLessThanOrEqual(MAX_HEIGHT)
}
})
it('mantém todo asset local entregue via next/image dentro do orçamento', async () => {
const refs = [...collectSourceImageRefs()].filter((src) => existsSync(publicFile(src)))
expect(refs.length).toBeGreaterThan(0)
for (const src of refs) {
const { width, height } = await sharp(publicFile(src)).metadata()
expect(width, src).toBeLessThanOrEqual(MAX_WIDTH)
expect(height, src).toBeLessThanOrEqual(MAX_HEIGHT)
}
})
it('configura TTL longo para os derivados do otimizador', () => {
const nextConfig = readFileSync(
fileURLToPath(new URL('../../next.config.mjs', import.meta.url)),
'utf8',
)
const ttl = nextConfig.match(/minimumCacheTTL:\s*(\d+)/)
expect(ttl, 'next.config.mjs deve definir images.minimumCacheTTL').toBeTruthy()
expect(Number(ttl?.[1])).toBeGreaterThanOrEqual(3600)
})
})