Skip to content

Commit fa9f995

Browse files
committed
Add image prefetch and cache.
1 parent 4f78c7a commit fa9f995

File tree

3 files changed

+123
-1
lines changed

3 files changed

+123
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,4 @@ src/content/days
147147
src/data/speakers.json
148148
src/data/sessions.json
149149
src/data/schedule.json
150+
src/assets/cache

src/image-cache.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import Sharp from "sharp";
4+
import * as crypto from "crypto";
5+
6+
interface CacheOptions {
7+
directory?: string;
8+
format?: "webp" | "png" | "jpeg";
9+
quality?: number;
10+
width?: number;
11+
height?: number;
12+
}
13+
14+
/**
15+
* Fetches and caches an image from a URL, returning the local path
16+
* @param imageUrl The URL of the image to fetch and cache
17+
* @param options Configuration options for caching
18+
* @returns Path to the cached image
19+
*/
20+
export async function getCachedImage(
21+
imageUrl: string,
22+
options: CacheOptions = {}
23+
): Promise<string> {
24+
if (!imageUrl || typeof imageUrl !== "string") {
25+
throw new Error("Invalid image URL provided");
26+
}
27+
28+
// Default options
29+
const directory = options.directory || "./src/assets/cache";
30+
const format = options.format || "webp";
31+
const quality = options.quality || 80;
32+
33+
// Create hash from URL for the filename
34+
const hash = crypto.createHash("md5").update(imageUrl).digest("hex");
35+
const filename = `${hash}.${format}`;
36+
const cachePath = path.join(directory, filename);
37+
38+
console.log(`Cache: ${cachePath}`);
39+
40+
try {
41+
// Create cache directory if it doesn't exist
42+
if (!fs.existsSync(directory)) {
43+
fs.mkdirSync(directory, { recursive: true });
44+
}
45+
46+
// Check if file exists and is valid
47+
if (fs.existsSync(cachePath)) {
48+
try {
49+
// Verify file is valid by trying to read its metadata
50+
await Sharp(cachePath).metadata();
51+
// If we get here, the file is valid
52+
} catch (error) {
53+
console.warn(
54+
`Invalid cached image detected, will re-download: ${cachePath}`
55+
);
56+
// Delete the corrupted file
57+
fs.unlinkSync(cachePath);
58+
}
59+
}
60+
61+
// Download and process the image if needed
62+
if (!fs.existsSync(cachePath)) {
63+
const response = await fetch(imageUrl);
64+
65+
if (!response.ok) {
66+
throw new Error(
67+
`Failed to fetch image: ${response.status} ${response.statusText}`
68+
);
69+
}
70+
71+
const buffer = Buffer.from(await response.arrayBuffer());
72+
73+
// Process with Sharp
74+
let sharpInstance = Sharp(buffer);
75+
76+
// Apply resizing if requested
77+
if (options.width || options.height) {
78+
sharpInstance = sharpInstance.resize({
79+
width: options.width,
80+
height: options.height,
81+
fit: "inside",
82+
withoutEnlargement: true,
83+
});
84+
}
85+
86+
// Convert to requested format with quality setting
87+
switch (format) {
88+
case "webp":
89+
sharpInstance = sharpInstance.webp({ quality });
90+
break;
91+
case "png":
92+
sharpInstance = sharpInstance.png({ quality });
93+
break;
94+
case "jpeg":
95+
sharpInstance = sharpInstance.jpeg({ quality });
96+
break;
97+
}
98+
99+
// Save to file
100+
await sharpInstance.toFile(cachePath);
101+
102+
// Verify the file was written correctly
103+
await Sharp(cachePath).metadata();
104+
}
105+
106+
// Return path relative to project root
107+
return cachePath.startsWith("./") ? cachePath.slice(1) : cachePath;
108+
} catch (error) {
109+
console.error(`Error caching image ${imageUrl}:`, error);
110+
if (error instanceof Error) {
111+
throw new Error(`Failed to cache image: ${error.message}`);
112+
} else {
113+
throw new Error(`Failed to cache image: ${String(error)}`);
114+
}
115+
}
116+
}

src/pages/speaker/[slug].astro

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Picture, getImage } from "astro:assets";
66
import Markdown from "@ui/Markdown.astro";
77
import Headline from "@ui/Headline.astro";
88
import Placeholder from "@assets/placeholder.png";
9+
import { getCachedImage } from "../../image-cache";
910
1011
export async function getStaticPaths() {
1112
const entries = await getCollection("speakers");
@@ -19,9 +20,13 @@ const {entry} = Astro.props;
1920
2021
let avatar: any;
2122
23+
2224
if (entry.data.avatar){
25+
2326
try {
24-
avatar = await getImage({ src: entry.data.avatar, inferSize: true, format: "webp", alt: 'User avatar' });
27+
const url = await getCachedImage(entry.data.avatar);
28+
const image = import(/* @vite-ignore */url);
29+
avatar = await getImage({ src: image, inferSize: true, format: "webp", alt: 'User avatar' });
2530
} catch (e) {
2631
//TODO: improve placeholders and offline
2732
//avatar = await getImage({ src: 'https://placehold.co/600x400?text=x', width: '600', height:'400', alt: 'Default avatar' });

0 commit comments

Comments
 (0)