Initial release v1.0.0
Open-source marketplace for AI Agent skills. Features: - Next.js 15 web app with i18n (en/fa) - CLI tool for skill installation (npx skillhub) - GitHub crawler/indexer with multi-strategy discovery - Security scanning for all indexed skills - Self-hostable with Docker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
280
scripts/generate-marketing-assets.mjs
Normal file
280
scripts/generate-marketing-assets.mjs
Normal file
@@ -0,0 +1,280 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { writeFileSync, mkdirSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_APP_URL || 'https://skills.palebluedot.live';
|
||||
const SCREENSHOTS_DIR = './screenshots';
|
||||
const OG_DIR = './apps/web/public/og';
|
||||
|
||||
// Read logo SVG and encode for inline use
|
||||
const logoSvg = readFileSync('./apps/web/public/logo.svg', 'utf-8');
|
||||
const logoBase64 = Buffer.from(logoSvg).toString('base64');
|
||||
|
||||
// Screenshots to capture (English locale, scroll past header)
|
||||
const SCREENSHOTS = [
|
||||
{ name: '01-homepage', url: '/en', scrollY: 0, description: 'Homepage with hero and featured skills' },
|
||||
{ name: '02-browse', url: '/en/browse', scrollY: 80, description: 'Browse page with filters and skill cards' },
|
||||
{ name: '03-skill-detail', url: '/en/skill/anthropics/skills/pdf', scrollY: 60, description: 'Skill detail page' },
|
||||
{ name: '04-categories', url: '/en/categories', scrollY: 80, description: 'Categories page' },
|
||||
{ name: '05-search-results', url: '/en/browse?q=python', scrollY: 80, description: 'Search results' },
|
||||
];
|
||||
|
||||
async function createOGImage(page) {
|
||||
console.log('Creating OG image (1200x630)...');
|
||||
|
||||
// Create a branded OG image with logo and evergreen stats
|
||||
const ogHTML = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 1200px;
|
||||
height: 630px;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
font-family: 'Inter', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
width: 700px;
|
||||
height: 700px;
|
||||
background: radial-gradient(circle, rgba(102, 179, 230, 0.12) 0%, transparent 70%);
|
||||
top: -150px;
|
||||
right: -150px;
|
||||
}
|
||||
|
||||
.glow-2 {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: radial-gradient(circle, rgba(247, 193, 80, 0.08) 0%, transparent 70%);
|
||||
bottom: -100px;
|
||||
left: -100px;
|
||||
}
|
||||
|
||||
.content {
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 40px 60px;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 64px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #66b3e6 0%, #38bdf8 50%, #f7c150 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 28px;
|
||||
color: #e2e8f0;
|
||||
font-weight: 500;
|
||||
margin-bottom: 50px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.tagline span {
|
||||
color: #66b3e6;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 80px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
color: #f7c150;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 16px;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.platforms {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.platform {
|
||||
background: rgba(102, 179, 230, 0.15);
|
||||
border: 1px solid rgba(102, 179, 230, 0.3);
|
||||
border-radius: 20px;
|
||||
padding: 8px 20px;
|
||||
color: #7dd3fc;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
font-size: 18px;
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer .badge {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: white;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="glow"></div>
|
||||
<div class="glow-2"></div>
|
||||
<div class="content">
|
||||
<div class="logo-container">
|
||||
<img class="logo-icon" src="data:image/svg+xml;base64,${logoBase64}" alt="SkillHub Logo" />
|
||||
<div class="logo-text">SkillHub</div>
|
||||
</div>
|
||||
<div class="tagline">Open-Source Marketplace for <span>AI Agent Skills</span></div>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value">170K+</div>
|
||||
<div class="stat-label">Skills</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">4K+</div>
|
||||
<div class="stat-label">Contributors</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value">30</div>
|
||||
<div class="stat-label">Categories</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="platforms">
|
||||
<span class="platform">Claude Code</span>
|
||||
<span class="platform">Codex CLI</span>
|
||||
<span class="platform">GitHub Copilot</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
${new URL(BASE_URL).hostname}
|
||||
<span class="badge">MIT License</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
await page.setContent(ogHTML);
|
||||
await page.setViewportSize({ width: 1200, height: 630 });
|
||||
await page.screenshot({
|
||||
path: join(OG_DIR, 'og-default.png'),
|
||||
type: 'png'
|
||||
});
|
||||
console.log(' Saved: og-default.png (1200x630)');
|
||||
}
|
||||
|
||||
async function captureScreenshots(page) {
|
||||
console.log('\nCapturing marketing screenshots (English)...');
|
||||
|
||||
for (const shot of SCREENSHOTS) {
|
||||
console.log(' Capturing: ' + shot.name + ' - ' + shot.description);
|
||||
|
||||
try {
|
||||
await page.goto(BASE_URL + shot.url, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
// Wait for content to load
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Scroll past header if specified
|
||||
if (shot.scrollY > 0) {
|
||||
await page.evaluate((scrollY) => {
|
||||
window.scrollTo(0, scrollY);
|
||||
}, shot.scrollY);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
await page.screenshot({
|
||||
path: join(SCREENSHOTS_DIR, shot.name + '.png'),
|
||||
fullPage: false,
|
||||
});
|
||||
|
||||
console.log(' Saved: ' + shot.name + '.png');
|
||||
} catch (error) {
|
||||
console.error(' Error capturing ' + shot.name + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('=== SkillHub Marketing Assets Generator ===\n');
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
// Create context with HiDPI settings for screenshots
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2, // Retina quality
|
||||
locale: 'en-US', // Force English
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
// Create OG Image first
|
||||
await createOGImage(page);
|
||||
|
||||
// Capture screenshots from live site (English)
|
||||
await captureScreenshots(page);
|
||||
|
||||
await browser.close();
|
||||
|
||||
console.log('\n=== Done! ===');
|
||||
console.log('\nOG Image: ' + OG_DIR + '/og-default.png');
|
||||
console.log('Screenshots: ' + SCREENSHOTS_DIR + '/');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user