chore(deps): drop the archived image-size dependency (#6485)

* chore(deps): drop the archived image-size dependency

* improvement(content): cover GIF and warn when OG dimensions are unreadable

* improvement(content): read OG dimensions via sharp instead of a bespoke parser
This commit is contained in:
Waleed
2026-08-09 23:14:14 -07:00
committed by GitHub
parent 3fe2f4f5af
commit 4228b04eaa
4 changed files with 73 additions and 8 deletions
+55
View File
@@ -0,0 +1,55 @@
/**
* @vitest-environment node
*/
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import sharp from 'sharp'
import { describe, expect, it } from 'vitest'
/**
* Guards the content invariant behind `ogImageWidth`/`ogImageHeight` in
* `registry-factory`: every local `ogImage` must exist and expose intrinsic
* dimensions, or the SEO builders silently fall back to a 1200x630 default that
* misdescribes the real asset.
*
* It also pins the format to one the social crawlers actually accept. SVG is the
* trap worth naming: it renders fine in the browser and `sharp` reports
* dimensions for it, so a dimension check alone would pass while Open Graph
* previews silently break.
*/
const CRAWLER_SAFE_FORMATS = ['jpeg', 'png', 'webp', 'gif']
function collectOgImages(): { slug: string; ogImage: string }[] {
const entries: { slug: string; ogImage: string }[] = []
for (const dir of ['content/blog', 'content/library']) {
if (!fs.existsSync(dir)) continue
for (const slug of fs.readdirSync(dir)) {
const mdxPath = path.join(dir, slug, 'index.mdx')
if (!fs.existsSync(mdxPath)) continue
const { data } = matter(fs.readFileSync(mdxPath, 'utf-8'))
if (typeof data.ogImage === 'string' && !data.ogImage.startsWith('http')) {
entries.push({ slug, ogImage: data.ogImage })
}
}
}
return entries
}
describe('content OG images', () => {
const entries = collectOgImages()
it('finds local OG images to check', () => {
expect(entries.length).toBeGreaterThan(0)
})
it.each(entries)('$slug resolves readable dimensions for $ogImage', async ({ ogImage }) => {
const file = path.join('public', ogImage)
expect(fs.existsSync(file), `${file} does not exist`).toBe(true)
const { width, height, format } = await sharp(fs.readFileSync(file)).metadata()
expect(width, `${file} has no readable width`).toBeGreaterThan(0)
expect(height, `${file} has no readable height`).toBeGreaterThan(0)
expect(CRAWLER_SAFE_FORMATS, `${file} is a ${format}, which crawlers reject`).toContain(format)
})
})
+16 -3
View File
@@ -1,17 +1,20 @@
import fs from 'fs/promises'
import path from 'path'
import { cache } from 'react'
import { createLogger } from '@sim/logger'
import matter from 'gray-matter'
import { imageSize } from 'image-size'
import { compileMDX } from 'next-mdx-remote/rsc'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypeSlug from 'rehype-slug'
import remarkGfm from 'remark-gfm'
import sharp from 'sharp'
import { mdxComponents } from '@/lib/content/mdx'
import type { Author, ContentMeta, ContentPost, TagWithCount } from '@/lib/content/schema'
import { AuthorSchema, ContentFrontmatterSchema } from '@/lib/content/schema'
import { byDateDesc, ensureContentDirs, toIsoDate } from '@/lib/content/utils'
const logger = createLogger('ContentRegistry')
/** Loads a post's custom MDX component overrides, keyed by slug. */
export type ContentComponentLoaders = Record<
string,
@@ -95,6 +98,10 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
* SEO builders can declare accurate `og:image` and JSON-LD sizes. Returns
* null for remote URLs or unreadable files, in which case the builders fall
* back to the 1200x630 OG default.
*
* Uses `sharp`, which only parses headers for `metadata()`. It replaced the
* `image-size` package, archived upstream with unpatched DoS advisories in
* its ICNS/JXL/HEIF parsers (GHSA-w3rx-r6r6-pgpr, GHSA-5p2g-fcmc-qvqq).
*/
async function readOgImageDimensions(
ogImage: string
@@ -102,8 +109,14 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
if (ogImage.startsWith('http')) return null
try {
const buffer = await fs.readFile(path.join(process.cwd(), 'public', ogImage))
const { width, height } = imageSize(buffer)
return width && height ? { width, height } : null
const { width, height } = await sharp(buffer).metadata()
if (!width || !height) {
logger.warn('OG image has no readable dimensions; falling back to the OG default', {
ogImage,
})
return null
}
return { width, height }
} catch {
return null
}
-1
View File
@@ -174,7 +174,6 @@
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"idb-keyval": "6.2.2",
"image-size": "2.0.2",
"imapflow": "1.2.4",
"input-otp": "^1.4.2",
"ioredis": "^5.6.0",
+2 -4
View File
@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
@@ -276,7 +277,6 @@
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"idb-keyval": "6.2.2",
"image-size": "2.0.2",
"imapflow": "1.2.4",
"input-otp": "^1.4.2",
"ioredis": "^5.6.0",
@@ -3121,7 +3121,7 @@
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
"image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="],
"imapflow": ["imapflow@1.2.4", "", { "dependencies": { "@zone-eu/mailsplit": "5.4.8", "encoding-japanese": "2.2.0", "iconv-lite": "0.7.1", "libbase64": "1.3.0", "libmime": "5.3.7", "libqp": "2.1.1", "nodemailer": "7.0.12", "pino": "10.1.0", "socks": "2.8.7" } }, "sha512-X/eRQeje33uZycfopjwoQKKbya+bBIaqpviOFxhPOD24DXU2hMfXwYe9e8j1+ADwFVgTvKq4G2/ljjZK3Y8mvg=="],
@@ -5155,8 +5155,6 @@
"pptxgenjs/@types/node": ["@types/node@22.19.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA=="],
"pptxgenjs/image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="],
"protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
"proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],