Created
July 13, 2025 01:04
-
-
Save Saad5400/a8ce37f0c294906ca7cd421ec4798471 to your computer and use it in GitHub Desktop.
Nuxt Serverless function to generate OG Image using puppeteer (Supports Arabic and RTL)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import chromium from "@sparticuz/chromium-min"; | |
import puppeteerCore, { Browser, Page } from "puppeteer-core"; | |
// ----------------------------------------------------------------------------- | |
// CONFIGURATION & CONSTANTS | |
// ----------------------------------------------------------------------------- | |
// URL to download your Chromium build; swap this for your own custom build or version | |
// Tip: You can host your own build on S3 or another CDN for faster startup | |
const remoteExecutablePath = | |
"https://github.com/Sparticuz/chromium/releases/download/v138.0.1/chromium-v138.0.1-pack.x64.tar"; | |
// Cache duration in seconds (here, 8 hours) | |
// Tip: Adjust based on how dynamic your underlying pages are; shorter for frequently changing content | |
const cache = 60 * 60 * 8; | |
// Singletons for browser and page to improve performance | |
let browser: Browser | null = null; | |
let page: Page | null = null; | |
// Default screenshot dimensions (1.91:1 aspect ratio) | |
// Tip: Change these or accept additional query params for custom aspect ratios | |
const DEFAULT_WIDTH = 720; | |
const DEFAULT_HEIGHT = 377; | |
// ----------------------------------------------------------------------------- | |
// MAIN SCREENSHOT HANDLER | |
// ----------------------------------------------------------------------------- | |
/** | |
* Handles incoming requests and returns a PNG screenshot of the target URL. | |
* | |
* @param event - Request event object (contains query params, headers, etc.) | |
* @returns Response containing the PNG buffer and HTTP headers | |
*/ | |
async function screenshotHandler(event: any) { | |
// ------------------------------------------------------------------------- | |
// PARSE & VALIDATE INPUT | |
// ------------------------------------------------------------------------- | |
// Extract `path`, `width`, and `height` from query parameters | |
// Tip: Extend getQuery to support other params like `format=jpeg` or `delay` | |
const { path, width: wQ, height: hQ } = getQuery(event); | |
if (!path) { | |
// Return a 400 error if `path` is missing | |
throw createError({ statusCode: 400, statusMessage: "path parameter is required" }); | |
} | |
// Coerce width/height or fallback to defaults | |
const width = wQ ? parseInt(wQ as string, 10) : DEFAULT_WIDTH; | |
const height = hQ ? parseInt(hQ as string, 10) : DEFAULT_HEIGHT; | |
// ------------------------------------------------------------------------- | |
// LAUNCH BROWSER & SETUP PAGE | |
// ------------------------------------------------------------------------- | |
// Lazily launch and cache the browser instance | |
if (!browser) { | |
browser = await puppeteerCore.launch({ | |
args: chromium.args, | |
// In DEV, use system-installed Chromium; otherwise download remote build | |
executablePath: process.env.DEV | |
? "/usr/bin/chromium" | |
: await chromium.executablePath(remoteExecutablePath), | |
headless: true, // Run in headless mode | |
}); | |
} | |
// Lazily instantiate a new page for speed | |
if (!page) { | |
page = await browser.newPage(); | |
} | |
// ------------------------------------------------------------------------- | |
// NAVIGATE & RENDER | |
// ------------------------------------------------------------------------- | |
// Construct the full URL; replace the base URL to match your own domain | |
// Tip: You can derive `baseUrl` dynamically from headers or config | |
const url = `https://uqucc.sb.sa${path}`; | |
await page.goto(url); | |
// Set viewport with high DPI for crisp images | |
await page.setViewport({ width, height, deviceScaleFactor: 2 }); | |
// Optional page tweaks: hide scrollbars or remove headers | |
await page.evaluate(() => { | |
// Ensure no layout shift due to scrollbars | |
document.documentElement.style.scrollbarGutter = "auto"; | |
// Remove the <header> element if present to focus on content | |
document.querySelector("header")?.remove(); | |
}); | |
// Capture screenshot as Buffer; can pass options to clip or change format | |
// Tip: Use `await page.screenshot({ clip: {...} })` to capture a specific region | |
const buffer = await page.screenshot(); | |
// ------------------------------------------------------------------------- | |
// PREPARE RESPONSE | |
// ------------------------------------------------------------------------- | |
// Set response headers; include caching in prod for CDN efficiency | |
const headers: Record<string, string> = { "Content-Type": "image/png" }; | |
if (!process.env.DEV) { | |
headers["Cache-Control"] = `public, max-age=${cache}`; | |
} | |
return new Response(buffer, { headers }); | |
} | |
// ----------------------------------------------------------------------------- | |
// EXPORT HANDLER | |
// ----------------------------------------------------------------------------- | |
// In DEV mode, return raw handler for local feedback; in prod, use cached wrapper | |
// Tip: `defineCachedEventHandler` is Nitro-specific—swap out for your own framework if needed | |
export default process.env.DEV | |
? defineEventHandler(screenshotHandler) | |
: defineCachedEventHandler(screenshotHandler, { maxAge: cache }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment