@nanggo/social-preview
Version:
Generate beautiful social media preview images from any URL
262 lines (259 loc) • 10.4 kB
JavaScript
;
/**
* 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* 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
};
}