import { parseFrontmatter, slugToTitle } from '@mintlify/common'; import { renderOgCardPng } from '@mintlify/og'; import { applyCardThemeOverride, applyThumbnailOverride, buildOgCardInput, checkOgCard, createLocalAssetLoaders, deriveOgThemeParams, getOgPageParams, MAX_OG_ASSET_BYTES, toOgFontAsset, toOgImageAsset, type OgAssetLoaders, } from '@mintlify/og/config'; import { getConfigObj } from '@mintlify/prebuild'; import { addLog, ErrorLog, SuccessLog, WarningLog } from '@mintlify/previewing'; import { validateDocsConfig, type ThumbnailOverride } from '@mintlify/validation'; import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import open from 'open'; import { CMD_EXEC_PATH } from './helpers.js'; export type OgCommandOptions = Pick & { page?: string; output?: string; open: boolean; }; const REMOTE_TIMEOUT_MS = 5_000; const PAGE_EXTENSIONS = ['.mdx', '.md']; const MAX_REPORTED_ISSUES = 5; const readLimited = async (response: Response): Promise => { const declared = Number(response.headers.get('content-length')); if (declared > MAX_OG_ASSET_BYTES || !response.body) return null; const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let size = 0; for (let result = await reader.read(); !result.done; result = await reader.read()) { size += result.value.byteLength; if (size > MAX_OG_ASSET_BYTES) { await reader.cancel(); return null; } chunks.push(result.value); } const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; } return bytes; }; const fetchAsset = async (src: string) => { try { const response = await fetch(src, { signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS) }); if (!response.ok) return null; const bytes = await readLimited(response); return bytes && { bytes, contentType: response.headers.get('content-type') ?? undefined }; } catch { return null; } }; const remoteLoaders: OgAssetLoaders = { image: async (src) => { const asset = await fetchAsset(src); return asset && toOgImageAsset(asset.bytes, asset.contentType); }, font: async (src) => { const asset = await fetchAsset(src); return asset && toOgFontAsset(asset.bytes); }, }; const normalizePagePath = (page: string | undefined) => path.posix .normalize(`/${(page ?? '').replace(/\\/g, '/')}`) .replace(/^\/+|\/+$/g, '') .replace(/\.mdx?$/, '') || 'index'; const pageHref = (pagePath: string) => pagePath.replace(/(^|\/)index$/, ''); const pageCandidates = (pagePath: string) => [pagePath, `${pagePath}/index`].flatMap((base) => PAGE_EXTENSIONS.map((extension) => `${base}${extension}`) ); const readPage = async (pagePath: string) => { const root = path.resolve(CMD_EXEC_PATH); for (const candidate of pageCandidates(pagePath)) { const filePath = path.resolve(root, candidate); if (!filePath.startsWith(`${root}${path.sep}`)) return null; try { return await readFile(filePath, 'utf-8'); } catch { continue; } } return null; }; const stringAttribute = (attributes: Record, key: string) => { const value = attributes[key]; return typeof value === 'string' && value ? value : undefined; }; const defaultOutputPath = async (pagePath: string) => path.join( await mkdtemp(path.join(os.tmpdir(), 'mintlify-og-')), `${pagePath.replace(/\//g, '-')}.png` ); export async function ogCommand(options: OgCommandOptions): Promise<0 | 1> { const rawConfig = await getConfigObj(CMD_EXEC_PATH, 'docs'); if (!rawConfig) { addLog(); return 1; } const validation = validateDocsConfig(rawConfig); if (!validation.success) { const issues = validation.error.issues .slice(0, MAX_REPORTED_ISSUES) .map((issue) => `${issue.path.join('.') || 'docs.json'}: ${issue.message}`); addLog(); return 1; } const docsConfig = validation.data; const pagePath = normalizePagePath(options.page); const contents = await readPage(pagePath); if (contents === null) { addLog( ); return 1; } const { attributes } = parseFrontmatter>(contents); const pageParams = getOgPageParams( { path: pagePath, title: stringAttribute(attributes, 'title') ?? stringAttribute(attributes, 'sidebarTitle') ?? slugToTitle(pageHref(pagePath)), description: stringAttribute(attributes, 'description'), thumbnail: attributes.thumbnail, }, docsConfig ); const themeParams = applyThumbnailOverride( applyCardThemeOverride(deriveOgThemeParams(docsConfig), pageParams.thumbnail), options ); const input = await buildOgCardInput( pageParams, themeParams, createLocalAssetLoaders(CMD_EXEC_PATH, remoteLoaders) ); const png = await renderOgCardPng(input); const outputPath = options.output ? path.resolve(options.output) : await defaultOutputPath(pagePath); await mkdir(path.dirname(outputPath), { recursive: true }); await writeFile(outputPath, new Uint8Array(png)); for (const warning of checkOgCard(input, themeParams)) { addLog(); } addLog(); if (options.open && !process.env.CI) await open(outputPath); return 0; }