feat(categories): replace procedural SVG art with Gemini-generated WebP editorial banners
Deploy / build (push) Successful in 23s
Deploy / build (push) Successful in 23s
This commit is contained in:
1 parent
fa156b0b9d
commit
0a4fc47fce
39 files changed
+1221
-179
No files matched your search
@@ -1,21 +1,34 @@
|
||||
---
|
||||
/**
|
||||
* CategoryArt — terminal-style SVG illustration per category.
|
||||
* Matches the banner-gen visual language (dark #0d1220 canvas, terminal
|
||||
* window with traffic-light dots, JetBrains Mono, status colors) but with a
|
||||
* per-category accent color, so each category is scannable at a glance.
|
||||
* Inline SVG inherits the blog's self-hosted JetBrains Mono for text lines.
|
||||
* CategoryArt — Editorial banner image per category.
|
||||
* High-end technical editorial visuals generated via Google Gemini Imagen,
|
||||
* saved in WebP/JPG format with dark technical aesthetics.
|
||||
* Falls back to terminal-style SVG if image is unavailable.
|
||||
*/
|
||||
import { categoryMeta } from '../lib/categories';
|
||||
|
||||
interface Props {
|
||||
slug: string;
|
||||
loading?: 'lazy' | 'eager';
|
||||
}
|
||||
|
||||
const { slug } = Astro.props;
|
||||
const { slug, loading = 'lazy' } = Astro.props;
|
||||
const meta = categoryMeta(slug);
|
||||
const alt = meta ? `${meta.name.en} category banner` : 'Category illustration';
|
||||
|
||||
// line types: prompt (accent), cmd #e6edf3, ok #3fb950, hl #f5a524, dim #6b7c99
|
||||
const IMAGE_CATEGORIES = new Set([
|
||||
'ai',
|
||||
'case-studies',
|
||||
'devops',
|
||||
'engineering',
|
||||
'notes',
|
||||
'tutorials',
|
||||
'web3',
|
||||
]);
|
||||
|
||||
const hasImage = IMAGE_CATEGORIES.has(slug);
|
||||
|
||||
// line types for fallback: prompt (accent), cmd #e6edf3, ok #3fb950, hl #f5a524, dim #6b7c99
|
||||
type Line = { t: 'prompt' | 'cmd' | 'ok' | 'hl' | 'dim'; c: string };
|
||||
|
||||
const ART: Record<string, { title: string; lines: Line[]; glyph: 'text' | 'check'; gc: string }> = {
|
||||
@@ -126,56 +139,70 @@ const LINE_COLORS: Record<Line['t'], string> = {
|
||||
};
|
||||
---
|
||||
|
||||
<svg
|
||||
viewBox="0 0 640 320"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label={meta ? `${meta.name.en} category` : 'Category illustration'}
|
||||
focusable="false"
|
||||
>
|
||||
<rect width="640" height="320" fill="#0d1220" />
|
||||
<circle cx="600" cy="-20" r="150" fill={accent} opacity="0.12" />
|
||||
<circle cx="10" cy="330" r="130" fill="#0e94ff" opacity="0.08" />
|
||||
|
||||
<!-- terminal window -->
|
||||
<rect x="28" y="24" width="584" height="268" rx="12" fill="#161e2e" stroke="#2a3550" stroke-width="1" />
|
||||
<!-- titlebar -->
|
||||
<path
|
||||
d="M 28 36 a 12 12 0 0 1 12 -12 h 560 a 12 12 0 0 1 12 12 v 32 h -584 z"
|
||||
fill="#131a2a"
|
||||
/>
|
||||
<circle cx="52" cy="46" r="6" fill="#ff5c5c" />
|
||||
<circle cx="74" cy="46" r="6" fill="#f5a524" />
|
||||
<circle cx="96" cy="46" r="6" fill="#3fb950" />
|
||||
<text x="118" y="52" fill="#6b7c99" font-family="'JetBrains Mono', ui-monospace, monospace" font-size="15">{art.title}</text>
|
||||
|
||||
<!-- body lines -->
|
||||
{art.lines.map((ln, i) => (
|
||||
<text
|
||||
x="52"
|
||||
y={112 + i * 36}
|
||||
fill={LINE_COLORS[ln.t]}
|
||||
font-family="'JetBrains Mono', ui-monospace, monospace"
|
||||
font-size="19"
|
||||
>{ln.c}</text>
|
||||
))}
|
||||
|
||||
<!-- watermark glyph -->
|
||||
{art.glyph === 'text' ? (
|
||||
<text
|
||||
x="588"
|
||||
y="262"
|
||||
text-anchor="end"
|
||||
fill={accent}
|
||||
opacity="0.16"
|
||||
font-family="'JetBrains Mono', ui-monospace, monospace"
|
||||
font-weight="700"
|
||||
font-size="96"
|
||||
>{art.gc}</text>
|
||||
{
|
||||
hasImage ? (
|
||||
<img
|
||||
src={`/categories/${slug}.webp`}
|
||||
alt={alt}
|
||||
width="1024"
|
||||
height="576"
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
class="category-banner-img"
|
||||
/>
|
||||
) : (
|
||||
<g opacity="0.22">
|
||||
<circle cx="560" cy="196" r="58" fill="none" stroke={accent} stroke-width="10" />
|
||||
<path d="M 536 196 l 17 17 l 32 -36" fill="none" stroke={accent} stroke-width="12" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
<svg
|
||||
viewBox="0 0 640 320"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label={meta ? `${meta.name.en} category` : 'Category illustration'}
|
||||
focusable="false"
|
||||
>
|
||||
<rect width="640" height="320" fill="#0d1220" />
|
||||
<circle cx="600" cy="-20" r="150" fill={accent} opacity="0.12" />
|
||||
<circle cx="10" cy="330" r="130" fill="#0e94ff" opacity="0.08" />
|
||||
|
||||
{/* terminal window */}
|
||||
<rect x="28" y="24" width="584" height="268" rx="12" fill="#161e2e" stroke="#2a3550" stroke-width="1" />
|
||||
{/* titlebar */}
|
||||
<path
|
||||
d="M 28 36 a 12 12 0 0 1 12 -12 h 560 a 12 12 0 0 1 12 12 v 32 h -584 z"
|
||||
fill="#131a2a"
|
||||
/>
|
||||
<circle cx="52" cy="46" r="6" fill="#ff5c5c" />
|
||||
<circle cx="74" cy="46" r="6" fill="#f5a524" />
|
||||
<circle cx="96" cy="46" r="6" fill="#3fb950" />
|
||||
<text x="118" y="52" fill="#6b7c99" font-family="'JetBrains Mono', ui-monospace, monospace" font-size="15">{art.title}</text>
|
||||
|
||||
{/* body lines */}
|
||||
{art.lines.map((ln, i) => (
|
||||
<text
|
||||
x="52"
|
||||
y={112 + i * 36}
|
||||
fill={LINE_COLORS[ln.t]}
|
||||
font-family="'JetBrains Mono', ui-monospace, monospace"
|
||||
font-size="19"
|
||||
>{ln.c}</text>
|
||||
))}
|
||||
|
||||
{/* watermark glyph */}
|
||||
{art.glyph === 'text' ? (
|
||||
<text
|
||||
x="588"
|
||||
y="262"
|
||||
text-anchor="end"
|
||||
fill={accent}
|
||||
opacity="0.16"
|
||||
font-family="'JetBrains Mono', ui-monospace, monospace"
|
||||
font-weight="700"
|
||||
font-size="96"
|
||||
>{art.gc}</text>
|
||||
) : (
|
||||
<g opacity="0.22">
|
||||
<circle cx="560" cy="196" r="58" fill="none" stroke={accent} stroke-width="10" />
|
||||
<path d="M 536 196 l 17 17 l 32 -36" fill="none" stroke={accent} stroke-width="12" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -12,8 +12,8 @@ const { counts, lang = 'en' } = Astro.props;
|
||||
const isZh = lang === 'zh';
|
||||
|
||||
const label = (count: number) => {
|
||||
if (isZh) return count === 0 ? '0 篇 · 敬请期待' : `${count} 篇`;
|
||||
return count === 0 ? '0 posts · coming soon' : `${count} post${count === 1 ? '' : 's'}`;
|
||||
if (isZh) return count === 0 ? '· 敬请期待' : '篇';
|
||||
return count === 0 ? '· coming soon' : count === 1 ? 'post' : 'posts';
|
||||
};
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user