@nanggo/social-preview
Version:
Generate beautiful social media preview images from any URL
290 lines (277 loc) • 10 kB
JavaScript
"use strict";
/**
* Modern Template
* A contemporary design with gradient overlays and clean typography
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.modernTemplate = void 0;
exports.generateModernOverlay = generateModernOverlay;
/**
* Modern template configuration
*/
exports.modernTemplate = {
name: 'modern',
layout: {
padding: 80,
titlePosition: 'center',
descriptionPosition: 'below-title',
imagePosition: 'background',
logoPosition: 'bottom-left',
},
typography: {
title: {
fontSize: 56,
fontWeight: '700',
lineHeight: 1.2,
maxLines: 2,
},
description: {
fontSize: 28,
fontWeight: '400',
lineHeight: 1.4,
maxLines: 2,
},
siteName: {
fontSize: 22,
fontWeight: '600',
},
},
effects: {
gradient: {
type: 'linear',
colors: ['rgba(0,0,0,0.7)', 'rgba(0,0,0,0.3)'],
direction: '180deg',
opacity: 1,
},
blur: {
radius: 3,
areas: 'background',
},
shadow: {
text: true,
box: false,
},
borderRadius: 0,
},
};
/**
* Generate modern template SVG overlay
*/
function generateModernOverlay(metadata, width, height, options = {}) {
const padding = exports.modernTemplate.layout.padding;
const textColor = options.colors?.text || '#ffffff';
const accentColor = options.colors?.accent || '#4a9eff';
// Typography settings
const titleFontSize = exports.modernTemplate.typography.title.fontSize;
const titleLineHeight = exports.modernTemplate.typography.title.lineHeight || 1.2;
const descFontSize = exports.modernTemplate.typography.description?.fontSize || 28;
const descLineHeight = exports.modernTemplate.typography.description?.lineHeight || 1.4;
const siteNameFontSize = exports.modernTemplate.typography.siteName?.fontSize || 22;
// Calculate text wrapping
const maxTitleWidth = width - (padding * 2);
const titleLines = wrapText(metadata.title, maxTitleWidth, titleFontSize, exports.modernTemplate.typography.title.maxLines || 2);
const descLines = metadata.description
? wrapText(metadata.description, maxTitleWidth, descFontSize, exports.modernTemplate.typography.description?.maxLines || 2)
: [];
// Calculate vertical centering
const totalContentHeight = (titleLines.length * titleFontSize * titleLineHeight) +
(descLines.length > 0 ? 30 : 0) + // Gap between title and description
(descLines.length * descFontSize * descLineHeight);
const contentStartY = (height - totalContentHeight) / 2;
const titleY = contentStartY + titleFontSize;
const descY = titleY + (titleLines.length * titleFontSize * titleLineHeight) + 30;
return `
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css">
<![CDATA[
.title {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: ${titleFontSize}px;
font-weight: 800;
fill: ${textColor};
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.4));
letter-spacing: -0.02em;
}
.description {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: ${descFontSize}px;
font-weight: 400;
fill: ${textColor};
opacity: 0.95;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
letter-spacing: -0.01em;
}
.siteName {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: ${siteNameFontSize}px;
font-weight: 600;
fill: ${textColor};
opacity: 0.8;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.domain {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 18px;
font-weight: 500;
fill: ${textColor};
opacity: 0.6;
}
]]>
</style>
<!-- Gradient overlays -->
<linearGradient id="bgOverlay" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:rgba(0,0,0,0.7);stop-opacity:1" />
<stop offset="50%" style="stop-color:rgba(0,0,0,0.5);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgba(0,0,0,0.3);stop-opacity:1" />
</linearGradient>
<linearGradient id="accentGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:${accentColor};stop-opacity:1" />
<stop offset="100%" style="stop-color:${adjustBrightness(accentColor, 20)};stop-opacity:1" />
</linearGradient>
<!-- Blur filter for background -->
<filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
</defs>
<!-- Background overlay -->
${metadata.image ? `
<rect width="${width}" height="${height}" fill="url(#bgOverlay)"/>
` : ''}
<!-- Top accent bar -->
<rect x="0" y="0" width="${width}" height="4" fill="url(#accentGradient)"/>
<!-- Content container with subtle background -->
<rect
x="${padding - 20}"
y="${contentStartY - 20}"
width="${width - (padding * 2) + 40}"
height="${totalContentHeight + 40}"
fill="rgba(0,0,0,0.2)"
rx="8"
filter="url(#blur)"
/>
<!-- Title -->
${titleLines.map((line, index) => `
<text
x="${width / 2}"
y="${titleY + (index * titleFontSize * titleLineHeight)}"
class="title"
text-anchor="middle"
>
${escapeXml(line)}
</text>
`).join('')}
<!-- Description -->
${descLines.length > 0 ? descLines.map((line, index) => `
<text
x="${width / 2}"
y="${descY + (index * descFontSize * descLineHeight)}"
class="description"
text-anchor="middle"
>
${escapeXml(line)}
</text>
`).join('') : ''}
<!-- Bottom section -->
<g transform="translate(${padding}, ${height - padding})">
<!-- Site name -->
${metadata.siteName ? `
<text x="0" y="-30" class="siteName">
${escapeXml(metadata.siteName.toUpperCase())}
</text>
` : ''}
<!-- Domain -->
${metadata.domain ? `
<text x="0" y="-8" class="domain">
${escapeXml(metadata.domain)}
</text>
` : ''}
<!-- Decorative accent line -->
<rect x="0" y="-50" width="60" height="3" fill="${accentColor}" rx="1.5"/>
</g>
<!-- Right corner accent -->
<g transform="translate(${width - padding}, ${padding})">
<circle cx="0" cy="0" r="3" fill="${accentColor}" opacity="0.8"/>
<circle cx="12" cy="0" r="3" fill="${accentColor}" opacity="0.6"/>
<circle cx="24" cy="0" r="3" fill="${accentColor}" opacity="0.4"/>
</g>
</svg>
`;
}
/**
* Wrap text to fit within maximum width
*/
function wrapText(text, maxWidth, fontSize, maxLines) {
// Approximate character width (adjusted for Inter font)
const avgCharWidth = fontSize * 0.55;
const maxCharsPerLine = Math.floor(maxWidth / avgCharWidth);
const words = text.split(' ');
const lines = [];
let currentLine = '';
for (let i = 0; i < words.length; i++) {
const word = words[i];
const testLine = currentLine ? `${currentLine} ${word}` : word;
if (testLine.length <= maxCharsPerLine) {
currentLine = testLine;
}
else {
if (currentLine) {
lines.push(currentLine);
currentLine = word;
}
else {
// Word is too long, truncate it
lines.push(word.substring(0, maxCharsPerLine - 3) + '...');
currentLine = '';
}
}
// Check if we've reached max lines
if (lines.length >= maxLines - 1 && currentLine) {
const remainingWords = words.slice(i + 1);
if (remainingWords.length > 0) {
// Add ellipsis if there's more text
const truncatedLine = currentLine.substring(0, maxCharsPerLine - 3) + '...';
lines.push(truncatedLine);
}
else {
lines.push(currentLine);
}
break;
}
}
if (currentLine && lines.length < maxLines) {
lines.push(currentLine);
}
return lines;
}
/**
* Escape XML special characters
*/
function escapeXml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
/**
* Adjust color brightness
*/
function adjustBrightness(color, percent) {
// Simple brightness adjustment for hex colors
if (color.startsWith('#')) {
const num = parseInt(color.replace('#', ''), 16);
const amt = Math.round(2.55 * percent);
const R = (num >> 16) + amt;
const G = (num >> 8 & 0x00FF) + amt;
const B = (num & 0x0000FF) + amt;
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
(B < 255 ? B < 1 ? 0 : B : 255))
.toString(16)
.slice(1);
}
return color;
}