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)
+ })
+})