Skip to content

Commit 52ee2a5

Browse files
authored
fix(landing): load sharp lazily so a missing native binary can't 500 /blog and /library (#6496)
1 parent 5da48d0 commit 52ee2a5

4 files changed

Lines changed: 104 additions & 17 deletions

File tree

apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-menu-chip/types'
22

33
/**
4-
* The Platform menu - Sim's modules. Six items in a three-column grid. Each
5-
* description names the outcome the module unlocks for your agents.
4+
* The Platform menu - Sim's modules. Five items in a three-column grid, so the
5+
* bottom-right cell is empty. Each description names the outcome the module
6+
* unlocks for your agents.
67
*/
78
export const PLATFORM_MENU: NavMenu = {
89
label: 'Platform',

apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/nav-menu-chip.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,9 @@ import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-m
3131
* `--surface-4` ring (`p-[3px]`, overlay shadow) wrapping an inner `--bg`
3232
* surface, with the item grid padded inside.
3333
*
34-
* The grid renders three visual columns on six tracks (each tile spans two),
35-
* which keeps six-item menus pixel-identical to a plain three-column grid while
36-
* letting a five-item menu center its two-tile last row - the second-to-last
37-
* tile starts on track two, so the short row sits symmetrically instead of
38-
* leaving a hole in the corner.
34+
* The grid is a plain three-column grid filled in reading order, so a menu with
35+
* a non-multiple-of-three item count leaves its gap in the bottom-right corner
36+
* rather than centering the short row.
3937
*/
4038

4139
interface NavMenuChipProps {
@@ -81,14 +79,7 @@ export function NavMenuChip({ menu }: NavMenuChipProps) {
8179
<div className={cn(PANEL_BASE, !closed && PANEL_REVEAL)}>
8280
<div className='w-[840px] rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-[var(--shadow-overlay)]'>
8381
<div className='rounded-lg border border-[var(--border-1)] bg-[var(--bg)] p-2'>
84-
<div
85-
className={cn(
86-
'grid grid-cols-6 gap-1 [&>*]:col-span-2',
87-
items.length % 3 === 2 && '[&>*:nth-last-child(2)]:col-start-2'
88-
)}
89-
role='group'
90-
aria-label={label}
91-
>
82+
<div className='grid grid-cols-3 gap-1' role='group' aria-label={label}>
9283
{items.map((item) => (
9384
<NavMenuItem key={item.title} item={item} onSelect={handleSelect} />
9485
))}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import fs from 'fs/promises'
5+
import os from 'os'
6+
import path from 'path'
7+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
8+
9+
/**
10+
* `sharp` resolves a platform-specific `@img/sharp-*` native binary that the
11+
* standalone file tracer cannot follow, so a deployment can ship without it. It
12+
* must therefore be loaded lazily and its failure contained: an unreadable OG
13+
* dimension is optional metadata, not a reason to take down `/blog`, `/library`,
14+
* and every tag, author, slug, and RSS route that reads the registry.
15+
*
16+
* This mock makes `import('sharp')` fail the way a missing native binary does.
17+
*/
18+
vi.mock('sharp', () => {
19+
throw new Error('Could not load the sharp module using the linux-x64 runtime')
20+
})
21+
22+
vi.mock('next-mdx-remote/rsc', () => ({
23+
compileMDX: vi.fn(async () => ({ content: null })),
24+
}))
25+
26+
vi.mock('@/lib/content/mdx', () => ({ mdxComponents: {} }))
27+
28+
import { createContentRegistry } from '@/lib/content/registry-factory'
29+
30+
let root: string
31+
let contentDir: string
32+
let authorsDir: string
33+
34+
const POST = `---
35+
slug: sharp-is-unavailable
36+
title: Sharp Is Unavailable
37+
description: The registry still serves posts when the native binary is missing.
38+
date: 2026-08-10
39+
authors: [waleed]
40+
ogImage: /blog/missing-og.png
41+
canonical: https://sim.ai/blog/sharp-is-unavailable
42+
---
43+
44+
Body copy.
45+
`
46+
47+
const AUTHOR = JSON.stringify({ id: 'waleed', name: 'Waleed Latif' })
48+
49+
beforeAll(async () => {
50+
root = await fs.mkdtemp(path.join(os.tmpdir(), 'content-registry-'))
51+
contentDir = path.join(root, 'content', 'blog')
52+
authorsDir = path.join(root, 'content', 'authors')
53+
await fs.mkdir(path.join(contentDir, 'sharp-is-unavailable'), { recursive: true })
54+
await fs.mkdir(authorsDir, { recursive: true })
55+
await fs.writeFile(path.join(contentDir, 'sharp-is-unavailable', 'index.mdx'), POST)
56+
await fs.writeFile(path.join(authorsDir, 'waleed.json'), AUTHOR)
57+
})
58+
59+
afterAll(async () => {
60+
await fs.rm(root, { recursive: true, force: true })
61+
})
62+
63+
describe('createContentRegistry without a loadable sharp', () => {
64+
it('still lists posts, omitting only the OG dimensions', async () => {
65+
const registry = createContentRegistry({ contentDir, authorsDir })
66+
67+
const posts = await registry.getAllPostMeta()
68+
69+
expect(posts).toHaveLength(1)
70+
expect(posts[0].slug).toBe('sharp-is-unavailable')
71+
expect(posts[0].ogImage).toBe('/blog/missing-og.png')
72+
expect(posts[0].ogImageWidth).toBeUndefined()
73+
expect(posts[0].ogImageHeight).toBeUndefined()
74+
})
75+
76+
it('still resolves a single post by slug', async () => {
77+
const registry = createContentRegistry({ contentDir, authorsDir })
78+
79+
const post = await registry.getPostBySlug('sharp-is-unavailable')
80+
81+
expect(post?.title).toBe('Sharp Is Unavailable')
82+
})
83+
})

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import fs from 'fs/promises'
22
import path from 'path'
33
import { cache } from 'react'
44
import { createLogger } from '@sim/logger'
5+
import { getErrorMessage } from '@sim/utils/errors'
56
import matter from 'gray-matter'
67
import { compileMDX } from 'next-mdx-remote/rsc'
78
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
89
import rehypeSlug from 'rehype-slug'
910
import remarkGfm from 'remark-gfm'
10-
import sharp from 'sharp'
1111
import { mdxComponents } from '@/lib/content/mdx'
1212
import type { Author, ContentMeta, ContentPost, TagWithCount } from '@/lib/content/schema'
1313
import { AuthorSchema, ContentFrontmatterSchema } from '@/lib/content/schema'
@@ -102,13 +102,21 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
102102
* Uses `sharp`, which only parses headers for `metadata()`. It replaced the
103103
* `image-size` package, archived upstream with unpatched DoS advisories in
104104
* its ICNS/JXL/HEIF parsers (GHSA-w3rx-r6r6-pgpr, GHSA-5p2g-fcmc-qvqq).
105+
*
106+
* `sharp` is loaded lazily, never as a top-level import. It resolves a
107+
* platform-specific `@img/sharp-*` native binary that the standalone file
108+
* tracer cannot follow, so a deployment that ships without it makes
109+
* `import 'sharp'` throw at module scope — which would take down every route
110+
* that touches this registry (`/blog`, `/library`, their tag, author, slug,
111+
* and RSS routes) rather than degrading one optional OG dimension.
105112
*/
106113
async function readOgImageDimensions(
107114
ogImage: string
108115
): Promise<{ width: number; height: number } | null> {
109116
if (ogImage.startsWith('http')) return null
110117
try {
111118
const buffer = await fs.readFile(path.join(process.cwd(), 'public', ogImage))
119+
const sharp = (await import('sharp')).default
112120
const { width, height } = await sharp(buffer).metadata()
113121
if (!width || !height) {
114122
logger.warn('OG image has no readable dimensions; falling back to the OG default', {
@@ -117,7 +125,11 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
117125
return null
118126
}
119127
return { width, height }
120-
} catch {
128+
} catch (error) {
129+
logger.warn('Failed to read OG image dimensions; falling back to the OG default', {
130+
ogImage,
131+
error: getErrorMessage(error),
132+
})
121133
return null
122134
}
123135
}

0 commit comments

Comments
 (0)