From a04a2aab8e8d3bb281428c7d17ee62b57f4bcc95 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Wed, 22 Feb 2023 15:40:18 -0800 Subject: [PATCH 01/10] Add Builder.io --- README.md | 3 +- data/domains.json | 1 + demo/src/examples.json | 4 +++ src/package-lock.json | 6 ++++ src/parse.ts | 2 ++ src/transform.ts | 2 ++ src/transformers/builder.test.ts | 48 ++++++++++++++++++++++++++++++++ src/transformers/builder.ts | 34 ++++++++++++++++++++++ src/types.ts | 1 + 9 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/package-lock.json create mode 100644 src/transformers/builder.test.ts create mode 100644 src/transformers/builder.ts diff --git a/README.md b/README.md index 33e7eaa..fe5dd65 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ is not auto-detected. - Imgix, including Unsplash, DatoCMS, Sanity and Prismic - Contentful +- Builder.io - Cloudinary - Shopify - WordPress.com and Jetpack Site Accelerator @@ -112,7 +113,7 @@ is not auto-detected. for transforming images. This is often used to resize images on the fly, but can also be used to apply other transforms such as cropping, rotation, compression, etc. This includes dedicated image CDNs such as Imgix and - Cloudinary, CMSs such as Contentful and Sanity, general CDNs such as Bunny.net + Cloudinary, CMSs such as Contentful, Builder.io and Sanity, general CDNs such as Bunny.net that provide an image API, but also other service providers such as Shopify. The CMSs and other service providers often use a dedicated image CDN to provide the image API, most commonly Imgix. In most cases they support the diff --git a/data/domains.json b/data/domains.json index bfe1f27..960f392 100644 --- a/data/domains.json +++ b/data/domains.json @@ -1,6 +1,7 @@ { "res.cloudinary.com": "cloudinary", "images.ctfassets.net": "contentful", + "cdn.builder.io": "builder", "images.prismic.io": "imgix", "www.datocms-assets.com": "imgix", "cdn.sanity.io": "imgix", diff --git a/demo/src/examples.json b/demo/src/examples.json index 516f834..7cc9a5e 100644 --- a/demo/src/examples.json +++ b/demo/src/examples.json @@ -7,6 +7,10 @@ "Contentful", "https://images.ctfassets.net/yadj1kx9rmg0/wtrHxeu3zEoEce2MokCSi/cf6f68efdcf625fdc060607df0f3baef/quwowooybuqbl6ntboz3.jpg?fm=jpg" ], + [ + "Builder.io", + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?format=webp" + ], [ "Cloudinary", "https://res.cloudinary.com/demo/image/upload/c_lfill,w_800,h_550,f_auto/dog.webp" diff --git a/src/package-lock.json b/src/package-lock.json new file mode 100644 index 0000000..f770275 --- /dev/null +++ b/src/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "src", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/src/parse.ts b/src/parse.ts index 14d4152..4de1534 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,5 +1,6 @@ import { getImageCdnForUrl } from "./detect.ts"; import { parse as contentful } from "./transformers/contentful.ts"; +import { parse as builder } from "./transformers/builder.ts"; import { parse as imgix } from "./transformers/imgix.ts"; import { parse as shopify } from "./transformers/shopify.ts"; import { parse as wordpress } from "./transformers/wordpress.ts"; @@ -12,6 +13,7 @@ import { ImageCdn, ParsedUrl, SupportedImageCdn, UrlParser } from "./types.ts"; export const parsers = { imgix, contentful, + builder, shopify, wordpress, cloudinary, diff --git a/src/transform.ts b/src/transform.ts index 99a6940..9768a0e 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -1,5 +1,6 @@ import { getImageCdnForUrl } from "./detect.ts"; import { transform as contentful } from "./transformers/contentful.ts"; +import { transform as builder } from "./transformers/builder.ts"; import { transform as imgix } from "./transformers/imgix.ts"; import { transform as shopify } from "./transformers/shopify.ts"; import { transform as wordpress } from "./transformers/wordpress.ts"; @@ -12,6 +13,7 @@ import { ImageCdn, SupportedImageCdn, UrlTransformer } from "./types.ts"; export const transformers = { imgix, contentful, + builder, shopify, wordpress, cloudinary, diff --git a/src/transformers/builder.test.ts b/src/transformers/builder.test.ts new file mode 100644 index 0000000..0854283 --- /dev/null +++ b/src/transformers/builder.test.ts @@ -0,0 +1,48 @@ +import { assertEquals } from "https://deno.land/std@0.172.0/testing/asserts.ts"; + +import { transform } from "./builder.ts"; + +const img = + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f"; + +Deno.test("builder", async (t) => { + await t.step("should format a URL", () => { + const result = transform({ + url: img, + width: 200, + height: 100, + }); + assertEquals( + result?.toString(), + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200&height=100", + ); + }); + await t.step("should not set height if not provided", () => { + const result = transform({ url: img, width: 200 }); + assertEquals( + result?.toString(), + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200", + ); + }); + await t.step("should delete height if not set", () => { + const url = new URL(img); + url.searchParams.set("h", "100"); + const result = transform({ url, width: 200 }); + assertEquals( + result?.toString(), + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200", + ); + }); + + await t.step("should round non-integer params", () => { + const result = transform({ + url: img, + width: 200.6, + height: 100.2, + }); + assertEquals( + result?.toString(), + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200&height=100", + ); + }); +}); diff --git a/src/transformers/builder.ts b/src/transformers/builder.ts new file mode 100644 index 0000000..ed1878d --- /dev/null +++ b/src/transformers/builder.ts @@ -0,0 +1,34 @@ +import { UrlParser, UrlTransformer } from "../types.ts"; +import { + getNumericParam, + setParamIfDefined, + setParamIfUndefined, +} from "../utils.ts"; + +export const parse: UrlParser<{ fit?: string }> = (url) => { + const parsedUrl = new URL(url); + + const width = getNumericParam(parsedUrl, "width"); + const height = getNumericParam(parsedUrl, "height"); + const quality = getNumericParam(parsedUrl, "quality"); + const format = parsedUrl.searchParams.get("format") || undefined; + parsedUrl.search = ""; + return { + width, + height, + format, + base: parsedUrl.toString(), + params: { quality }, + cdn: "builder", + }; +}; + +export const transform: UrlTransformer = ( + { url: originalUrl, width, height, format }, +) => { + const url = new URL(originalUrl); + setParamIfDefined(url, "width", width, true, true); + setParamIfDefined(url, "height", height, true, true); + setParamIfDefined(url, "format", format); + return url; +}; diff --git a/src/types.ts b/src/types.ts index 42ea5b4..23920ad 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,7 @@ export interface UrlParser< export type ImageCdn = | "contentful" + | "builder" | "cloudinary" | "cloudflare" | "imgix" From 1a5de01a5aa8d8e7990e2c8e93ee783d7aab8546 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Wed, 22 Feb 2023 15:42:15 -0800 Subject: [PATCH 02/10] Fix accidentally checked in file --- src/package-lock.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/package-lock.json diff --git a/src/package-lock.json b/src/package-lock.json deleted file mode 100644 index f770275..0000000 --- a/src/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "src", - "lockfileVersion": 2, - "requires": true, - "packages": {} -} From 70a6f1d8dc9a5b2f151c9a410bccaee9909d3b4a Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 07:39:16 -0800 Subject: [PATCH 03/10] Update src/transformers/builder.ts Co-authored-by: Matt Kane --- src/transformers/builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transformers/builder.ts b/src/transformers/builder.ts index ed1878d..2b2d767 100644 --- a/src/transformers/builder.ts +++ b/src/transformers/builder.ts @@ -5,7 +5,7 @@ import { setParamIfUndefined, } from "../utils.ts"; -export const parse: UrlParser<{ fit?: string }> = (url) => { +export const parse: UrlParser<{ quality?: number }> = (url) => { const parsedUrl = new URL(url); const width = getNumericParam(parsedUrl, "width"); From 7d6e3f983f9aa96f11cf6d4c23b1385229eb6a52 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 08:07:02 -0800 Subject: [PATCH 04/10] PR feedback and aim to get tests to pass --- demo/src/examples.json | 2 +- src/transformers/builder.test.ts | 2 +- src/transformers/builder.ts | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/demo/src/examples.json b/demo/src/examples.json index 7cc9a5e..4de7810 100644 --- a/demo/src/examples.json +++ b/demo/src/examples.json @@ -9,7 +9,7 @@ ], [ "Builder.io", - "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?format=webp" + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?format=webp&fit=contain" ], [ "Cloudinary", diff --git a/src/transformers/builder.test.ts b/src/transformers/builder.test.ts index 0854283..aba4e93 100644 --- a/src/transformers/builder.test.ts +++ b/src/transformers/builder.test.ts @@ -42,7 +42,7 @@ Deno.test("builder", async (t) => { }); assertEquals( result?.toString(), - "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200&height=100", + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=201&height=100", ); }); }); diff --git a/src/transformers/builder.ts b/src/transformers/builder.ts index 2b2d767..af7a00e 100644 --- a/src/transformers/builder.ts +++ b/src/transformers/builder.ts @@ -5,30 +5,36 @@ import { setParamIfUndefined, } from "../utils.ts"; +<<<<<<< HEAD export const parse: UrlParser<{ quality?: number }> = (url) => { +======= +export const parse: UrlParser<{ fit?: string, quality?: number }> = (url) => { +>>>>>>> df803a3 (PR feedback and aim to get tests to pass) const parsedUrl = new URL(url); const width = getNumericParam(parsedUrl, "width"); const height = getNumericParam(parsedUrl, "height"); const quality = getNumericParam(parsedUrl, "quality"); const format = parsedUrl.searchParams.get("format") || undefined; + const fit = parsedUrl.searchParams.get("fit") || undefined; parsedUrl.search = ""; return { width, height, format, base: parsedUrl.toString(), - params: { quality }, + params: { quality, fit }, cdn: "builder", }; }; export const transform: UrlTransformer = ( - { url: originalUrl, width, height, format }, + { url: originalUrl, width, height, format, fit }, ) => { const url = new URL(originalUrl); setParamIfDefined(url, "width", width, true, true); setParamIfDefined(url, "height", height, true, true); setParamIfDefined(url, "format", format); + setParamIfDefined(url, "fit", fit, "cover"); return url; }; From 582017fb3b7c6069f192632ab43bf5fea2e57b4f Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 08:14:58 -0800 Subject: [PATCH 05/10] Small tweaks --- data/domains.json | 2 +- src/transformers/builder.test.ts | 20 +++++++++++++++----- src/transformers/builder.ts | 9 +++------ src/types.ts | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/data/domains.json b/data/domains.json index 960f392..395c108 100644 --- a/data/domains.json +++ b/data/domains.json @@ -1,7 +1,7 @@ { "res.cloudinary.com": "cloudinary", "images.ctfassets.net": "contentful", - "cdn.builder.io": "builder", + "cdn.builder.io": "builder.io", "images.prismic.io": "imgix", "www.datocms-assets.com": "imgix", "cdn.sanity.io": "imgix", diff --git a/src/transformers/builder.test.ts b/src/transformers/builder.test.ts index aba4e93..438cb08 100644 --- a/src/transformers/builder.test.ts +++ b/src/transformers/builder.test.ts @@ -5,7 +5,7 @@ import { transform } from "./builder.ts"; const img = "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f"; -Deno.test("builder", async (t) => { +Deno.test("builder.io", async (t) => { await t.step("should format a URL", () => { const result = transform({ url: img, @@ -14,14 +14,14 @@ Deno.test("builder", async (t) => { }); assertEquals( result?.toString(), - "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200&height=100", + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=200&height=100", ); }); await t.step("should not set height if not provided", () => { const result = transform({ url: img, width: 200 }); assertEquals( result?.toString(), - "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200", + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=200", ); }); await t.step("should delete height if not set", () => { @@ -30,7 +30,7 @@ Deno.test("builder", async (t) => { const result = transform({ url, width: 200 }); assertEquals( result?.toString(), - "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=200", + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=200", ); }); @@ -42,7 +42,17 @@ Deno.test("builder", async (t) => { }); assertEquals( result?.toString(), - "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?width=201&height=100", + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=cover&width=201&height=100", + ); + }); + + await t.step("should not set fit=cover if another value exists", () => { + const url = new URL(img); + url.searchParams.set("fit", "inside"); + const result = transform({ url, width: 200 }); + assertEquals( + result?.toString(), + "https://images.ctfassets.net/aaaa/xxxx/yyyy/how-to-wow-a-customer.jpg?fit=inside&w=200", ); }); }); diff --git a/src/transformers/builder.ts b/src/transformers/builder.ts index af7a00e..2ed5654 100644 --- a/src/transformers/builder.ts +++ b/src/transformers/builder.ts @@ -5,11 +5,7 @@ import { setParamIfUndefined, } from "../utils.ts"; -<<<<<<< HEAD -export const parse: UrlParser<{ quality?: number }> = (url) => { -======= export const parse: UrlParser<{ fit?: string, quality?: number }> = (url) => { ->>>>>>> df803a3 (PR feedback and aim to get tests to pass) const parsedUrl = new URL(url); const width = getNumericParam(parsedUrl, "width"); @@ -18,13 +14,14 @@ export const parse: UrlParser<{ fit?: string, quality?: number }> = (url) => { const format = parsedUrl.searchParams.get("format") || undefined; const fit = parsedUrl.searchParams.get("fit") || undefined; parsedUrl.search = ""; + return { width, height, format, base: parsedUrl.toString(), params: { quality, fit }, - cdn: "builder", + cdn: "builder.io", }; }; @@ -35,6 +32,6 @@ export const transform: UrlTransformer = ( setParamIfDefined(url, "width", width, true, true); setParamIfDefined(url, "height", height, true, true); setParamIfDefined(url, "format", format); - setParamIfDefined(url, "fit", fit, "cover"); + url.searchParams.set("fit", fit || "cover"); return url; }; diff --git a/src/types.ts b/src/types.ts index 23920ad..5a85175 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,7 +54,7 @@ export interface UrlParser< export type ImageCdn = | "contentful" - | "builder" + | "builder.io" | "cloudinary" | "cloudflare" | "imgix" From 5a7ff5c249cbc25a6e0e0d03c89a5c48c4c0f96f Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 08:16:39 -0800 Subject: [PATCH 06/10] Minor fix --- src/transformers/builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transformers/builder.ts b/src/transformers/builder.ts index 2ed5654..57f4ff5 100644 --- a/src/transformers/builder.ts +++ b/src/transformers/builder.ts @@ -32,6 +32,6 @@ export const transform: UrlTransformer = ( setParamIfDefined(url, "width", width, true, true); setParamIfDefined(url, "height", height, true, true); setParamIfDefined(url, "format", format); - url.searchParams.set("fit", fit || "cover"); + setParamIfUndefined(url, "fit", "cover"); return url; }; From 4a079417a99076579dbc0d028b2a7a229471b64d Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 08:17:19 -0800 Subject: [PATCH 07/10] Fix test --- src/transformers/builder.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transformers/builder.test.ts b/src/transformers/builder.test.ts index 438cb08..6cb1c0a 100644 --- a/src/transformers/builder.test.ts +++ b/src/transformers/builder.test.ts @@ -26,7 +26,7 @@ Deno.test("builder.io", async (t) => { }); await t.step("should delete height if not set", () => { const url = new URL(img); - url.searchParams.set("h", "100"); + url.searchParams.set("height", "100"); const result = transform({ url, width: 200 }); assertEquals( result?.toString(), From e037dd9a5c2cae8c316e88c4e3ae18a560c0fe73 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 08:30:52 -0800 Subject: [PATCH 08/10] Tests passing --- src/transformers/builder.test.ts | 2 +- src/transformers/builder.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/transformers/builder.test.ts b/src/transformers/builder.test.ts index 6cb1c0a..9fd7196 100644 --- a/src/transformers/builder.test.ts +++ b/src/transformers/builder.test.ts @@ -52,7 +52,7 @@ Deno.test("builder.io", async (t) => { const result = transform({ url, width: 200 }); assertEquals( result?.toString(), - "https://images.ctfassets.net/aaaa/xxxx/yyyy/how-to-wow-a-customer.jpg?fit=inside&w=200", + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?fit=inside&width=200", ); }); }); diff --git a/src/transformers/builder.ts b/src/transformers/builder.ts index 57f4ff5..1b60af6 100644 --- a/src/transformers/builder.ts +++ b/src/transformers/builder.ts @@ -5,7 +5,7 @@ import { setParamIfUndefined, } from "../utils.ts"; -export const parse: UrlParser<{ fit?: string, quality?: number }> = (url) => { +export const parse: UrlParser<{ fit?: string; quality?: number }> = (url) => { const parsedUrl = new URL(url); const width = getNumericParam(parsedUrl, "width"); @@ -26,12 +26,12 @@ export const parse: UrlParser<{ fit?: string, quality?: number }> = (url) => { }; export const transform: UrlTransformer = ( - { url: originalUrl, width, height, format, fit }, + { url: originalUrl, width, height, format }, ) => { const url = new URL(originalUrl); + setParamIfUndefined(url, "fit", "cover"); setParamIfDefined(url, "width", width, true, true); setParamIfDefined(url, "height", height, true, true); setParamIfDefined(url, "format", format); - setParamIfUndefined(url, "fit", "cover"); return url; }; From a5197ff2d8a19bc687c7bba6ee8f396929beec38 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 09:17:11 -0800 Subject: [PATCH 09/10] Fixes --- demo/src/examples.json | 2 +- src/parse.ts | 2 +- src/transform.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/src/examples.json b/demo/src/examples.json index 4de7810..2e81dd5 100644 --- a/demo/src/examples.json +++ b/demo/src/examples.json @@ -9,7 +9,7 @@ ], [ "Builder.io", - "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?format=webp&fit=contain" + "https://cdn.builder.io/api/v1/image/assets%2FYJIGb4i01jvw0SRdL5Bt%2F462d29d57dda42cb9e26441501db535f?format=webp&fit=cover" ], [ "Cloudinary", diff --git a/src/parse.ts b/src/parse.ts index 4de1534..dc16233 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -13,7 +13,7 @@ import { ImageCdn, ParsedUrl, SupportedImageCdn, UrlParser } from "./types.ts"; export const parsers = { imgix, contentful, - builder, + "builder.io": builder, shopify, wordpress, cloudinary, diff --git a/src/transform.ts b/src/transform.ts index 9768a0e..b00a0f6 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -13,7 +13,7 @@ import { ImageCdn, SupportedImageCdn, UrlTransformer } from "./types.ts"; export const transformers = { imgix, contentful, - builder, + "builder.io": builder, shopify, wordpress, cloudinary, From c498e5130ca45a896eb786db1dc904c15f664d02 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Thu, 23 Feb 2023 09:57:15 -0800 Subject: [PATCH 10/10] Minor CSS suggestion --- demo/src/style.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/demo/src/style.css b/demo/src/style.css index c87e389..93a0d52 100644 --- a/demo/src/style.css +++ b/demo/src/style.css @@ -69,11 +69,14 @@ summary { .imagePanel { - display: grid; place-items: center; } +.imagePanel img { + object-fit: cover; +} + .code { overflow: auto; }