diff --git a/apps/sim/app/(landing)/components/cta/cta.test.tsx b/apps/sim/app/(landing)/components/cta/cta.test.tsx
new file mode 100644
index 00000000000..f4d5eed5a84
--- /dev/null
+++ b/apps/sim/app/(landing)/components/cta/cta.test.tsx
@@ -0,0 +1,50 @@
+/** @vitest-environment jsdom */
+import { createHash } from 'node:crypto'
+import { readFile } from 'node:fs/promises'
+import path from 'node:path'
+import { renderToStaticMarkup } from 'react-dom/server'
+import sharp from 'sharp'
+import { describe, expect, it, vi } from 'vitest'
+import { Cta } from '@/app/(landing)/components/cta/cta'
+import { FOOTER_ARTWORK } from '@/app/(landing)/components/cta/footer-artwork.generated'
+
+vi.mock('@/app/(landing)/components/hero-cta', () => ({ HeroCta: () => null }))
+
+describe('Footer artwork delivery', () => {
+ it('renders responsive static sources with lazy, decorative fallbacks and no optimizer requests', () => {
+ const host = document.createElement('div')
+ host.innerHTML = renderToStaticMarkup()
+ expect(host.innerHTML).not.toContain('/_next/image')
+ expect(host.querySelectorAll('picture')).toHaveLength(2)
+ expect(host.querySelector('picture')?.className).toContain('dark:hidden')
+ expect(host.querySelectorAll('picture')[1].className).toContain('dark:block')
+ for (const picture of host.querySelectorAll('picture')) {
+ expect(picture.closest('[aria-hidden="true"]')).not.toBeNull()
+ expect([...picture.querySelectorAll('source')].map((source) => source.type)).toEqual([
+ 'image/avif',
+ 'image/webp',
+ ])
+ const image = picture.querySelector('img')!
+ expect(image.getAttribute('loading')).toBe('lazy')
+ expect(image.alt).toBe('')
+ expect(image.src).toContain('/landing/footer-artwork/')
+ expect(image.getAttribute('style')).toContain('data:image/svg+xml')
+ }
+ })
+
+ it('ships every advertised size with the matching dimensions and immutable content hash', async () => {
+ for (const artwork of Object.values(FOOTER_ARTWORK)) {
+ for (const srcSet of [artwork.avifSrcSet, artwork.webpSrcSet]) {
+ for (const entry of srcSet.split(', ')) {
+ const [src, descriptor] = entry.split(' ')
+ const width = Number.parseInt(descriptor, 10)
+ const buffer = await readFile(path.join(process.cwd(), 'public', src))
+ const metadata = await sharp(buffer).metadata()
+ expect(metadata.width).toBe(width)
+ expect(metadata.height).toBe((width * 9) / 16)
+ expect(src).toContain(createHash('sha256').update(buffer).digest('hex').slice(0, 12))
+ }
+ }
+ }
+ })
+})
diff --git a/apps/sim/app/(landing)/components/cta/cta.tsx b/apps/sim/app/(landing)/components/cta/cta.tsx
index af8d30cc038..665c91faee9 100644
--- a/apps/sim/app/(landing)/components/cta/cta.tsx
+++ b/apps/sim/app/(landing)/components/cta/cta.tsx
@@ -1,21 +1,22 @@
import { cn } from '@sim/emcn'
import Image from 'next/image'
import styles from '@/app/(landing)/components/cta/cta.module.css'
+import { FOOTER_ARTWORK } from '@/app/(landing)/components/cta/footer-artwork.generated'
import { HeroCta } from '@/app/(landing)/components/hero-cta'
import {
HOME_TYPE,
LANDING_CONTENT_WIDTH,
LANDING_GUTTER,
} from '@/app/(landing)/components/landing-layout'
-import ctaDark from '@/public/landing/cta-san-francisco-painted-dark.webp'
-import ctaLight from '@/public/landing/cta-san-francisco-painted-light.webp'
/** One shared closing statement, with a deliberate line break between sentences. */
const CTA_HEADLINE = ['Every agent your company runs.', 'All in one place.'] as const
+const ARTWORK_SIZES = '(max-width: 639px) 112vw, 100vw'
/**
* Painted pre-footer CTA for every marketing page, mounted once by LandingShell.
* Theme classes select the matching lazy-loaded painting without client state.
+ * Pre-encoded sources avoid request-time image optimization at every resolution.
* An embedded preview fills the scene while the full-resolution image loads.
* The sky mask keeps the copy clear and the lower edge fades into the footer.
*/
@@ -52,26 +53,29 @@ export function Cta() {
aria-hidden='true'
className='-mt-[clamp(96px,12.5vw,240px)] max-sm:-mt-6 pointer-events-none relative aspect-video w-full max-sm:aspect-[16/10]'
>
-
-
+ {Object.entries(FOOTER_ARTWORK).map(([theme, artwork]) => (
+
+
+
+
+
+ ))}
)
diff --git a/apps/sim/app/(landing)/components/cta/footer-artwork.generated.ts b/apps/sim/app/(landing)/components/cta/footer-artwork.generated.ts
new file mode 100644
index 00000000000..fcaa1d44fc5
--- /dev/null
+++ b/apps/sim/app/(landing)/components/cta/footer-artwork.generated.ts
@@ -0,0 +1,21 @@
+/** Generated by scripts/generate-footer-artwork.ts. */
+export const FOOTER_ARTWORK = {
+ light: {
+ avifSrcSet:
+ '/landing/footer-artwork/light-1200-f29e84fa39b5.avif 1200w, /landing/footer-artwork/light-1920-352cf7c570ad.avif 1920w, /landing/footer-artwork/light-3840-e889bb4bc039.avif 3840w',
+ webpSrcSet:
+ '/landing/footer-artwork/light-1200-8764ab359e07.webp 1200w, /landing/footer-artwork/light-1920-c9ec24660024.webp 1920w, /landing/footer-artwork/light-3840-6a6c3eab4b02.webp 3840w',
+ src: '/landing/footer-artwork/light-3840-6a6c3eab4b02.webp',
+ blurDataURL:
+ 'data:image/webp;base64,UklGRjYAAABXRUJQVlA4ICoAAACwAQCdASoIAAQAAkA4JaQAAxaZftQAAP75igYWrDK4ciyu9bneK5BAAAA=',
+ },
+ dark: {
+ avifSrcSet:
+ '/landing/footer-artwork/dark-1200-a1f7345fe7f6.avif 1200w, /landing/footer-artwork/dark-1920-638fa2f584cb.avif 1920w, /landing/footer-artwork/dark-3840-c0bf8a67335d.avif 3840w',
+ webpSrcSet:
+ '/landing/footer-artwork/dark-1200-117c8741a843.webp 1200w, /landing/footer-artwork/dark-1920-2c586b41810a.webp 1920w, /landing/footer-artwork/dark-3840-3140a296c362.webp 3840w',
+ src: '/landing/footer-artwork/dark-3840-3140a296c362.webp',
+ blurDataURL:
+ 'data:image/webp;base64,UklGRiwAAABXRUJQVlA4ICAAAABwAQCdASoIAAQAAkA4JaRtgAAIQAD+9MlFFCUE/BGgAA==',
+ },
+} as const
diff --git a/apps/sim/app/(landing)/components/landing-shell/landing-shell.test.tsx b/apps/sim/app/(landing)/components/landing-shell/landing-shell.test.tsx
index b707d1afc11..67b6a338242 100644
--- a/apps/sim/app/(landing)/components/landing-shell/landing-shell.test.tsx
+++ b/apps/sim/app/(landing)/components/landing-shell/landing-shell.test.tsx
@@ -9,9 +9,8 @@ import ts from '@typescript/typescript6'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { FOOTER_ARTWORK } from '@/app/(landing)/components/cta/footer-artwork.generated'
import { LandingShell } from '@/app/(landing)/components/landing-shell/landing-shell'
-import ctaDark from '@/public/landing/cta-san-francisco-painted-dark.webp'
-import ctaLight from '@/public/landing/cta-san-francisco-painted-light.webp'
interface TestLinkProps extends AnchorHTMLAttributes {
href: string
@@ -113,8 +112,8 @@ describe('LandingShell shared closing section', () => {
'Every agent your company runs. All in one place. '
)
expect(Array.from(cta.querySelectorAll('img'), (image) => image.getAttribute('src'))).toEqual([
- ctaLight,
- ctaDark,
+ FOOTER_ARTWORK.light.src,
+ FOOTER_ARTWORK.dark.src,
])
expect(Array.from(cta.querySelectorAll('img'), (image) => image.alt)).toEqual(['', ''])
expect(
diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts
index 7f315651b63..cc5c48bba11 100644
--- a/apps/sim/next.config.ts
+++ b/apps/sim/next.config.ts
@@ -268,6 +268,11 @@ const nextConfig: NextConfig = {
},
],
},
+ {
+ /** Generated footer artwork uses content hashes, so URLs are immutable. */
+ source: '/landing/footer-artwork/:path*',
+ headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
+ },
{
source: '/.well-known/:path*',
headers: [
diff --git a/apps/sim/public/landing/footer-artwork/dark-1200-117c8741a843.webp b/apps/sim/public/landing/footer-artwork/dark-1200-117c8741a843.webp
new file mode 100644
index 00000000000..38d54b39b14
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/dark-1200-117c8741a843.webp differ
diff --git a/apps/sim/public/landing/footer-artwork/dark-1200-a1f7345fe7f6.avif b/apps/sim/public/landing/footer-artwork/dark-1200-a1f7345fe7f6.avif
new file mode 100644
index 00000000000..9c800ff6955
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/dark-1200-a1f7345fe7f6.avif differ
diff --git a/apps/sim/public/landing/footer-artwork/dark-1920-2c586b41810a.webp b/apps/sim/public/landing/footer-artwork/dark-1920-2c586b41810a.webp
new file mode 100644
index 00000000000..0ff68f03bfa
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/dark-1920-2c586b41810a.webp differ
diff --git a/apps/sim/public/landing/footer-artwork/dark-1920-638fa2f584cb.avif b/apps/sim/public/landing/footer-artwork/dark-1920-638fa2f584cb.avif
new file mode 100644
index 00000000000..f878315bc9c
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/dark-1920-638fa2f584cb.avif differ
diff --git a/apps/sim/public/landing/footer-artwork/dark-3840-3140a296c362.webp b/apps/sim/public/landing/footer-artwork/dark-3840-3140a296c362.webp
new file mode 100644
index 00000000000..c15f94a86e5
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/dark-3840-3140a296c362.webp differ
diff --git a/apps/sim/public/landing/footer-artwork/dark-3840-c0bf8a67335d.avif b/apps/sim/public/landing/footer-artwork/dark-3840-c0bf8a67335d.avif
new file mode 100644
index 00000000000..6b1cbd10561
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/dark-3840-c0bf8a67335d.avif differ
diff --git a/apps/sim/public/landing/footer-artwork/light-1200-8764ab359e07.webp b/apps/sim/public/landing/footer-artwork/light-1200-8764ab359e07.webp
new file mode 100644
index 00000000000..f3512895a4e
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/light-1200-8764ab359e07.webp differ
diff --git a/apps/sim/public/landing/footer-artwork/light-1200-f29e84fa39b5.avif b/apps/sim/public/landing/footer-artwork/light-1200-f29e84fa39b5.avif
new file mode 100644
index 00000000000..75c79595d7c
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/light-1200-f29e84fa39b5.avif differ
diff --git a/apps/sim/public/landing/footer-artwork/light-1920-352cf7c570ad.avif b/apps/sim/public/landing/footer-artwork/light-1920-352cf7c570ad.avif
new file mode 100644
index 00000000000..4e2f1cc21e0
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/light-1920-352cf7c570ad.avif differ
diff --git a/apps/sim/public/landing/footer-artwork/light-1920-c9ec24660024.webp b/apps/sim/public/landing/footer-artwork/light-1920-c9ec24660024.webp
new file mode 100644
index 00000000000..1698a9c7a44
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/light-1920-c9ec24660024.webp differ
diff --git a/apps/sim/public/landing/footer-artwork/light-3840-6a6c3eab4b02.webp b/apps/sim/public/landing/footer-artwork/light-3840-6a6c3eab4b02.webp
new file mode 100644
index 00000000000..b6bbc2fff91
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/light-3840-6a6c3eab4b02.webp differ
diff --git a/apps/sim/public/landing/footer-artwork/light-3840-e889bb4bc039.avif b/apps/sim/public/landing/footer-artwork/light-3840-e889bb4bc039.avif
new file mode 100644
index 00000000000..12570ca4ad0
Binary files /dev/null and b/apps/sim/public/landing/footer-artwork/light-3840-e889bb4bc039.avif differ
diff --git a/scripts/generate-footer-artwork.ts b/scripts/generate-footer-artwork.ts
new file mode 100644
index 00000000000..c2747d21aac
--- /dev/null
+++ b/scripts/generate-footer-artwork.ts
@@ -0,0 +1,81 @@
+/**
+ * Pre-encodes the footer paintings so visitors never wait for image optimization.
+ * Run `bun run scripts/generate-footer-artwork.ts` after changing either master.
+ * Generated filenames include the encoded content hash for immutable caching.
+ */
+import { createHash } from 'node:crypto'
+import { mkdir, readdir, unlink, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import { createLogger } from '@sim/logger'
+import sharp from 'sharp'
+
+const logger = createLogger('FooterArtwork')
+const ROOT = path.resolve(import.meta.dirname, '..')
+const PUBLIC_DIR = path.join(ROOT, 'apps/sim/public/landing')
+const OUTPUT_DIR = path.join(PUBLIC_DIR, 'footer-artwork')
+const MANIFEST = path.join(
+ ROOT,
+ 'apps/sim/app/(landing)/components/cta/footer-artwork.generated.ts'
+)
+const WIDTHS = [1200, 1920, 3840] as const
+const THEMES = ['light', 'dark'] as const
+/** Match the existing Next.js quality=90 WebP and AVIF encodes. */
+const QUALITY = 90
+
+interface FooterArtwork {
+ avifSrcSet: string
+ webpSrcSet: string
+ src: string
+ blurDataURL: string
+}
+
+sharp.concurrency(2)
+await mkdir(OUTPUT_DIR, { recursive: true })
+const generatedFiles = new Set()
+const artwork: Record = {}
+
+for (const theme of THEMES) {
+ const master = path.join(PUBLIC_DIR, `cta-san-francisco-painted-${theme}.webp`)
+ const sources: Record<'avif' | 'webp', string[]> = { avif: [], webp: [] }
+ let fallback = ''
+ for (const format of ['avif', 'webp'] as const) {
+ for (const width of WIDTHS) {
+ const image = sharp(master).rotate().resize(width, undefined, { withoutEnlargement: true })
+ const buffer = await (format === 'avif'
+ ? image.avif({ quality: Math.round(QUALITY * (50 / 80)), effort: 3 })
+ : image.webp({ quality: QUALITY })
+ ).toBuffer()
+ const hash = createHash('sha256').update(buffer).digest('hex').slice(0, 12)
+ const filename = `${theme}-${width}-${hash}.${format}`
+ await writeFile(path.join(OUTPUT_DIR, filename), buffer)
+ generatedFiles.add(filename)
+ const url = `/landing/footer-artwork/${filename}`
+ sources[format].push(`${url} ${width}w`)
+ if (format === 'webp') fallback = url
+ logger.info('Encoded footer artwork', { theme, width, format, bytes: buffer.length })
+ }
+ }
+ const preview = await sharp(master).resize(8).webp({ quality: 70 }).toBuffer()
+ artwork[theme] = {
+ avifSrcSet: sources.avif.join(', '),
+ webpSrcSet: sources.webp.join(', '),
+ src: fallback,
+ blurDataURL: `data:image/webp;base64,${preview.toString('base64')}`,
+ }
+}
+
+await writeFile(
+ MANIFEST,
+ `/** Generated by scripts/generate-footer-artwork.ts. */\nexport const FOOTER_ARTWORK = ${JSON.stringify(artwork, null, 2)} as const\n`
+)
+const format = Bun.spawn(['bunx', 'biome', 'format', '--write', MANIFEST], { cwd: ROOT })
+if ((await format.exited) !== 0) throw new Error('Could not format the footer artwork manifest')
+
+for (const filename of await readdir(OUTPUT_DIR)) {
+ if (
+ /^(light|dark)-\d+-[a-f0-9]{12}\.(avif|webp)$/.test(filename) &&
+ !generatedFiles.has(filename)
+ ) {
+ await unlink(path.join(OUTPUT_DIR, filename))
+ }
+}