*]:col-span-2',
- items.length % 3 === 2 && '[&>*:nth-last-child(2)]:col-start-2'
- )}
- role='group'
- aria-label={label}
- >
+
{items.map((item) => (
))}
diff --git a/apps/sim/lib/content/registry-factory.test.ts b/apps/sim/lib/content/registry-factory.test.ts
new file mode 100644
index 00000000000..4a160728929
--- /dev/null
+++ b/apps/sim/lib/content/registry-factory.test.ts
@@ -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('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/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')
+ })
+})
diff --git a/apps/sim/lib/content/registry-factory.ts b/apps/sim/lib/content/registry-factory.ts
index 8df6fceba6f..a526cfd8b6c 100644
--- a/apps/sim/lib/content/registry-factory.ts
+++ b/apps/sim/lib/content/registry-factory.ts
@@ -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
}
}