diff --git a/src/pages/images/avatars/[author].png.js b/src/pages/images/avatars/[author].png.js
index 7ffe78f..df69c22 100644
--- a/src/pages/images/avatars/[author].png.js
+++ b/src/pages/images/avatars/[author].png.js
@@ -1,5 +1,82 @@
import { getCollection } from 'astro:content';
import { getImage } from "astro:assets";
+import { mkdir, readFile, writeFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import { existsSync } from 'node:fs';
+
+// Simple in-memory cache for frequently accessed avatars
+const AVATAR_CACHE = new Map();
+const CACHE_MAX_SIZE = 50; // Maximum number of avatars to keep in memory
+const CACHE_DIR = 'node_modules/.astro/avatar-cache';
+const AVATAR_CACHE_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
+const MC_CACHE_TIME = 7 * 24 * 60 * 60; // 7 days in seconds for HTTP caching
+
+// Cache helpers
+async function ensureCacheDir() {
+ try {
+ await mkdir(CACHE_DIR, { recursive: true });
+ } catch (err) {
+ console.warn('Failed to create cache directory:', err);
+ }
+}
+
+async function getCachedAvatar(cacheKey) {
+ // Check memory cache first
+ if (AVATAR_CACHE.has(cacheKey)) {
+ const { data, timestamp } = AVATAR_CACHE.get(cacheKey);
+ // Validate that cache entry isn't too old
+ if (Date.now() - timestamp < AVATAR_CACHE_TTL) {
+ return data;
+ }
+ // Remove stale cache entry
+ AVATAR_CACHE.delete(cacheKey);
+ }
+
+ // Check file cache
+ const filePath = join(CACHE_DIR, `${cacheKey}.png`);
+ try {
+ if (existsSync(filePath)) {
+ const stat = await import('node:fs/promises').then(f => f.stat(filePath));
+ // Check if file cache is still valid
+ if (Date.now() - stat.mtimeMs < AVATAR_CACHE_TTL) {
+ const data = await readFile(filePath);
+ // Update memory cache
+ addToMemoryCache(cacheKey, data);
+ return data;
+ }
+ }
+ } catch (err) {
+ console.warn(`Failed to read cached avatar ${cacheKey}:`, err);
+ }
+
+ return null;
+}
+
+async function cacheAvatar(cacheKey, data) {
+ // Add to memory cache
+ addToMemoryCache(cacheKey, data);
+
+ // Add to file cache
+ try {
+ await ensureCacheDir();
+ await writeFile(join(CACHE_DIR, `${cacheKey}.png`), data);
+ } catch (err) {
+ console.warn(`Failed to cache avatar ${cacheKey}:`, err);
+ }
+}
+
+function addToMemoryCache(key, data) {
+ // If cache is full, remove oldest entry
+ if (AVATAR_CACHE.size >= CACHE_MAX_SIZE) {
+ const oldestKey = AVATAR_CACHE.keys().next().value;
+ AVATAR_CACHE.delete(oldestKey);
+ }
+
+ AVATAR_CACHE.set(key, {
+ data,
+ timestamp: Date.now()
+ });
+}
export async function getStaticPaths() {
const authorsData = await getCollection('authors');
@@ -11,8 +88,22 @@ export async function getStaticPaths() {
export async function GET({ props }) {
const { author } = props;
+ const authorId = author.id;
+ // Try to retrieve from cache first
if (author.data.avatar) {
+ const cacheKey = `avatar-${authorId}`;
+ const cachedAvatar = await getCachedAvatar(cacheKey);
+
+ if (cachedAvatar) {
+ return new Response(cachedAvatar, {
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Cache-Control': `public, max-age=${MC_CACHE_TIME}`
+ }
+ });
+ }
+
try {
const optimizedImage = await getImage({
src: author.data.avatar,
@@ -28,11 +119,15 @@ export async function GET({ props }) {
}
const imageBuffer = await imageResponse.arrayBuffer();
+ const bufferData = new Uint8Array(imageBuffer);
- return new Response(imageBuffer, {
+ // Cache the avatar for future requests
+ await cacheAvatar(cacheKey, bufferData);
+
+ return new Response(bufferData, {
headers: {
'Content-Type': 'image/png',
- 'Cache-Control': 'public, max-age=86400' // Cache for 24 hours
+ 'Cache-Control': `public, max-age=${MC_CACHE_TIME}`
}
});
} catch (error) {
@@ -46,11 +141,22 @@ export async function GET({ props }) {
}
const username = author.data.mcplayerid;
+ const cacheKey = `mc-${username}`;
+
+ // Check cache for Minecraft avatar
+ const cachedMcAvatar = await getCachedAvatar(cacheKey);
+ if (cachedMcAvatar) {
+ return new Response(cachedMcAvatar, {
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Cache-Control': `public, max-age=${MC_CACHE_TIME}`
+ }
+ });
+ }
try {
- // get Minecraft profile by username
+ // Get Minecraft profile by username
const profileResponse = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`);
-
if (!profileResponse.ok) {
return new Response('Player not found', { status: 404 });
}
@@ -58,31 +164,43 @@ export async function GET({ props }) {
const profile = await profileResponse.json();
const uuid = profile.id;
- // get skin data from session server
+ // Get skin data from session server
const sessionResponse = await fetch(`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`);
- const sessionData = await sessionResponse.json();
+ if (!sessionResponse.ok) {
+ return new Response('Session data not found', { status: 404 });
+ }
+
+ const sessionData = await sessionResponse.json();
const texturesProperty = sessionData.properties.find((prop) => prop.name === 'textures');
+
+ if (!texturesProperty) {
+ return new Response('Textures not found', { status: 404 });
+ }
+
const texturesData = JSON.parse(atob(texturesProperty.value));
const skinUrl = texturesData.textures.SKIN?.url;
+
if (!skinUrl) {
return new Response('Skin not found', { status: 404 });
}
- // get skin image from the URL
+ // Get skin image from the URL
const skinResponse = await fetch(skinUrl);
const skinBuffer = await skinResponse.arrayBuffer();
- // render the Minecraft head image
+ // Render the Minecraft head image
const headImage = await renderMinecraftHead(new Uint8Array(skinBuffer));
+ // Cache the rendered head
+ await cacheAvatar(cacheKey, headImage);
+
return new Response(headImage, {
headers: {
'Content-Type': 'image/png',
- 'Cache-Control': 'public, max-age=3600', // 缓存1小时
+ 'Cache-Control': `public, max-age=${MC_CACHE_TIME}`
},
});
-
} catch (error) {
console.error('Error fetching Minecraft head:', error);
return new Response('Internal server error', { status: 500 });
@@ -93,7 +211,7 @@ async function renderMinecraftHead(skinData) {
// Use sharp library to process images
const sharp = (await import('sharp')).default;
- // Load the skin image
+ // Load the skin image once and get metadata
const skinImage = sharp(skinData);
const metadata = await skinImage.metadata();
const { width, height } = metadata;
@@ -102,54 +220,67 @@ async function renderMinecraftHead(skinData) {
const isNewFormat = height === 64;
const headSize = 8; // Head is 8x8 pixels
const scale = 8; // Scale factor, final output is 64x64
+ const offset = -1; // 3D-like effect offset
- // 3D-like effect: slightly offset hat layer
- // TODO: real 3D effect, which would require more complex rendering
- const offset = -1; // Negative value moves up/left (creates 3D effect)
+ // Extract head base layer and prepare for compositing if needed
+ let extractionPromises = [
+ // Head base layer extraction promise
+ await skinImage
+ .clone()
+ .extract({left: 8, top: 8, width: headSize, height: headSize})
+ .png()
+ .toBuffer()
+ ];
- // Extract head base layer (8x8 pixels)
- const headBase = await skinImage
- .clone() // Clone to avoid modifying original
- .extract({ left: 8, top: 8, width: headSize, height: headSize })
- .png()
- .toBuffer();
+ // If new format, also extract hat layer in parallel
+ if (isNewFormat && width >= 48 && height >= 16) {
+ extractionPromises.push(
+ skinImage
+ .clone()
+ .extract({ left: 40, top: 8, width: headSize, height: headSize })
+ .png()
+ .toBuffer()
+ );
+ }
+ // Wait for all extractions to complete in parallel
+ const [headBase, hatLayer] = await Promise.all(extractionPromises);
+
+ // Start with base layer
let finalHead = sharp(headBase).resize(headSize * scale, headSize * scale, {
- kernel: 'nearest' // Keep pixel art style
+ kernel: 'nearest', // Keep pixel art style
+ fastShrinkOnLoad: true // Performance optimization
});
- // If new format and has hat layer, composite hat layer
- if (isNewFormat) {
+ // If we have a hat layer, composite it
+ if (hatLayer) {
try {
- // Check if we're in bounds before extracting hat layer
- if (width >= 48 && height >= 16) {
- // Extract hat layer (8x8 pixels)
- const hatLayer = await skinImage
- .clone() // Clone to avoid modifying original
- .extract({ left: 40, top: 8, width: headSize, height: headSize })
- .png()
- .toBuffer();
+ const hatResized = await sharp(hatLayer)
+ .resize(headSize * scale, headSize * scale, {
+ kernel: 'nearest',
+ fastShrinkOnLoad: true
+ })
+ .png({ compressionLevel: 9, adaptiveFiltering: true })
+ .toBuffer();
- // Resize hat layer
- const hatResized = await sharp(hatLayer)
- .resize(headSize * scale, headSize * scale, { kernel: 'nearest' })
- .png()
- .toBuffer();
-
- // Composite base layer and hat layer with offset for 3D effect
- finalHead = finalHead.composite([{
- input: hatResized,
- left: offset * scale, // Apply scaled offset horizontally
- top: offset * scale, // Apply scaled offset vertically
- blend: 'over'
- }]);
- }
+ finalHead = finalHead.composite([{
+ input: hatResized,
+ left: offset * scale,
+ top: offset * scale,
+ blend: 'over'
+ }]);
} catch (error) {
- // If hat layer processing fails, just use base layer
console.warn('Failed to process hat layer:', error);
}
}
- const result = await finalHead.png().toBuffer();
+ // Optimize PNG compression
+ const result = await finalHead.png({
+ compressionLevel: 9,
+ adaptiveFiltering: true,
+ force: true
+ }).toBuffer();
+
return new Uint8Array(result);
-}
\ No newline at end of file
+}
+
diff --git a/src/pages/post/[slug]/featured.png.js b/src/pages/post/[slug]/featured.png.js
index bcd4371..4755d27 100644
--- a/src/pages/post/[slug]/featured.png.js
+++ b/src/pages/post/[slug]/featured.png.js
@@ -1,6 +1,65 @@
import { getCollection } from 'astro:content';
import sharp from 'sharp';
import {getImage} from "astro:assets";
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import crypto from 'node:crypto';
+
+// Ensure cache directory exists
+const CACHE_DIR = 'node_modules/.astro/og-cache';
+
+// Cache for external images to avoid repeated processing
+const externalImageCache = new Map();
+// Cache for generated OG images (memory cache)
+const ogImageCache = new Map();
+
+// Initialize cache directory
+async function ensureCacheDir() {
+ try {
+ await fs.mkdir(CACHE_DIR, { recursive: true });
+ } catch (error) {
+ console.error('Error creating cache directory:', error);
+ }
+}
+
+// Create hash for consistent filenames
+function createHash(str) {
+ return crypto.createHash('md5').update(str).digest('hex');
+}
+
+// File-based cache operations
+const fileCache = {
+ async get(key) {
+ try {
+ const filePath = path.join(CACHE_DIR, `${createHash(key)}.webp`);
+ const stats = await fs.stat(filePath);
+
+ // Check if file exists and is not too old (30 days)
+ const now = new Date();
+ const fileAge = (now - stats.mtime) / (1000 * 60 * 60 * 24);
+
+ if (fileAge > 30) {
+ return null; // File is too old
+ }
+
+ return await fs.readFile(filePath);
+ } catch (error) {
+ return null; // File doesn't exist or can't be read
+ }
+ },
+
+ async set(key, data) {
+ try {
+ await ensureCacheDir();
+ const filePath = path.join(CACHE_DIR, `${createHash(key)}.webp`);
+ await fs.writeFile(filePath, data);
+ return true;
+ } catch (error) {
+ console.error('Error writing to file cache:', error);
+ return false;
+ }
+ }
+};
export async function getStaticPaths() {
const blogEntries = await getCollection('posts');
@@ -11,13 +70,22 @@ export async function getStaticPaths() {
// get the post has a external featured.* image files
async function getExternalImage(post) {
+ // Check cache first
+ if (externalImageCache.has(post.slug)) {
+ return externalImageCache.get(post.slug);
+ }
+
const featuredImages = import.meta.glob(`/src/content/posts/*/featured.*`, {import: 'default', eager: true});
const matchedImage = Object.keys(featuredImages).find(path => path.includes(post.slug));
let matchedImage_;
if (matchedImage) {
matchedImage_ = await getImage({src: featuredImages[matchedImage], format: 'webp'}) || null;
}
- return matchedImage_?.src;
+
+ // Store in cache
+ const result = matchedImage_?.src;
+ externalImageCache.set(post.slug, result);
+ return result;
}
// Function to check for images in markdown without rendering
@@ -30,12 +98,52 @@ function checkForImages(markdownContent) {
// This function dynamically generates og:images for posts that don't have a featured image
export async function GET({ props }) {
const {post} = props;
- const ExternalImageURL = await getExternalImage(post);
- const hasImage = checkForImages(post.body);
+
+ // Generate consistent cache key
+ const cacheKey = `${post.slug}-${post.id}`;
+
+ // Check in-memory cache first (fastest)
+ if (ogImageCache.has(cacheKey)) {
+ return new Response(ogImageCache.get(cacheKey), {
+ headers: {
+ 'Content-Type': 'image/webp',
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ 'ETag': `"${cacheKey}"`
+ }
+ });
+ }
+
+ // Then check file cache (persists between server restarts)
+ const cachedFile = await fileCache.get(cacheKey);
+ if (cachedFile) {
+ // Store in memory cache for faster subsequent access
+ ogImageCache.set(cacheKey, cachedFile);
+
+ return new Response(cachedFile, {
+ headers: {
+ 'Content-Type': 'image/webp',
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ 'ETag': `"${cacheKey}"`,
+ 'X-Cache': 'HIT-FILE'
+ }
+ });
+ }
+
try {
- // Check if a custom cover image already exists
- if (post.data.cover || ExternalImageURL || hasImage) {
- // set it to empty to prevent the image generation
+ // Short-circuit early if we know we won't generate an image
+ if (post.data.cover) {
+ return new Response(null);
+ }
+
+ // Only fetch external image if needed
+ const ExternalImageURL = await getExternalImage(post);
+ if (ExternalImageURL) {
+ return new Response(null);
+ }
+
+ // Only check for images if needed
+ const hasImage = checkForImages(post.body);
+ if (hasImage) {
return new Response(null);
}
@@ -43,29 +151,44 @@ export async function GET({ props }) {
const width = 1280;
const height = 720;
- // Create a simple image with text
- const svg = `
-
- `;
+ // Sanitize text for SVG
+ const sanitizeText = (text) => {
+ return text
+ ? text.replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ : '';
+ };
- // Convert SVG to WebP
+ const title = sanitizeText(post.data.title);
+ const description = sanitizeText(post.data.description);
+
+ // Create a simple image with text - optimized SVG
+ const svg = ``;
+
+ // Convert SVG to WebP with optimized settings
const buffer = await sharp(Buffer.from(svg))
- .toFormat('webp')
+ .webp({ quality: 80, lossless: false })
.toBuffer();
- // Return the image
+ // Store in both memory and file caches
+ ogImageCache.set(cacheKey, buffer);
+ await fileCache.set(cacheKey, buffer);
+
+ // Return the image with proper caching headers
return new Response(buffer, {
headers: {
- 'Content-Type': 'image/png',
- 'Cache-Control': 'public, max-age=31536000'
+ 'Content-Type': 'image/webp',
+ 'Cache-Control': 'public, max-age=31536000, immutable',
+ 'ETag': `"${cacheKey}"`,
+ 'X-Cache': 'MISS'
}
});
} catch (error) {
console.error('Error generating image:', error);
return new Response('Error generating image', { status: 500 });
}
-}
\ No newline at end of file
+}
+