fix(landing): load sharp lazily so a missing native binary can't 500 /blog and /library (#6496)

This commit is contained in:
Waleed
2026-08-10 11:02:20 -07:00
committed by GitHub
parent 5da48d015a
commit 52ee2a5c5c
4 changed files with 104 additions and 17 deletions
@@ -1,8 +1,9 @@
import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-menu-chip/types'
/**
* The Platform menu - Sim's modules. Six items in a three-column grid. Each
* description names the outcome the module unlocks for your agents.
* The Platform menu - Sim's modules. Five items in a three-column grid, so the
* bottom-right cell is empty. Each description names the outcome the module
* unlocks for your agents.
*/
export const PLATFORM_MENU: NavMenu = {
label: 'Platform',
@@ -31,11 +31,9 @@ import type { NavMenu } from '@/app/(landing)/components/navbar/components/nav-m
* `--surface-4` ring (`p-[3px]`, overlay shadow) wrapping an inner `--bg`
* surface, with the item grid padded inside.
*
* The grid renders three visual columns on six tracks (each tile spans two),
* which keeps six-item menus pixel-identical to a plain three-column grid while
* letting a five-item menu center its two-tile last row - the second-to-last
* tile starts on track two, so the short row sits symmetrically instead of
* leaving a hole in the corner.
* The grid is a plain three-column grid filled in reading order, so a menu with
* a non-multiple-of-three item count leaves its gap in the bottom-right corner
* rather than centering the short row.
*/
interface NavMenuChipProps {
@@ -81,14 +79,7 @@ export function NavMenuChip({ menu }: NavMenuChipProps) {
<div className={cn(PANEL_BASE, !closed && PANEL_REVEAL)}>
<div className='w-[840px] rounded-xl border border-[var(--border-muted)] bg-[var(--surface-4)] p-[3px] shadow-[var(--shadow-overlay)]'>
<div className='rounded-lg border border-[var(--border-1)] bg-[var(--bg)] p-2'>
<div
className={cn(
'grid grid-cols-6 gap-1 [&>*]:col-span-2',
items.length % 3 === 2 && '[&>*:nth-last-child(2)]:col-start-2'
)}
role='group'
aria-label={label}
>
<div className='grid grid-cols-3 gap-1' role='group' aria-label={label}>
{items.map((item) => (
<NavMenuItem key={item.title} item={item} onSelect={handleSelect} />
))}
@@ -0,0 +1,83 @@
/**
* @vitest-environment node
*/
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
/**
* `sharp` resolves a platform-specific `@img/sharp-*` native binary that the
* standalone file tracer cannot follow, so a deployment can ship without it. It
* must therefore be loaded lazily and its failure contained: an unreadable OG
* dimension is optional metadata, not a reason to take down `/blog`, `/library`,
* and every tag, author, slug, and RSS route that reads the registry.
*
* This mock makes `import('sharp')` fail the way a missing native binary does.
*/
vi.mock('sharp', () => {
throw new Error('Could not load the sharp module using the linux-x64 runtime')
})
vi.mock('next-mdx-remote/rsc', () => ({
compileMDX: vi.fn(async () => ({ content: null })),
}))
vi.mock('@/lib/content/mdx', () => ({ mdxComponents: {} }))
import { createContentRegistry } from '@/lib/content/registry-factory'
let root: string
let contentDir: string
let authorsDir: string
const POST = `---
slug: sharp-is-unavailable
title: Sharp Is Unavailable
description: The registry still serves posts when the native binary is missing.
date: 2026-08-10
authors: [waleed]
ogImage: /blog/missing-og.png
canonical: https://sim.ai/blog/sharp-is-unavailable
---
Body copy.
`
const AUTHOR = JSON.stringify({ id: 'waleed', name: 'Waleed Latif' })
beforeAll(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), 'content-registry-'))
contentDir = path.join(root, 'content', 'blog')
authorsDir = path.join(root, 'content', 'authors')
await fs.mkdir(path.join(contentDir, 'sharp-is-unavailable'), { recursive: true })
await fs.mkdir(authorsDir, { recursive: true })
await fs.writeFile(path.join(contentDir, 'sharp-is-unavailable', 'index.mdx'), POST)
await fs.writeFile(path.join(authorsDir, 'waleed.json'), AUTHOR)
})
afterAll(async () => {
await fs.rm(root, { recursive: true, force: true })
})
describe('createContentRegistry without a loadable sharp', () => {
it('still lists posts, omitting only the OG dimensions', async () => {
const registry = createContentRegistry({ contentDir, authorsDir })
const posts = await registry.getAllPostMeta()
expect(posts).toHaveLength(1)
expect(posts[0].slug).toBe('sharp-is-unavailable')
expect(posts[0].ogImage).toBe('/blog/missing-og.png')
expect(posts[0].ogImageWidth).toBeUndefined()
expect(posts[0].ogImageHeight).toBeUndefined()
})
it('still resolves a single post by slug', async () => {
const registry = createContentRegistry({ contentDir, authorsDir })
const post = await registry.getPostBySlug('sharp-is-unavailable')
expect(post?.title).toBe('Sharp Is Unavailable')
})
})
+14 -2
View File
@@ -2,12 +2,12 @@ import fs from 'fs/promises'
import path from 'path'
import { cache } from 'react'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import matter from 'gray-matter'
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'
@@ -102,6 +102,13 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
* 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).
*
* `sharp` is loaded lazily, never as a top-level import. It resolves a
* platform-specific `@img/sharp-*` native binary that the standalone file
* tracer cannot follow, so a deployment that ships without it makes
* `import 'sharp'` throw at module scope — which would take down every route
* that touches this registry (`/blog`, `/library`, their tag, author, slug,
* and RSS routes) rather than degrading one optional OG dimension.
*/
async function readOgImageDimensions(
ogImage: string
@@ -109,6 +116,7 @@ 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 sharp = (await import('sharp')).default
const { width, height } = await sharp(buffer).metadata()
if (!width || !height) {
logger.warn('OG image has no readable dimensions; falling back to the OG default', {
@@ -117,7 +125,11 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
return null
}
return { width, height }
} catch {
} catch (error) {
logger.warn('Failed to read OG image dimensions; falling back to the OG default', {
ogImage,
error: getErrorMessage(error),
})
return null
}
}