Files
2026-10-09 16:20:55 +03:00

80 lines
3.8 KiB
JavaScript

/* Cover generator — run: npm run covers
Produces 12 JPGs (1600x900) in public/covers/ plus per-category defaults. */
import sharp from 'sharp'
import { mkdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const dirname = path.dirname(fileURLToPath(import.meta.url))
const outDir = path.resolve(dirname, '../public/covers')
mkdirSync(outDir, { recursive: true })
const W = 1600
const H = 900
const POSTS = [
// [file, title, category, kind: free|vip]
['agents-free', 'بناء أول وكيل بالعربية', 'وكلاء'],
['agents-vip', 'حلقة الوكيل: تخطيط وتنفيذ وتحقق', 'وكلاء'],
['models-free', 'كيف تختار نموذجك', 'نماذج'],
['models-vip', 'تقييم النماذج منهجياً', 'نماذج'],
['retrieval-free', 'الاسترجاع بلا تعقيد', 'استرجاع'],
['retrieval-vip', 'قواعد بيانات المتجهات عملياً', 'استرجاع'],
['safety-free', 'حدود الوكيل الآمنة', 'سلامة'],
['safety-vip', 'مراجعة مخرجات الوكلاء', 'سلامة'],
['tools-free', 'أدوات لا غنى عنها', 'أدوات'],
['tools-vip', 'أنابيب الأدوات والصلاحيات', 'أدوات'],
['business-free', 'الذكاء الاصطناعي في عملك', 'أعمال'],
['business-vip', 'من نموذج أولي إلى إنتاج', 'أعمال'],
// fallback default cover for the agent API
['cover-default', 'أنور بي آي', 'وكلاء'],
// per-category default covers for the agent API
['cover-agents', 'وكلاء', 'وكلاء'],
['cover-models', 'نماذج', 'نماذج'],
['cover-retrieval', 'استرجاع', 'استرجاع'],
['cover-safety', 'سلامة', 'سلامة'],
['cover-tools', 'أدوات', 'أدوات'],
['cover-business', 'أعمال', 'أعمال'],
['cover-default', 'أنور بي آي', 'وكلاء'],
]
function svgCover(title, category) {
const titleEsc = title.replace(/&/g, '&amp;').replace(/</g, '&lt;')
const catEsc = category.replace(/&/g, '&amp;').replace(/</g, '&lt;')
return Buffer.from(`
<svg width="${W}" height="${H}" xmlns="http://www.w3.org/2000/svg" dir="rtl">
<defs>
<radialGradient id="g" cx="20%" cy="15%" r="90%">
<stop offset="0%" stop-color="#16202a"/>
<stop offset="55%" stop-color="#0c1016"/>
<stop offset="100%" stop-color="#050708"/>
</radialGradient>
<linearGradient id="beam" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#7fd8cc"/>
<stop offset="100%" stop-color="#3a6a8a"/>
</linearGradient>
</defs>
<rect width="${W}" height="${H}" fill="url(#g)"/>
<g opacity="0.5" stroke="#31414f" stroke-width="1">
${Array.from({ length: 9 }, (_, i) => `<line x1="0" y1="${(i + 1) * 90}" x2="${W}" y2="${(i + 1) * 90}"/>`).join('')}
</g>
<g transform="translate(${W - 140}, 110)">
<rect x="-26" y="-26" width="52" height="52" transform="rotate(45)" fill="none" stroke="url(#beam)" stroke-width="5"/>
<rect x="-9" y="-9" width="18" height="18" transform="rotate(45)" fill="#e8f6f3"/>
</g>
<text x="${W - 80}" y="480" text-anchor="end" fill="#f2f7f6"
font-family="SF Arabic, Noto Sans Arabic, sans-serif" font-weight="700" font-size="72">${titleEsc}</text>
<text x="${W - 80}" y="560" text-anchor="end" fill="#8fa3ad"
font-family="SF Mono, monospace" font-size="30" letter-spacing="4">${catEsc} · أنور بي آي</text>
<rect x="${W - 80 - 180}" y="620" width="180" height="3" fill="url(#beam)"/>
</svg>`)
}
for (const [name, title, category] of POSTS) {
await sharp(svgCover(title, category))
.jpeg({ quality: 82 })
.toFile(path.join(outDir, `${name}.jpg`))
console.log(`✓ ${name}.jpg`)
}
console.log(`Done: ${POSTS.length} covers → ${outDir}`)