From 1f6e47c2380f0014b700fc2b96cf2e2d7b2819f4 Mon Sep 17 00:00:00 2001 From: Cornelius Ukena Date: Sat, 8 Aug 2026 17:55:55 -0500 Subject: [PATCH 1/5] fix(frontend): avoid zeroed 2x srcset dimensions and fall back to intrinsic size --- frontend/src/lib/util/image.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/util/image.ts b/frontend/src/lib/util/image.ts index 3b686e2..e5bcb4f 100644 --- a/frontend/src/lib/util/image.ts +++ b/frontend/src/lib/util/image.ts @@ -50,7 +50,11 @@ function attributes(source: ImageSource, loading = 'lazy') { if (asset && asset.uid && asset.src) { const src = imgix(asset.src, args || {}) - const src2x = imgix(asset.src, { ...args, width: width * 2, height: height * 2 }) + const src2x = imgix(asset.src, { + ...args, + ...(width ? { width: width * 2 } : {}), + ...(height ? { height: height * 2 } : {}), + }) const alt = asset.alt if (!width && height) { @@ -61,6 +65,9 @@ function attributes(source: ImageSource, loading = 'lazy') { height = Math.floor(Math.min(asset.width, width) * (asset.height / asset.width)) } + width = width || asset.width + height = height || asset.height + Object.assign(attrs, { width, height, From 4b2d8517350a264035f26dbf8feef18f765aa6b0 Mon Sep 17 00:00:00 2001 From: Cornelius Ukena Date: Sat, 8 Aug 2026 17:55:55 -0500 Subject: [PATCH 2/5] fix(buildchain): hash raw image bytes and pass tinify key explicitly Hashing asset.source.toString() is lossy for binary images, so distinct images could collide on the same cache key. The plugin also read TINYPNG_KEY from process.env, which SvelteKit never populates from .env during builds; the key is now loaded via loadEnv and passed as a plugin option. --- frontend/.env.example | 1 + frontend/utility/vite-plugin-tinify.ts | 8 +-- frontend/vite.config.ts | 90 ++++++++++++++------------ utility/vite-plugin-tinify.ts | 8 +-- 4 files changed, 56 insertions(+), 51 deletions(-) diff --git a/frontend/.env.example b/frontend/.env.example index 86be4ce..b45984a 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -3,3 +3,4 @@ PUBLIC_CMS_URL=https://cms.craftcms-sveltekit.ddev.site PUBLIC_OBJECT_STORAGE_URL= PUBLIC_IMGIX_URL=https://craftcms-sveltekit.imgix.net CRAFT_INTERNAL_HOST=http://0.0.0.0 +TINYPNG_KEY= diff --git a/frontend/utility/vite-plugin-tinify.ts b/frontend/utility/vite-plugin-tinify.ts index 8de40a9..9878bb2 100644 --- a/frontend/utility/vite-plugin-tinify.ts +++ b/frontend/utility/vite-plugin-tinify.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs' import tinify from 'tinify' import type { Plugin } from 'vite' -export default function tinifyPlugin(): Plugin { +export default function tinifyPlugin(options: { key?: string } = {}): Plugin { return { name: 'vite-plugin-tinify', async generateBundle(_, bundler) { @@ -12,7 +12,7 @@ export default function tinifyPlugin(): Plugin { continue } - const checksum = crypto.createHash('sha1').update(asset.source.toString()).digest('hex') + const checksum = crypto.createHash('sha1').update(asset.source).digest('hex') const checksumfile = `node_modules/.vite/tinify/${checksum}` let content: Buffer | Uint8Array | undefined @@ -20,7 +20,7 @@ export default function tinifyPlugin(): Plugin { if (fs.existsSync(checksumfile)) { content = fs.readFileSync(checksumfile) } else { - if (!process.env.TINYPNG_KEY) { + if (!options.key) { throw new Error('vite-plugin-tinify: TINYPNG_KEY not defined. **Images not optimized**') } @@ -42,7 +42,7 @@ export default function tinifyPlugin(): Plugin { }, process.cwd()) } - tinify.key = process.env.TINYPNG_KEY + tinify.key = options.key content = await tinify.fromBuffer(asset.source).toBuffer() fs.writeFile(checksumfile, content, error => error && console.log(error)) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1ed262a..6b1377c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,58 +1,62 @@ import { join, dirname } from 'node:path' import { sveltekit } from '@sveltejs/kit/vite' import tailwindcss from '@tailwindcss/vite' -import { defineConfig, searchForWorkspaceRoot } from 'vite' +import { defineConfig, loadEnv, searchForWorkspaceRoot } from 'vite' import svgo from './utility/vite-plugin-svgo' import tinify from './utility/vite-plugin-tinify' -export default defineConfig({ - plugins: [tailwindcss(), sveltekit(), tinify(), svgo()], - css: { - transformer: 'lightningcss', - lightningcss: { - drafts: { customMedia: true }, - customAtRules: { - 'reference': { prelude: '', body: null }, - 'utility': { prelude: '*', body: 'style-block' }, - 'theme': { prelude: '*', body: 'style-block' }, - 'custom-variant': { prelude: '*', body: null }, - 'source': { prelude: '*', body: null }, - 'apply': { prelude: '*', body: null }, +export default defineConfig(({ mode }) => { + const { TINYPNG_KEY } = loadEnv(mode, import.meta.dirname, '') + + return { + plugins: [tailwindcss(), sveltekit(), tinify({ key: TINYPNG_KEY }), svgo()], + css: { + transformer: 'lightningcss', + lightningcss: { + drafts: { customMedia: true }, + customAtRules: { + 'reference': { prelude: '', body: null }, + 'utility': { prelude: '*', body: 'style-block' }, + 'theme': { prelude: '*', body: 'style-block' }, + 'custom-variant': { prelude: '*', body: null }, + 'source': { prelude: '*', body: null }, + 'apply': { prelude: '*', body: null }, + }, }, }, - }, - build: { - cssMinify: 'lightningcss', - rollupOptions: { - output: { - manualChunks(id) { - if (id.includes('node_modules/svelte')) { - return 'svelte' - } + build: { + cssMinify: 'lightningcss', + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules/svelte')) { + return 'svelte' + } - if (id.includes('node_modules/zod')) { - return 'zod' - } + if (id.includes('node_modules/zod')) { + return 'zod' + } + }, }, }, }, - }, - resolve: { - alias: { - '@fontawesome': join( - dirname(import.meta.dirname), - 'backend/vendor/npm-asset/fortawesome--fontawesome-pro/svgs', - ), + resolve: { + alias: { + '@fontawesome': join( + dirname(import.meta.dirname), + 'backend/vendor/npm-asset/fortawesome--fontawesome-pro/svgs', + ), + }, }, - }, - server: { - allowedHosts: ['.ddev.site'], - fs: { - allow: [ - join(import.meta.dirname, 'static'), - join(dirname(import.meta.dirname), 'backend/vendor/npm-asset/fortawesome--fontawesome-pro/svgs'), - searchForWorkspaceRoot(import.meta.dirname), - ], + server: { + allowedHosts: ['.ddev.site'], + fs: { + allow: [ + join(import.meta.dirname, 'static'), + join(dirname(import.meta.dirname), 'backend/vendor/npm-asset/fortawesome--fontawesome-pro/svgs'), + searchForWorkspaceRoot(import.meta.dirname), + ], + }, }, - }, + } }) diff --git a/utility/vite-plugin-tinify.ts b/utility/vite-plugin-tinify.ts index f8333d5..ed224ab 100644 --- a/utility/vite-plugin-tinify.ts +++ b/utility/vite-plugin-tinify.ts @@ -3,12 +3,12 @@ import fs from 'node:fs' import tinify from 'tinify' import type { Plugin } from 'vite' -export default (): Plugin => ({ +export default (options: { key?: string } = {}): Plugin => ({ name: 'vite-plugin-tinify', async generateBundle(_, bundler) { for (const [path, asset] of Object.entries(bundler)) { if (/\.(png|jpe?g)$/.test(path) && asset.type === 'asset' && typeof asset.source !== 'undefined') { - const checksum = crypto.createHash('sha1').update(asset.source.toString()).digest('hex') + const checksum = crypto.createHash('sha1').update(asset.source).digest('hex') const checksumfile = `node_modules/.vite/tinify/${checksum}` let content: Buffer | Uint8Array | undefined @@ -16,7 +16,7 @@ export default (): Plugin => ({ if (fs.existsSync(checksumfile)) { content = fs.readFileSync(checksumfile) } else { - if (!process.env.TINYPNG_KEY) { + if (!options.key) { throw new Error('vite-plugin-tinify: TINYPNG_KEY not defined. **Images not optimized**') } @@ -40,7 +40,7 @@ export default (): Plugin => ({ }, process.cwd()) } - tinify.key = process.env.TINYPNG_KEY + tinify.key = options.key content = await tinify.fromBuffer(asset.source).toBuffer() fs.writeFile(checksumfile, content, error => error && console.log(error)) From fe799dd9f411e77212613d6d347f618733346538 Mon Sep 17 00:00:00 2001 From: Cornelius Ukena Date: Sat, 8 Aug 2026 17:55:55 -0500 Subject: [PATCH 3/5] fix(backend): stop leaking onlyEnv markup via template tags Wrapping excluded markup in