| name | Asset Manager |
| description | Organize design assets, optimize images and fonts, maintain brand asset libraries, implement version control for assets, and enforce naming conventions. Keep design assets organized and production-ready. |
| version | 1.0.0 |
| tags | assets, images, fonts, optimization, organization |
Asset Manager
Keep design assets organized, optimized, and accessible.
Core Principle
Organized assets = faster development.
Assets should be:
- Easy to find
- Properly named
- Optimized for production
- Version controlled
- Consistently formatted
Phase 1: Asset Organization
Directory Structure
assets/
├── images/
│ ├── products/
│ ├── team/
│ ├── marketing/
│ └── ui/
├── icons/
│ ├── svg/
│ └── png/
├── fonts/
│ ├── primary/
│ └── secondary/
├── videos/
├── logos/
│ ├── svg/
│ ├── png/
│ └── variants/
└── brand/
├── colors.json
├── typography.json
└── guidelines.pdf
Naming Conventions
Images:
{category}-{description}-{size}.{format}
Examples:
product-hero-1920x1080.jpg
team-sarah-400x400.jpg
ui-background-pattern.png
Icons:
{icon-name}-{variant}.svg
Examples:
home-outline.svg
home-filled.svg
user-circle.svg
arrow-right.svg
Fonts:
{font-family}-{weight}.{format}
Examples:
Inter-Regular.woff2
Inter-Bold.woff2
Poppins-SemiBold.woff2
Automated Organization Script
// scripts/organize-assets.ts
import fs from 'fs/promises'
import path from 'path'
interface AssetRule {
pattern: RegExp
destination: string
}
const rules: AssetRule[] = [
{ pattern: /product-/i, destination: 'images/products' },
{ pattern: /team-/i, destination: 'images/team' },
{ pattern: /icon-/i, destination: 'icons/svg' },
{ pattern: /logo-/i, destination: 'logos' },
]
async function organizeAssets(sourceDir: string) {
const files = await fs.readdir(sourceDir)
for (const file of files) {
const sourcePath = path.join(sourceDir, file)
const stat = await fs.stat(sourcePath)
if (stat.isDirectory()) continue
// Find matching rule
const rule = rules.find(r => r.pattern.test(file))
if (rule) {
const destDir = path.join('assets', rule.destination)
await fs.mkdir(destDir, { recursive: true })
const destPath = path.join(destDir, file)
await fs.rename(sourcePath, destPath)
console.log(`Moved: ${file} → ${rule.destination}`)
}
}
console.log('Assets organized!')
}
organizeAssets('./unsorted-assets')
Phase 2: Image Optimization
Install Optimization Tools
npm install sharp imagemin imagemin-mozjpeg imagemin-pngquant imagemin-svgo
Optimize Images Script
// scripts/optimize-images.ts
import sharp from 'sharp'
import imagemin from 'imagemin'
import imageminMozjpeg from 'imagemin-mozjpeg'
import imageminPngquant from 'imagemin-pngquant'
import imageminSvgo from 'imagemin-svgo'
import fs from 'fs/promises'
import path from 'path'
interface OptimizeOptions {
quality?: number
maxWidth?: number
formats?: ('jpg' | 'png' | 'webp' | 'avif')[]
}
async function optimizeImages(
inputDir: string,
outputDir: string,
options: OptimizeOptions = {}
) {
const {
quality = 80,
maxWidth = 2000,
formats = ['jpg', 'png', 'webp']
} = options
const files = await fs.readdir(inputDir)
for (const file of files) {
const inputPath = path.join(inputDir, file)
const stat = await fs.stat(inputPath)
if (stat.isDirectory()) continue
const ext = path.extname(file).toLowerCase()
const name = path.basename(file, ext)
console.log(`Processing: ${file}`)
// Skip SVGs (handle separately)
if (ext === '.svg') {
await optimizeSVG(inputPath, outputDir)
continue
}
// Skip non-images
if (!['.jpg', '.jpeg', '.png'].includes(ext)) continue
// Read image
const image = sharp(inputPath)
const metadata = await image.metadata()
// Resize if too large
if (metadata.width && metadata.width > maxWidth) {
image.resize(maxWidth, null, {
withoutEnlargement: true,
fit: 'inside'
})
}
// Generate formats
for (const format of formats) {
const outputPath = path.join(outputDir, `${name}.${format}`)
if (format === 'jpg') {
await image
.jpeg({ quality, mozjpeg: true })
.toFile(outputPath)
} else if (format === 'png') {
await image
.png({ quality, compressionLevel: 9 })
.toFile(outputPath)
} else if (format === 'webp') {
await image
.webp({ quality })
.toFile(outputPath)
} else if (format === 'avif') {
await image
.avif({ quality })
.toFile(outputPath)
}
console.log(` ✓ Generated ${format}`)
}
}
console.log('Images optimized!')
}
async function optimizeSVG(inputPath: string, outputDir: string) {
const fileName = path.basename(inputPath)
const outputPath = path.join(outputDir, fileName)
await imagemin([inputPath], {
destination: outputDir,
plugins: [
imageminSvgo({
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'removeDimensions', active: true },
{ name: 'removeUselessStrokeAndFill', active: true }
]
})
]
})
console.log(` ✓ Optimized SVG`)
}
// Usage
optimizeImages('./assets/images/raw', './assets/images/optimized', {
quality: 85,
maxWidth: 1920,
formats: ['jpg', 'webp', 'avif']
})
Responsive Images Generator
// scripts/generate-responsive-images.ts
import sharp from 'sharp'
import fs from 'fs/promises'
import path from 'path'
const breakpoints = [
{ name: 'mobile', width: 640 },
{ name: 'tablet', width: 768 },
{ name: 'desktop', width: 1920 }
]
async function generateResponsiveImages(inputPath: string) {
const ext = path.extname(inputPath)
const name = path.basename(inputPath, ext)
const dir = path.dirname(inputPath)
for (const bp of breakpoints) {
const image = sharp(inputPath)
// Resize
image.resize(bp.width, null, {
withoutEnlargement: true,
fit: 'inside'
})
// Generate WebP
const webpPath = path.join(dir, `${name}-${bp.name}.webp`)
await image.webp({ quality: 80 }).toFile(webpPath)
console.log(` ✓ ${name}-${bp.name}.webp`)
// Generate AVIF
const avifPath = path.join(dir, `${name}-${bp.name}.avif`)
await image.avif({ quality: 80 }).toFile(avifPath)
console.log(` ✓ ${name}-${bp.name}.avif`)
}
console.log(`Generated responsive images for: ${name}`)
}
// Usage
generateResponsiveImages('./assets/images/hero.jpg')
Phase 3: Font Management
Font Optimization
// scripts/optimize-fonts.ts
import { exec } from 'child_process'
import { promisify } from 'util'
import fs from 'fs/promises'
import path from 'path'
const execAsync = promisify(exec)
async function optimizeFonts(inputDir: string, outputDir: string) {
const files = await fs.readdir(inputDir)
for (const file of files) {
const inputPath = path.join(inputDir, file)
const ext = path.extname(file).toLowerCase()
if (ext !== '.ttf' && ext !== '.otf') continue
const name = path.basename(file, ext)
console.log(`Processing: ${file}`)
// Convert to WOFF2 (best compression)
const woff2Path = path.join(outputDir, `${name}.woff2`)
await convertToWOFF2(inputPath, woff2Path)
console.log(` ✓ ${name}.woff2`)
// Convert to WOFF (fallback)
const woffPath = path.join(outputDir, `${name}.woff`)
await convertToWOFF(inputPath, woffPath)
console.log(` ✓ ${name}.woff`)
}
console.log('Fonts optimized!')
}
async function convertToWOFF2(input: string, output: string) {
// Requires woff2_compress tool
// Install: brew install woff2
await execAsync(`woff2_compress ${input}`)
const woff2File = input.replace(/\.(ttf|otf)$/, '.woff2')
await fs.rename(woff2File, output)
}
async function convertToWOFF(input: string, output: string) {
// Requires sfnt2woff tool
await execAsync(`sfnt2woff ${input}`)
const woffFile = input.replace(/\.(ttf|otf)$/, '.woff')
await fs.rename(woffFile, output)
}
optimizeFonts('./assets/fonts/raw', './assets/fonts/optimized')
Font Loading Strategy
/* fonts.css */
/* Preload critical fonts */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Regular.woff2') format('woff2'),
url('/fonts/Inter-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap; /* Show fallback first */
}
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Bold.woff2') format('woff2'),
url('/fonts/Inter-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
Preload in HTML:
<head>
<!-- Preload critical fonts -->
<link rel="preload" href="/fonts/Inter-Regular.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/Inter-Bold.woff2" as="font" type="font/woff2" crossorigin>
</head>
Phase 4: Asset Version Control
Git LFS Setup
# Install Git LFS
brew install git-lfs
git lfs install
# Track large files
git lfs track "*.psd"
git lfs track "*.ai"
git lfs track "*.sketch"
git lfs track "*.fig"
git lfs track "*.mp4"
git lfs track "*.mov"
git lfs track "assets/images/**/*.jpg"
git lfs track "assets/images/**/*.png"
# Add .gitattributes
git add .gitattributes
git commit -m "Configure Git LFS"
Asset Versioning System
// scripts/version-assets.ts
import fs from 'fs/promises'
import path from 'path'
import crypto from 'crypto'
interface AssetVersion {
path: string
hash: string
size: number
modified: string
version: number
}
class AssetVersionManager {
private versionFile = 'asset-versions.json'
private versions: Map<string, AssetVersion[]> = new Map()
async load() {
try {
const data = await fs.readFile(this.versionFile, 'utf-8')
const parsed = JSON.parse(data)
for (const [path, versions] of Object.entries(parsed)) {
this.versions.set(path, versions as AssetVersion[])
}
} catch (error) {
// File doesn't exist yet
}
}
async save() {
const data = Object.fromEntries(this.versions)
await fs.writeFile(this.versionFile, JSON.stringify(data, null, 2))
}
async trackAsset(filePath: string) {
const buffer = await fs.readFile(filePath)
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
const stat = await fs.stat(filePath)
const versions = this.versions.get(filePath) || []
const lastVersion = versions[versions.length - 1]
// Check if changed
if (lastVersion && lastVersion.hash === hash) {
console.log(`No changes: ${filePath}`)
return
}
// Add new version
versions.push({
path: filePath,
hash,
size: stat.size,
modified: stat.mtime.toISOString(),
version: versions.length + 1
})
this.versions.set(filePath, versions)
console.log(`Tracked: ${filePath} (v${versions.length})`)
}
async trackDirectory(dirPath: string) {
const files = await fs.readdir(dirPath, { recursive: true })
for (const file of files) {
const filePath = path.join(dirPath, file.toString())
const stat = await fs.stat(filePath)
if (stat.isDirectory()) continue
await this.trackAsset(filePath)
}
}
}
// Usage
const manager = new AssetVersionManager()
await manager.load()
await manager.trackDirectory('./assets')
await manager.save()
Phase 5: Brand Asset Library
Brand Kit Structure
brand/
├── logos/
│ ├── primary/
│ │ ├── logo-full.svg
│ │ ├── logo-icon.svg
│ │ └── logo-wordmark.svg
│ ├── variations/
│ │ ├── logo-white.svg
│ │ ├── logo-black.svg
│ │ └── logo-inverted.svg
│ └── exports/
│ ├── png/
│ ├── pdf/
│ └── eps/
├── colors/
│ ├── colors.json
│ ├── colors.css
│ └── colors.scss
├── typography/
│ ├── fonts/
│ └── typography.json
└── guidelines/
├── brand-guidelines.pdf
├── logo-usage.pdf
└── color-usage.pdf
Brand Asset Manifest
// brand/manifest.ts
export interface BrandAssets {
version: string
lastUpdated: string
logos: LogoAsset[]
colors: ColorAsset[]
typography: TypographyAsset[]
}
export interface LogoAsset {
name: string
variants: {
full: string
icon: string
wordmark: string
}
formats: {
svg: string
png: { [size: string]: string }
pdf: string
}
}
export interface ColorAsset {
name: string
hex: string
rgb: { r: number; g: number; b: number }
usage: string
}
export interface TypographyAsset {
name: string
family: string
weights: number[]
formats: string[]
}
export const brandAssets: BrandAssets = {
version: '2.0.0',
lastUpdated: '2024-01-15',
logos: [
{
name: 'Primary Logo',
variants: {
full: '/brand/logos/logo-full.svg',
icon: '/brand/logos/logo-icon.svg',
wordmark: '/brand/logos/logo-wordmark.svg'
},
formats: {
svg: '/brand/logos/logo-full.svg',
png: {
'1x': '/brand/logos/exports/png/logo-full@1x.png',
'2x': '/brand/logos/exports/png/logo-full@2x.png',
'3x': '/brand/logos/exports/png/logo-full@3x.png'
},
pdf: '/brand/logos/exports/pdf/logo-full.pdf'
}
}
],
colors: [
{
name: 'Primary',
hex: '#0066cc',
rgb: { r: 0, g: 102, b: 204 },
usage: 'Primary actions, links, brand elements'
}
],
typography: [
{
name: 'Inter',
family: 'Inter',
weights: [400, 600, 700],
formats: ['woff2', 'woff']
}
]
}
Best Practices
1. Optimize for Web
- Images: Use WebP/AVIF with JPEG fallback
- Icons: Use SVG sprites for multiple icons
- Fonts: Use WOFF2 with WOFF fallback
- Videos: Compress and provide multiple formats
2. Lazy Loading
// Lazy load images
<img
src="placeholder.jpg"
data-src="hero.jpg"
loading="lazy"
alt="Hero"
/>
// Or use Next.js Image
import Image from 'next/image'
<Image
src="/hero.jpg"
width={1920}
height={1080}
placeholder="blur"
alt="Hero"
/>
3. Asset CDN
// Use CDN for assets
const CDN_URL = process.env.CDN_URL || ''
export function getAssetUrl(path: string): string {
if (CDN_URL) {
return `${CDN_URL}${path}`
}
return path
}
// Usage
<img src={getAssetUrl('/images/hero.jpg')} alt="Hero" />
4. Automated Asset Pipeline
// scripts/asset-pipeline.ts
async function runAssetPipeline() {
console.log('Starting asset pipeline...')
// 1. Organize assets
await organizeAssets('./unsorted')
// 2. Optimize images
await optimizeImages('./assets/images/raw', './assets/images/optimized')
// 3. Generate responsive images
await generateResponsiveImages('./assets/images/optimized')
// 4. Optimize fonts
await optimizeFonts('./assets/fonts/raw', './assets/fonts/optimized')
// 5. Version assets
const manager = new AssetVersionManager()
await manager.load()
await manager.trackDirectory('./assets')
await manager.save()
console.log('Asset pipeline complete!')
}
runAssetPipeline()
Tools & Resources
Optimization Tools:
- Sharp - Image processing
- ImageOptim - Image compression
- SVGO - SVG optimization
- Squoosh - Online image compression
Font Tools:
- Glyphhanger - Font subsetting
- Transfonter - Font conversion
- FontForge - Font editor
Related Skills:
visual-designer- Design principlesfigma-developer- Export from Figmabrand-designer- Brand asset creation
Organized assets, optimized performance. 📦