For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Maximize SEO with build-time generated og-image, redesigned favicon, social meta tags, font preloading, and enhanced structured data.
Architecture: TypeScript scripts in scripts/ generate all image assets at build time. satori renders declarative markup to SVG (for og-image with text), sharp converts SVG to PNG at various sizes, png-to-ico creates multi-size ICO files. The build script chains asset generation before VitePress build.
Tech Stack: satori, sharp, png-to-ico, tsx (devDependencies)
| Action | Path | Responsibility |
|---|---|---|
| Create | scripts/generate-assets.ts | Entry point — runs both generators |
| Create | scripts/generate-og-image.ts | Generates public/og-image.png via satori + sharp |
| Create | scripts/generate-favicon.ts | Generates all favicon variants from SVG |
| Modify | public/favicon.svg | Redesigned spider icon |
| Modify | package.json | Add devDependencies and generate-assets script |
| Modify | .vitepress/config.ts | Meta tags, resource hints, structured data |
| Generate | public/og-image.png | 1200x630 social sharing image |
| Generate | public/favicon.ico | 16x16 + 32x32 multi-size |
| Generate | public/favicon-192x192.png | PWA icon |
| Generate | public/favicon-512x512.png | PWA large icon |
| Generate | public/apple-touch-icon.png | 180x180 Apple icon |
Files:
Modify: package.json
[ ] Step 1: Install devDependencies
npm install --save-dev satori sharp png-to-ico tsxIn package.json, add/modify the scripts section:
{
"scripts": {
"dev": "vitepress dev",
"build": "npm run generate-assets && vitepress build",
"preview": "vitepress preview",
"generate-assets": "tsx scripts/generate-assets.ts"
}
}git add package.json package-lock.json
git commit -m "chore: add satori, sharp, png-to-ico, tsx for asset generation"Files:
Modify: public/favicon.svg
[ ] Step 1: Replace favicon.svg with new spider design
The new design should be a clearer spider icon that reads well at 16x16. Key improvements over current:
Replace public/favicon.svg with:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<!-- Body: head + abdomen -->
<circle cx="16" cy="12" r="4.5" fill="#8b5cf6"/>
<ellipse cx="16" cy="21" rx="5.5" ry="6" fill="#8b5cf6"/>
<!-- Legs: 4 pairs, thicker strokes for small-size readability -->
<!-- Front legs (up and out) -->
<path d="M12.5 11 Q7 6 3 4" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M19.5 11 Q25 6 29 4" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<!-- Upper-mid legs (out and slightly down) -->
<path d="M11.5 14 Q5 13 1 12" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M20.5 14 Q27 13 31 12" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<!-- Lower-mid legs (out and down) -->
<path d="M11 19 Q5 22 2 25" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M21 19 Q27 22 30 25" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<!-- Back legs (down and out) -->
<path d="M12.5 24 Q8 28 5 31" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M19.5 24 Q24 28 27 31" stroke="#8b5cf6" stroke-width="2" stroke-linecap="round" fill="none"/>
<!-- Eyes -->
<circle cx="14.2" cy="11" r="1.2" fill="#0a0a0f"/>
<circle cx="17.8" cy="11" r="1.2" fill="#0a0a0f"/>
</svg>Open public/favicon.svg in a browser or preview tool. Confirm:
Spider is clearly recognizable
8 legs visible and evenly spaced
Eyes are visible
Purple (#8b5cf6) color is correct
[ ] Step 3: Commit
git add public/favicon.svg
git commit -m "feat: redesign favicon with clearer spider icon"Files:
Create: scripts/generate-favicon.ts
[ ] Step 1: Create the favicon generation script
Create scripts/generate-favicon.ts:
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import sharp from 'sharp'
import pngToIco from 'png-to-ico'
const PUBLIC_DIR = resolve(import.meta.dirname, '..', 'public')
const SIZES = {
'favicon-192x192.png': 192,
'favicon-512x512.png': 512,
'apple-touch-icon.png': 180,
} as const
export async function generateFavicon(): Promise<void> {
const svgPath = resolve(PUBLIC_DIR, 'favicon.svg')
const svgBuffer = await readFile(svgPath)
// Generate PNG variants
const pngPromises = Object.entries(SIZES).map(async ([filename, size]) => {
const pngBuffer = await sharp(svgBuffer)
.resize(size, size)
.png()
.toBuffer()
await sharp(pngBuffer).toFile(resolve(PUBLIC_DIR, filename))
console.log(` ✓ ${filename} (${size}x${size})`)
return { filename, buffer: pngBuffer }
})
const results = await Promise.all(pngPromises)
// Generate ICO from 16x16 and 32x32 PNGs
const ico16 = await sharp(svgBuffer).resize(16, 16).png().toBuffer()
const ico32 = await sharp(svgBuffer).resize(32, 32).png().toBuffer()
const icoBuffer = await pngToIco([ico16, ico32])
await sharp(icoBuffer).toFile(resolve(PUBLIC_DIR, 'favicon.ico'))
console.log(' ✓ favicon.ico (16x16 + 32x32)')
}npx tsx --eval "import('./scripts/generate-favicon.ts').then(m => console.log('OK: exports', Object.keys(m)))"Expected: OK: exports [ 'generateFavicon' ]
git add scripts/generate-favicon.ts
git commit -m "feat: add favicon generation script"Files:
Create: scripts/generate-og-image.ts
[ ] Step 1: Create the og-image generation script
Create scripts/generate-og-image.ts:
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import satori from 'satori'
import sharp from 'sharp'
const PUBLIC_DIR = resolve(import.meta.dirname, '..', 'public')
const FONTS_DIR = resolve(PUBLIC_DIR, 'fonts')
const WIDTH = 1200
const HEIGHT = 630
// Spider icon as SVG path data (simplified from favicon for embedding)
const SPIDER_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<circle cx="16" cy="12" r="4.5" fill="#c4b5fd"/>
<ellipse cx="16" cy="21" rx="5.5" ry="6" fill="#c4b5fd"/>
<path d="M12.5 11 Q7 6 3 4" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M19.5 11 Q25 6 29 4" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M11.5 14 Q5 13 1 12" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M20.5 14 Q27 13 31 12" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M11 19 Q5 22 2 25" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M21 19 Q27 22 30 25" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M12.5 24 Q8 28 5 31" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<path d="M19.5 24 Q24 28 27 31" stroke="#c4b5fd" stroke-width="2" stroke-linecap="round" fill="none"/>
<circle cx="14.2" cy="11" r="1.2" fill="#0a0a0f"/>
<circle cx="17.8" cy="11" r="1.2" fill="#0a0a0f"/>
</svg>
`.trim()
// Decorative web lines as SVG overlay
const WEB_LINES_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${WIDTH} ${HEIGHT}" fill="none">
<!-- Corner web lines -->
<path d="M0 0 Q200 80 350 200" stroke="#8b5cf6" stroke-width="1" opacity="0.15" fill="none"/>
<path d="M0 0 Q100 150 180 300" stroke="#8b5cf6" stroke-width="1" opacity="0.1" fill="none"/>
<path d="M${WIDTH} 0 Q${WIDTH - 200} 80 ${WIDTH - 350} 200" stroke="#8b5cf6" stroke-width="1" opacity="0.15" fill="none"/>
<path d="M${WIDTH} 0 Q${WIDTH - 100} 150 ${WIDTH - 180} 300" stroke="#8b5cf6" stroke-width="1" opacity="0.1" fill="none"/>
<path d="M0 ${HEIGHT} Q200 ${HEIGHT - 80} 350 ${HEIGHT - 200}" stroke="#8b5cf6" stroke-width="1" opacity="0.1" fill="none"/>
<path d="M${WIDTH} ${HEIGHT} Q${WIDTH - 200} ${HEIGHT - 80} ${WIDTH - 350} ${HEIGHT - 200}" stroke="#8b5cf6" stroke-width="1" opacity="0.1" fill="none"/>
<!-- Radial web circles -->
<circle cx="${WIDTH / 2}" cy="${HEIGHT / 2}" r="250" stroke="#8b5cf6" stroke-width="0.5" opacity="0.08" fill="none"/>
<circle cx="${WIDTH / 2}" cy="${HEIGHT / 2}" r="180" stroke="#8b5cf6" stroke-width="0.5" opacity="0.06" fill="none"/>
</svg>
`.trim()
export async function generateOgImage(): Promise<void> {
// Load fonts
const [cinzelFont, interFont] = await Promise.all([
readFile(resolve(FONTS_DIR, 'cinzel-v23-latin-700.woff2')),
readFile(resolve(FONTS_DIR, 'inter-v18-latin-400.woff2')),
])
// Generate text layout with satori
const svg = await satori(
{
type: 'div',
props: {
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#0a0a0f',
background: 'radial-gradient(ellipse at center, #1a1028 0%, #0a0a0f 70%)',
},
children: [
{
type: 'div',
props: {
style: {
fontSize: 72,
fontFamily: 'Cinzel',
fontWeight: 700,
color: '#e9e5f0',
letterSpacing: '0.04em',
marginTop: 80,
},
children: 'Aranea Development',
},
},
{
type: 'div',
props: {
style: {
width: 120,
height: 2,
backgroundColor: '#8b5cf6',
marginTop: 24,
marginBottom: 24,
borderRadius: 1,
},
},
},
{
type: 'div',
props: {
style: {
fontSize: 28,
fontFamily: 'Inter',
color: '#a89cc0',
letterSpacing: '0.02em',
},
children: 'Solo developer building tools for developers',
},
},
],
},
},
{
width: WIDTH,
height: HEIGHT,
fonts: [
{ name: 'Cinzel', data: cinzelFont, weight: 700, style: 'normal' },
{ name: 'Inter', data: interFont, weight: 400, style: 'normal' },
],
},
)
// Render satori SVG to base PNG
const basePng = await sharp(Buffer.from(svg)).png().toBuffer()
// Render spider icon to PNG (centered, upper area — 120x120 at y=40)
const spiderPng = await sharp(Buffer.from(SPIDER_SVG))
.resize(120, 120)
.png()
.toBuffer()
// Render web lines overlay
const webLinesPng = await sharp(Buffer.from(WEB_LINES_SVG))
.resize(WIDTH, HEIGHT)
.png()
.toBuffer()
// Composite all layers: base + web lines + spider
await sharp(basePng)
.composite([
{ input: webLinesPng, top: 0, left: 0 },
{ input: spiderPng, top: 40, left: Math.round((WIDTH - 120) / 2) },
])
.png({ quality: 90 })
.toFile(resolve(PUBLIC_DIR, 'og-image.png'))
console.log(` ✓ og-image.png (${WIDTH}x${HEIGHT})`)
}npx tsx --eval "import('./scripts/generate-og-image.ts').then(m => console.log('OK: exports', Object.keys(m)))"Expected: OK: exports [ 'generateOgImage' ]
git add scripts/generate-og-image.ts
git commit -m "feat: add og-image generation script"Files:
Create: scripts/generate-assets.ts
[ ] Step 1: Create the entry point
Create scripts/generate-assets.ts:
import { generateFavicon } from './generate-favicon.js'
import { generateOgImage } from './generate-og-image.js'
async function main() {
console.log('Generating assets...')
console.log('\nFavicons:')
await generateFavicon()
console.log('\nOG Image:')
await generateOgImage()
console.log('\nDone!')
}
main().catch((err) => {
console.error('Asset generation failed:', err)
process.exit(1)
})npm run generate-assetsExpected output:
Generating assets...
Favicons:
✓ favicon-192x192.png (192x192)
✓ favicon-512x512.png (512x512)
✓ apple-touch-icon.png (180x180)
✓ favicon.ico (16x16 + 32x32)
OG Image:
✓ og-image.png (1200x630)
Done!ls -la public/og-image.png public/favicon.ico public/favicon-192x192.png public/favicon-512x512.png public/apple-touch-icon.pngAll files should exist with reasonable sizes (og-image ~50-200KB, PNGs ~5-50KB each).
Open public/og-image.png and confirm:
Dark background with purple gradient
Spider icon in upper area
"Aranea Development" in Cinzel font
Tagline below in Inter
Subtle web line decorations
Clean, professional look at 1200x630
[ ] Step 5: Visually verify favicon
Open public/favicon.svg and public/favicon-192x192.png. Confirm:
Spider is recognizable
Colors match brand
PNG is crisp at 192x192
[ ] Step 6: Commit
git add scripts/generate-assets.ts public/og-image.png public/favicon.ico public/favicon-192x192.png public/favicon-512x512.png public/apple-touch-icon.png
git commit -m "feat: add asset generation entry point and generated assets"Files:
Modify: .vitepress/config.ts
[ ] Step 1: Add og:image, twitter:image, and resource hints
In .vitepress/config.ts, in the head array, add after the existing Twitter Card entries:
// Open Graph Image
['meta', { property: 'og:image', content: `${siteUrl}/og-image.png` }],
['meta', { property: 'og:image:width', content: '1200' }],
['meta', { property: 'og:image:height', content: '630' }],
['meta', { property: 'og:image:type', content: 'image/png' }],Add twitter:image after the existing twitter entries:
['meta', { name: 'twitter:image', content: `${siteUrl}/og-image.png` }],Change the existing twitter:card from summary to summary_large_image:
['meta', { name: 'twitter:card', content: 'summary_large_image' }],Add font preload hints after the favicon link entries (before meta tags):
// Font preloads
['link', { rel: 'preload', href: '/fonts/cinzel-v23-latin-700.woff2', as: 'font', type: 'font/woff2', crossorigin: '' }],
['link', { rel: 'preload', href: '/fonts/inter-v18-latin-400.woff2', as: 'font', type: 'font/woff2', crossorigin: '' }],npx tsx --eval "import('./.vitepress/config.ts').then(m => console.log('Config OK'))"Expected: Config OK
git add .vitepress/config.ts
git commit -m "feat: add og:image, twitter:image, font preloads to head config"Files:
Modify: .vitepress/config.ts
[ ] Step 1: Add Organization schema and enhance existing schemas
In .vitepress/config.ts, in the @graph array inside the JSON-LD script tag, add the Organization schema after the Person schema:
{
'@type': 'Organization',
'@id': `${siteUrl}/#organization`,
name: 'Aranea Development',
url: siteUrl,
logo: {
'@type': 'ImageObject',
url: `${siteUrl}/favicon-512x512.png`,
width: 512,
height: 512,
},
image: `${siteUrl}/og-image.png`,
founder: { '@id': `${siteUrl}/#person` },
sameAs: ['https://github.com/AraneaDev'],
},Add publisher to the existing WebSite schema:
publisher: { '@id': `${siteUrl}/#organization` },Add image to the existing WebSite schema:
image: `${siteUrl}/og-image.png`,Add worksFor to the existing Person schema:
worksFor: { '@id': `${siteUrl}/#organization` },npx tsx --eval "
import config from './.vitepress/config.ts';
const head = config.default?.head || config.head || [];
const jsonLd = head.find(h => h[1]?.type === 'application/ld+json');
const data = JSON.parse(jsonLd[2]);
console.log('Types:', data['@graph'].map(n => n['@type']));
console.log('Organization:', data['@graph'].find(n => n['@type'] === 'Organization')?.name);
console.log('WebSite publisher:', data['@graph'].find(n => n['@type'] === 'WebSite')?.publisher);
console.log('Person worksFor:', data['@graph'].find(n => n['@type'] === 'Person')?.worksFor);
"Expected output shows all 5 types (WebSite, Person, Organization, ItemList) with proper references.
git add .vitepress/config.ts
git commit -m "feat: add Organization schema, enhance structured data references"npm run buildExpected: Asset generation runs first, then VitePress builds successfully.
grep -E 'og:image|twitter:image|twitter:card|preload.*font|Organization' dist/index.htmlExpected: All new meta tags present in the built HTML output.
ls -la dist/og-image.png dist/favicon.ico dist/favicon-192x192.png dist/favicon-512x512.png dist/apple-touch-icon.png dist/favicon.svgAll files should be present (VitePress copies public/ to dist/).
npm run previewOpen the preview URL and check:
Favicon shows new spider icon in browser tab
View page source confirms all meta tags
Right-click → Inspect shows no console errors
[ ] Step 5: Commit any remaining changes
git add -A
git commit -m "chore: verify full build with SEO improvements"