fix(images): hero e cards da home servem masters WebP — AVIF fora do otimizador
Otimizar AVIF no Next 15.4.11 decodifica via libheif (GHSA-2xp9-vwfh-vxw4) e um hang transiente envenena a chave para sempre no Batcher do ResponseCache; o patch oficial (15.5.24) bypassa AVIF. Masters WebP mantêm a entrega e o enquadramento; guards proíbem AVIF em src/ e public/.
|
Before Width: | Height: | Size: 466 KiB After Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 612 KiB After Width: | Height: | Size: 60 KiB |
BIN
public/Lula.avif
BIN
public/Lula.webp
|
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 47 KiB |
|
|
@ -108,7 +108,7 @@ async function verticalMark(height) {
|
|||
|
||||
/** Foto oficial do candidato (cutout com alfa). */
|
||||
async function candidatePhoto(height) {
|
||||
const trim = await sharp(`${REPO}public/JOA00162.avif`)
|
||||
const trim = await sharp(`${REPO}public/JOA00162.webp`)
|
||||
.trim({ threshold: 5 })
|
||||
.png()
|
||||
.toBuffer({ resolveWithObject: true })
|
||||
|
|
|
|||
|
|
@ -52,14 +52,14 @@ const problemItems: CampaignCarouselItem[] = [
|
|||
id: 'escala-6x1',
|
||||
title: 'Pelo fim da escala 6x1!',
|
||||
body: 'Jornadas longas aumentam o risco de AVC e infarto. Descanso é saúde pública. A bancada de Lula derrotou a direita na Câmara: o fim da 6×1 avançou. Mas a briga continua no Senado, onde a direita tenta barrar o avanço. No dia 4 de outubro, o 1313 é o seu sim ao descanso.',
|
||||
image: '/fundo.avif',
|
||||
image: '/fundo.webp',
|
||||
imageAlt: 'Jorge Solla defendendo direitos dos trabalhadores na Câmara dos Deputados',
|
||||
},
|
||||
{
|
||||
id: 'sus',
|
||||
title: 'Pra valorizar o SUS',
|
||||
body: 'O SUS é o sistema de saúde de todo brasileiro. Com Lula, a saúde voltou a receber recursos: Nova PAC de R$ 30,5 bilhões e Mais Médicos retomado. Mas o teto de gastos da direita ainda trava o orçamento. O 1313 é seu voto em defesa do SUS no Congresso!',
|
||||
image: '/53569851134_02afc18fb4_o.avif',
|
||||
image: '/53569851134_02afc18fb4_o.webp',
|
||||
imageAlt: 'Jorge Solla em agenda pública de defesa do SUS',
|
||||
imageFrame: {
|
||||
width: 1024,
|
||||
|
|
@ -71,7 +71,7 @@ const problemItems: CampaignCarouselItem[] = [
|
|||
id: 'mataripe',
|
||||
title: 'Pra defender os baianos',
|
||||
body: 'A Refinaria de Mataripe foi vendida no governo Bolsonaro e segue nas mãos de estrangeiros. Com Lula e Jerônimo, a Bahia voltou a andar pra frente; mas recomprar Mataripe exige bancada forte no Congresso. Votar 1313 é devolver à Bahia o que é seu.',
|
||||
image: '/52396285023_561ffc0ff6_o.avif',
|
||||
image: '/52396285023_561ffc0ff6_o.webp',
|
||||
imageAlt: 'Vista da Bahia durante mobilização popular',
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,28 +5,28 @@ const SUPPORT_URL = 'https://apoiar.me/jorgesolla'
|
|||
const allyImages = [
|
||||
{
|
||||
id: 'rui',
|
||||
src: '/RUI - 2-2.avif',
|
||||
src: '/RUI - 2-2.webp',
|
||||
width: 177,
|
||||
height: 265,
|
||||
sizes: '(min-width: 1920px) 211px, (min-width: 1366px) calc(6.14vw + 93px), 177px',
|
||||
},
|
||||
{
|
||||
id: 'wagner',
|
||||
src: '/WAGNER - 2-9 final.avif',
|
||||
src: '/WAGNER - 2-9 final.webp',
|
||||
width: 177,
|
||||
height: 265,
|
||||
sizes: '(min-width: 1920px) 211px, (min-width: 1366px) calc(6.14vw + 93px), 177px',
|
||||
},
|
||||
{
|
||||
id: 'jeronimo',
|
||||
src: '/Jeronimo.avif',
|
||||
src: '/Jeronimo.webp',
|
||||
width: 177,
|
||||
height: 265,
|
||||
sizes: '(min-width: 1920px) 211px, (min-width: 1366px) calc(6.14vw + 93px), 177px',
|
||||
},
|
||||
{
|
||||
id: 'lula',
|
||||
src: '/Lula.avif',
|
||||
src: '/Lula.webp',
|
||||
width: 216,
|
||||
height: 213,
|
||||
sizes:
|
||||
|
|
@ -68,7 +68,7 @@ export const CampaignHero = () => (
|
|||
|
||||
<div className="campaign-hero-portrait absolute z-20 overflow-hidden">
|
||||
<Image
|
||||
src="/JOA00162.avif"
|
||||
src="/JOA00162.webp"
|
||||
alt="Jorge Solla, candidato a deputado federal pela Bahia"
|
||||
width={495}
|
||||
height={742}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,19 @@ describe('CampaignHero', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('serve masters WebP — AVIF fica fora do otimizador (libheif)', () => {
|
||||
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.endsWith('.webp'), source).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('expõe as provas e as fotos com nomes acessíveis', () => {
|
||||
render(<CampaignHero />)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,12 @@ const MAX_HEIGHT = 2400
|
|||
|
||||
const LOCAL_IMAGE_REF = /src[=:]\s*['"](\/[^'"`]+\.(?:avif|webp|png|jpe?g))['"]/g
|
||||
|
||||
// `image:` covers the home card data (`CampaignCarouselItem.image`), which is
|
||||
// also rendered through next/image — the dimension budget above only walks
|
||||
// `src` literals on purpose (the card masters predate the budget), but the
|
||||
// AVIF ban below must catch both spellings.
|
||||
const LOCAL_IMAGE_LITERAL = /(?:src|image)[=:]\s*['"](\/[^'"`]+\.(?:avif|webp|png|jpe?g))['"]/g
|
||||
|
||||
const publicFile = (src: string) =>
|
||||
fileURLToPath(new URL(`../../public${decodeURIComponent(src)}`, import.meta.url))
|
||||
|
||||
|
|
@ -24,14 +30,14 @@ const localHeroAssets = [
|
|||
...new Set([...campaignHeroSource.matchAll(LOCAL_IMAGE_REF)].map(([, src]) => src)),
|
||||
]
|
||||
|
||||
const collectSourceImageRefs = () => {
|
||||
const collectSourceImageRefs = (pattern: RegExp) => {
|
||||
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)
|
||||
for (const [, src] of code.matchAll(pattern)) refs.add(src)
|
||||
}
|
||||
|
||||
return refs
|
||||
|
|
@ -49,7 +55,9 @@ describe('entrega das imagens do hero (perf do /_next/image)', () => {
|
|||
})
|
||||
|
||||
it('mantém todo asset local entregue via next/image dentro do orçamento', async () => {
|
||||
const refs = [...collectSourceImageRefs()].filter((src) => existsSync(publicFile(src)))
|
||||
const refs = [...collectSourceImageRefs(LOCAL_IMAGE_REF)].filter((src) =>
|
||||
existsSync(publicFile(src)),
|
||||
)
|
||||
expect(refs.length).toBeGreaterThan(0)
|
||||
|
||||
for (const src of refs) {
|
||||
|
|
@ -59,6 +67,24 @@ describe('entrega das imagens do hero (perf do /_next/image)', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('não entrega AVIF pelo otimizador (libheif: GHSA-2xp9-vwfh-vxw4 e travamento por chave)', () => {
|
||||
const refs = [...collectSourceImageRefs(LOCAL_IMAGE_LITERAL)]
|
||||
expect(refs.length).toBeGreaterThan(0)
|
||||
|
||||
for (const src of refs) {
|
||||
expect(src.endsWith('.avif'), src).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('não mantém AVIF em public/ — o otimizador decodifica qualquer arquivo local por URL', () => {
|
||||
const publicDir = fileURLToPath(new URL('../../public', import.meta.url))
|
||||
const avifFiles = readdirSync(publicDir, { recursive: true, withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.avif'))
|
||||
.map((entry) => entry.name)
|
||||
|
||||
expect(avifFiles).toEqual([])
|
||||
})
|
||||
|
||||
it('configura TTL longo para os derivados do otimizador', () => {
|
||||
const nextConfig = readFileSync(
|
||||
fileURLToPath(new URL('../../next.config.mjs', import.meta.url)),
|
||||
|
|
|
|||