Skip to content

Commit 4228b04

Browse files
authored
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
1 parent 3fe2f4f commit 4228b04

4 files changed

Lines changed: 73 additions & 8 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import fs from 'fs'
5+
import path from 'path'
6+
import matter from 'gray-matter'
7+
import sharp from 'sharp'
8+
import { describe, expect, it } from 'vitest'
9+
10+
/**
11+
* Guards the content invariant behind `ogImageWidth`/`ogImageHeight` in
12+
* `registry-factory`: every local `ogImage` must exist and expose intrinsic
13+
* dimensions, or the SEO builders silently fall back to a 1200x630 default that
14+
* misdescribes the real asset.
15+
*
16+
* It also pins the format to one the social crawlers actually accept. SVG is the
17+
* trap worth naming: it renders fine in the browser and `sharp` reports
18+
* dimensions for it, so a dimension check alone would pass while Open Graph
19+
* previews silently break.
20+
*/
21+
const CRAWLER_SAFE_FORMATS = ['jpeg', 'png', 'webp', 'gif']
22+
23+
function collectOgImages(): { slug: string; ogImage: string }[] {
24+
const entries: { slug: string; ogImage: string }[] = []
25+
for (const dir of ['content/blog', 'content/library']) {
26+
if (!fs.existsSync(dir)) continue
27+
for (const slug of fs.readdirSync(dir)) {
28+
const mdxPath = path.join(dir, slug, 'index.mdx')
29+
if (!fs.existsSync(mdxPath)) continue
30+
const { data } = matter(fs.readFileSync(mdxPath, 'utf-8'))
31+
if (typeof data.ogImage === 'string' && !data.ogImage.startsWith('http')) {
32+
entries.push({ slug, ogImage: data.ogImage })
33+
}
34+
}
35+
}
36+
return entries
37+
}
38+
39+
describe('content OG images', () => {
40+
const entries = collectOgImages()
41+
42+
it('finds local OG images to check', () => {
43+
expect(entries.length).toBeGreaterThan(0)
44+
})
45+
46+
it.each(entries)('$slug resolves readable dimensions for $ogImage', async ({ ogImage }) => {
47+
const file = path.join('public', ogImage)
48+
expect(fs.existsSync(file), `${file} does not exist`).toBe(true)
49+
50+
const { width, height, format } = await sharp(fs.readFileSync(file)).metadata()
51+
expect(width, `${file} has no readable width`).toBeGreaterThan(0)
52+
expect(height, `${file} has no readable height`).toBeGreaterThan(0)
53+
expect(CRAWLER_SAFE_FORMATS, `${file} is a ${format}, which crawlers reject`).toContain(format)
54+
})
55+
})

apps/sim/lib/content/registry-factory.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import fs from 'fs/promises'
22
import path from 'path'
33
import { cache } from 'react'
4+
import { createLogger } from '@sim/logger'
45
import matter from 'gray-matter'
5-
import { imageSize } from 'image-size'
66
import { compileMDX } from 'next-mdx-remote/rsc'
77
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
88
import rehypeSlug from 'rehype-slug'
99
import remarkGfm from 'remark-gfm'
10+
import sharp from 'sharp'
1011
import { mdxComponents } from '@/lib/content/mdx'
1112
import type { Author, ContentMeta, ContentPost, TagWithCount } from '@/lib/content/schema'
1213
import { AuthorSchema, ContentFrontmatterSchema } from '@/lib/content/schema'
1314
import { byDateDesc, ensureContentDirs, toIsoDate } from '@/lib/content/utils'
1415

16+
const logger = createLogger('ContentRegistry')
17+
1518
/** Loads a post's custom MDX component overrides, keyed by slug. */
1619
export type ContentComponentLoaders = Record<
1720
string,
@@ -95,15 +98,25 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
9598
* SEO builders can declare accurate `og:image` and JSON-LD sizes. Returns
9699
* null for remote URLs or unreadable files, in which case the builders fall
97100
* back to the 1200x630 OG default.
101+
*
102+
* Uses `sharp`, which only parses headers for `metadata()`. It replaced the
103+
* `image-size` package, archived upstream with unpatched DoS advisories in
104+
* its ICNS/JXL/HEIF parsers (GHSA-w3rx-r6r6-pgpr, GHSA-5p2g-fcmc-qvqq).
98105
*/
99106
async function readOgImageDimensions(
100107
ogImage: string
101108
): Promise<{ width: number; height: number } | null> {
102109
if (ogImage.startsWith('http')) return null
103110
try {
104111
const buffer = await fs.readFile(path.join(process.cwd(), 'public', ogImage))
105-
const { width, height } = imageSize(buffer)
106-
return width && height ? { width, height } : null
112+
const { width, height } = await sharp(buffer).metadata()
113+
if (!width || !height) {
114+
logger.warn('OG image has no readable dimensions; falling back to the OG default', {
115+
ogImage,
116+
})
117+
return null
118+
}
119+
return { width, height }
107120
} catch {
108121
return null
109122
}

apps/sim/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@
174174
"http-proxy-agent": "7.0.2",
175175
"https-proxy-agent": "7.0.6",
176176
"idb-keyval": "6.2.2",
177-
"image-size": "2.0.2",
178177
"imapflow": "1.2.4",
179178
"input-otp": "^1.4.2",
180179
"ioredis": "^5.6.0",

bun.lock

Lines changed: 2 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)