diff --git a/next.config.mjs b/next.config.mjs index 87f8b205..fb48f7df 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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 diff --git a/public/JOA00162.avif b/public/JOA00162.avif index d39c3ae0..548380d2 100644 Binary files a/public/JOA00162.avif and b/public/JOA00162.avif differ diff --git a/public/Jeronimo.avif b/public/Jeronimo.avif index 9e4cbdc2..f9b79d29 100644 Binary files a/public/Jeronimo.avif and b/public/Jeronimo.avif differ diff --git a/public/Lula.avif b/public/Lula.avif index 23d974e0..6ac065e8 100644 Binary files a/public/Lula.avif and b/public/Lula.avif differ diff --git a/public/RUI - 2-2.avif b/public/RUI - 2-2.avif index 7a31aaf3..e0c0e424 100644 Binary files a/public/RUI - 2-2.avif and b/public/RUI - 2-2.avif differ diff --git a/public/WAGNER - 2-9 final.avif b/public/WAGNER - 2-9 final.avif index a292d15b..fce44019 100644 Binary files a/public/WAGNER - 2-9 final.avif and b/public/WAGNER - 2-9 final.avif differ diff --git a/src/components/CampaignHero.tsx b/src/components/CampaignHero.tsx index 4585a8d5..ff620a11 100644 --- a/src/components/CampaignHero.tsx +++ b/src/components/CampaignHero.tsx @@ -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" /> diff --git a/tests/unit/campaignHero.unit.spec.tsx b/tests/unit/campaignHero.unit.spec.tsx index 73883bcd..8af5108e 100644 --- a/tests/unit/campaignHero.unit.spec.tsx +++ b/tests/unit/campaignHero.unit.spec.tsx @@ -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() + + 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() diff --git a/tests/unit/imageDelivery.unit.spec.ts b/tests/unit/imageDelivery.unit.spec.ts new file mode 100644 index 00000000..021af680 --- /dev/null +++ b/tests/unit/imageDelivery.unit.spec.ts @@ -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() + 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) + }) +})