Openstatus www.openstatus.dev

fix: web layout shift (#1803)

* fix: image layout shift

* fix: image dimensions

* fix: missing suspense fallback

authored by

Maximilian Kaske and committed by
GitHub
83a8879d 2175ea78

+105 -19
+29 -18
apps/web/src/content/mdx.tsx
··· 1 1 import { existsSync } from "node:fs"; 2 2 import { join } from "node:path"; 3 + import { getImageDimensions } from "@/lib/image-dimensions"; 3 4 import { cn } from "@/lib/utils"; 4 5 import { Button } from "@openstatus/ui"; 5 6 import { MDXRemote, type MDXRemoteProps } from "next-mdx-remote/rsc"; ··· 241 242 className, 242 243 ...props 243 244 }: React.ComponentProps<typeof Image>) { 244 - const { src, alt, ...rest } = props; 245 + const { src, alt, width, height, ...rest } = props; 245 246 246 247 if (!src || typeof src !== "string") { 247 248 return ( ··· 256 257 className={className} 257 258 src={src} 258 259 alt={alt ?? "image"} 259 - width={0} 260 - height={0} 260 + fill 261 261 sizes="100vw" 262 - style={{ width: "100%", height: "auto" }} 262 + style={{ objectFit: "contain" }} 263 263 {...rest} 264 264 /> 265 265 </ImageZoom> ··· 267 267 </figure> 268 268 ); 269 269 } 270 + 271 + // Get actual image dimensions from filesystem 272 + const dimensions = getImageDimensions(src); 273 + const imageWidth = width || dimensions?.width || 1200; 274 + const imageHeight = height || dimensions?.height || 630; 270 275 271 276 // Generate dark mode image path by adding .dark before extension 272 277 const getDarkImagePath = (path: string) => { ··· 304 309 {...rest} 305 310 src={src} 306 311 alt={alt ?? ""} 307 - width={0} 308 - height={0} 312 + width={imageWidth} 313 + height={imageHeight} 309 314 sizes="100vw" 310 315 style={{ width: "100%", height: "auto" }} 311 316 className={cn("block dark:hidden", className)} ··· 321 326 {...rest} 322 327 src={useDarkImage ? darkSrc : src} 323 328 alt={alt ?? ""} 324 - width={0} 325 - height={0} 329 + width={imageWidth} 330 + height={imageHeight} 326 331 sizes="100vw" 327 332 style={{ width: "100%", height: "auto" }} 328 333 className={cn("hidden dark:block", className)} ··· 359 364 }, 360 365 }; 361 366 367 + function MDXContent(props: MDXRemoteProps) { 368 + return ( 369 + <MDXRemote 370 + {...props} 371 + components={ 372 + { 373 + ...components, 374 + ...props.components, 375 + } as MDXRemoteProps["components"] 376 + } 377 + /> 378 + ); 379 + } 380 + 362 381 export function CustomMDX(props: MDXRemoteProps) { 363 382 return ( 364 - <React.Suspense> 383 + <React.Suspense fallback={<MDXContent {...props} />}> 365 384 <HighlightText> 366 - <MDXRemote 367 - {...props} 368 - components={ 369 - { 370 - ...components, 371 - ...props.components, 372 - } as MDXRemoteProps["components"] 373 - } 374 - /> 385 + <MDXContent {...props} /> 375 386 </HighlightText> 376 387 </React.Suspense> 377 388 );
+1 -1
apps/web/src/content/pages/home.mdx
··· 15 15 /> 16 16 </div> 17 17 18 - <Image src="/assets/landing/dashboard.png" alt="dashboard monitor overview" /> 18 + <Image src="/assets/landing/dashboard.png" alt="dashboard monitor overview" priority /> 19 19 20 20 ## Used by awesome folks 21 21
+75
apps/web/src/lib/image-dimensions.ts
··· 1 + import { readFileSync } from "node:fs"; 2 + import { join } from "node:path"; 3 + 4 + interface ImageDimensions { 5 + width: number; 6 + height: number; 7 + } 8 + 9 + /** 10 + * Get image dimensions from PNG file 11 + * PNG spec: width and height are stored at bytes 16-23 as 4-byte integers 12 + */ 13 + function getPngDimensions(buffer: Buffer): ImageDimensions { 14 + const width = buffer.readUInt32BE(16); 15 + const height = buffer.readUInt32BE(20); 16 + return { width, height }; 17 + } 18 + 19 + /** 20 + * Get image dimensions from JPEG file 21 + */ 22 + function getJpegDimensions(buffer: Buffer): ImageDimensions { 23 + let offset = 2; // Skip SOI marker 24 + 25 + while (offset < buffer.length) { 26 + // Check for marker 27 + if (buffer[offset] !== 0xff) break; 28 + 29 + const marker = buffer[offset + 1]; 30 + offset += 2; 31 + 32 + // SOF markers (Start of Frame) 33 + if ( 34 + marker >= 0xc0 && 35 + marker <= 0xcf && 36 + marker !== 0xc4 && 37 + marker !== 0xc8 && 38 + marker !== 0xcc 39 + ) { 40 + const height = buffer.readUInt16BE(offset + 3); 41 + const width = buffer.readUInt16BE(offset + 5); 42 + return { width, height }; 43 + } 44 + 45 + // Skip to next marker 46 + const segmentLength = buffer.readUInt16BE(offset); 47 + offset += segmentLength; 48 + } 49 + 50 + throw new Error("Could not find JPEG dimensions"); 51 + } 52 + 53 + /** 54 + * Get dimensions for an image in the public directory 55 + */ 56 + export function getImageDimensions(publicPath: string): ImageDimensions | null { 57 + try { 58 + const fullPath = join(process.cwd(), "public", publicPath); 59 + const buffer = readFileSync(fullPath); 60 + 61 + // Check file signature 62 + if (buffer[0] === 0x89 && buffer.toString("ascii", 1, 4) === "PNG") { 63 + return getPngDimensions(buffer); 64 + } 65 + 66 + if (buffer[0] === 0xff && buffer[1] === 0xd8) { 67 + return getJpegDimensions(buffer); 68 + } 69 + 70 + return null; 71 + } catch (error) { 72 + console.warn(`Failed to get dimensions for ${publicPath}:`, error); 73 + return null; 74 + } 75 + }