UNPKG

@nanggo/social-preview

Version:

Generate beautiful social media preview images from any URL

262 lines (259 loc) 10.4 kB
"use strict"; /** * Social Preview Generator * Generate beautiful social media preview images from any URL */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PreviewGeneratorError = exports.ErrorType = void 0; exports.generatePreview = generatePreview; exports.generatePreviewWithDetails = generatePreviewWithDetails; const types_1 = require("./types"); Object.defineProperty(exports, "ErrorType", { enumerable: true, get: function () { return types_1.ErrorType; } }); Object.defineProperty(exports, "PreviewGeneratorError", { enumerable: true, get: function () { return types_1.PreviewGeneratorError; } }); const metadata_extractor_1 = require("./core/metadata-extractor"); const image_generator_1 = require("./core/image-generator"); const modern_1 = require("./templates/modern"); const sharp_1 = __importDefault(require("sharp")); /** * Template registry */ const templates = { modern: modern_1.modernTemplate, // TODO: Add classic and minimal templates }; /** * Generate a social preview image from a URL * @param url - The URL to generate preview for * @param options - Configuration options * @returns Buffer containing the generated image */ async function generatePreview(url, options = {}) { try { // Set default options const finalOptions = { template: 'modern', width: image_generator_1.DEFAULT_DIMENSIONS.width, height: image_generator_1.DEFAULT_DIMENSIONS.height, quality: 90, cache: true, ...options, }; // Extract metadata from URL let metadata; try { metadata = await (0, metadata_extractor_1.extractMetadata)(url); // Validate metadata if (!(0, metadata_extractor_1.validateMetadata)(metadata)) { // Apply fallbacks if metadata is incomplete metadata = (0, metadata_extractor_1.applyFallbacks)(metadata, url); } } catch (error) { // If metadata extraction fails completely, use fallback if (finalOptions.fallback?.strategy === 'generate' || finalOptions.fallback?.strategy === 'auto') { return await (0, image_generator_1.createFallbackImage)(url, finalOptions); } throw error; } // Get template configuration const templateName = finalOptions.template || 'modern'; const template = templates[templateName]; if (!template && templateName !== 'custom') { throw new types_1.PreviewGeneratorError(types_1.ErrorType.TEMPLATE_ERROR, `Template "${templateName}" not found`); } // Generate image based on template const imageBuffer = await generateImageWithTemplate(metadata, template || modern_1.modernTemplate, finalOptions); return imageBuffer; } catch (error) { if (error instanceof types_1.PreviewGeneratorError) { throw error; } throw new types_1.PreviewGeneratorError(types_1.ErrorType.IMAGE_ERROR, `Failed to generate preview for ${url}: ${error instanceof Error ? error.message : String(error)}`, error); } } /** * Generate image with specific template */ async function generateImageWithTemplate(metadata, template, options) { const width = options.width || image_generator_1.DEFAULT_DIMENSIONS.width; const height = options.height || image_generator_1.DEFAULT_DIMENSIONS.height; const quality = options.quality || 90; try { // Create base image let baseImage; if (metadata.image) { // Use existing image as background const { fetchImage } = await Promise.resolve().then(() => __importStar(require('./core/metadata-extractor'))); try { const imageBuffer = await fetchImage(metadata.image); baseImage = (0, sharp_1.default)(imageBuffer) .resize(width, height, { fit: 'cover', position: 'center', }) .blur(3) .modulate({ brightness: 0.7, }); } catch (error) { // If image fetch fails, create blank canvas baseImage = await createBlankCanvas(width, height, options); } } else { // Create blank canvas with gradient baseImage = await createBlankCanvas(width, height, options); } // Generate overlay based on template let overlayBuffer; if (template.name === 'modern') { const overlaySvg = (0, modern_1.generateModernOverlay)(metadata, width, height, options); overlayBuffer = Buffer.from(overlaySvg); } else { // Default overlay generation overlayBuffer = await generateDefaultOverlay(metadata, template, width, height, options); } // Composite overlay on base image const finalImage = await baseImage .composite([{ input: overlayBuffer, top: 0, left: 0, }]) .jpeg({ quality }) .toBuffer(); return finalImage; } catch (error) { throw new types_1.PreviewGeneratorError(types_1.ErrorType.IMAGE_ERROR, `Failed to generate image with template: ${error instanceof Error ? error.message : String(error)}`, error); } } /** * Create blank canvas with gradient background */ async function createBlankCanvas(width, height, options) { const backgroundColor = options.colors?.background || '#1a1a2e'; const accentColor = options.colors?.accent || '#16213e'; const gradientSvg = ` <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"> <defs> <linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" style="stop-color:${backgroundColor};stop-opacity:1" /> <stop offset="100%" style="stop-color:${accentColor};stop-opacity:1" /> </linearGradient> <pattern id="pattern" x="0" y="0" width="100" height="100" patternUnits="userSpaceOnUse"> <circle cx="2" cy="2" r="1" fill="white" opacity="0.05"/> </pattern> </defs> <rect width="${width}" height="${height}" fill="url(#bgGradient)"/> <rect width="${width}" height="${height}" fill="url(#pattern)"/> </svg> `; return (0, sharp_1.default)(Buffer.from(gradientSvg)); } /** * Generate default overlay for non-modern templates */ async function generateDefaultOverlay(metadata, template, width, height, options) { const padding = template.layout.padding || 60; const textColor = options.colors?.text || '#ffffff'; // Simple default overlay const overlaySvg = ` <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg"> <defs> <style> .title { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: ${template.typography.title.fontSize}px; font-weight: ${template.typography.title.fontWeight || '700'}; fill: ${textColor}; } .description { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: ${template.typography.description?.fontSize || 24}px; font-weight: ${template.typography.description?.fontWeight || '400'}; fill: ${textColor}; opacity: 0.9; } </style> </defs> <text x="${padding}" y="${height / 2}" class="title"> ${escapeXml(metadata.title)} </text> ${metadata.description ? ` <text x="${padding}" y="${height / 2 + 40}" class="description"> ${escapeXml(metadata.description.substring(0, 100))} </text> ` : ''} </svg> `; return Buffer.from(overlaySvg); } /** * Escape XML special characters */ function escapeXml(text) { return text .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&apos;'); } /** * Generate preview with full result details */ async function generatePreviewWithDetails(url, options = {}) { const buffer = await generatePreview(url, options); const metadata = await (0, metadata_extractor_1.extractMetadata)(url); return { buffer, format: 'jpeg', dimensions: { width: options.width || image_generator_1.DEFAULT_DIMENSIONS.width, height: options.height || image_generator_1.DEFAULT_DIMENSIONS.height, }, metadata, template: options.template || 'modern', cached: false, // TODO: Implement caching }; }