Skip to content

Commit d8b6b10

Browse files
committed
Precached images.
1 parent 743ead1 commit d8b6b10

File tree

7 files changed

+175
-448
lines changed

7 files changed

+175
-448
lines changed

.gitignore

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

astro.config.mjs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,6 @@ export default defineConfig({
7878
image: {
7979
remotePatterns: [{ protocol: "https" }],
8080
domains: ["programme.europython.eu", "placehold.co"],
81-
service: {
82-
entrypoint: "src/image.service.ts",
83-
},
8481
},
8582
experimental: {
8683
svg: true,

src/content/config.ts

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
11
import { defineCollection, reference, z } from "astro:content";
2+
import fs from "fs/promises";
3+
import path from "path";
4+
5+
const CACHE_DIR = ".cache/data";
6+
7+
async function ensureCacheDir() {
8+
await fs.mkdir(CACHE_DIR, { recursive: true });
9+
}
10+
11+
async function fetchWithCache(url: string, filename: string): Promise<Buffer> {
12+
const filePath = path.join(CACHE_DIR, filename);
13+
14+
try {
15+
// Return cached if available
16+
const data = await fs.readFile(filePath);
17+
console.log(`Fetch from cache: ${filePath}`);
18+
return data;
19+
} catch {
20+
const res = await fetch(url);
21+
if (!res.ok) throw new Error(`Failed to fetch: ${url}`);
22+
23+
const arrayBuffer = await res.arrayBuffer();
24+
const buffer: Buffer = Buffer.from(arrayBuffer);
25+
await fs.writeFile(filePath, new Uint8Array(buffer));
26+
return buffer;
27+
}
28+
}
229

330
const tiers = [
431
"Keystone",
@@ -54,26 +81,23 @@ const keynoters = defineCollection({
5481
}),
5582
});
5683

57-
// Cache for fetched data to prevent duplicate network requests
58-
let cachedSpeakersData: any = null;
59-
let cachedSessionsData: any = null;
60-
6184
// Shared data fetching function
6285
async function getCollectionsData() {
6386
// Only fetch if not already cached
64-
if (!cachedSpeakersData || !cachedSessionsData) {
65-
const [speakersResponse, sessionsResponse] = await Promise.all([
66-
fetch(
67-
"https://gist.github.com/egeakman/469f9abb23a787df16d8787f438dfdb6/raw/62d2b7e77c1b078a0e27578c72598a505f9fafbf/speakers.json"
68-
),
69-
fetch(
70-
"https://gist.githubusercontent.com/egeakman/eddfb15f32ae805e8cfb4c5856ae304b/raw/466f8c20c17a9f6c5875f973acaec60e4e4d0fae/sessions.json"
71-
),
72-
]);
73-
74-
cachedSpeakersData = await speakersResponse.json();
75-
cachedSessionsData = await sessionsResponse.json();
76-
}
87+
await ensureCacheDir();
88+
89+
const speakersBuffer = await fetchWithCache(
90+
"https://gist.github.com/egeakman/469f9abb23a787df16d8787f438dfdb6/raw/62d2b7e77c1b078a0e27578c72598a505f9fafbf/speakers.json",
91+
"speakers.json"
92+
);
93+
94+
const sessionsBuffer = await fetchWithCache(
95+
"https://gist.githubusercontent.com/egeakman/eddfb15f32ae805e8cfb4c5856ae304b/raw/466f8c20c17a9f6c5875f973acaec60e4e4d0fae/sessions.json",
96+
"sessions.json"
97+
);
98+
99+
const cachedSpeakersData = JSON.parse(speakersBuffer.toString("utf-8"));
100+
const cachedSessionsData = JSON.parse(sessionsBuffer.toString("utf-8"));
77101

78102
// Create indexed versions for efficient lookups
79103
const speakersById = Object.entries(cachedSpeakersData).reduce(

src/image-cache.ts

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,86 @@ import fs from "fs";
22
import Sharp from "sharp";
33
import * as crypto from "crypto";
44

5-
export async function getCachedImage(image_url: string): Promise<string> {
6-
const directory = "./src/assets/cache";
7-
const name = crypto.createHash("md5").update(image_url).digest("hex");
8-
const filename = `${name}.webp`;
9-
const path = `${directory}/${filename}`;
10-
console.log(`Cache ${path}`);
5+
const MAX_RETRIES = 3;
6+
const TIMEOUT_MS = 5000;
7+
8+
async function fetchWithTimeout(
9+
url: string,
10+
timeout: number,
11+
retryCount = 0
12+
): Promise<Response> {
13+
const controller = new AbortController();
14+
const timeoutId = setTimeout(() => controller.abort(), timeout);
1115

1216
try {
13-
if (!fs.existsSync(path)) {
14-
if (!fs.existsSync(directory)) {
15-
fs.mkdirSync(directory, { recursive: true });
16-
}
17+
const response = await fetch(url, {
18+
signal: controller.signal,
19+
headers: {
20+
"User-Agent": "Mozilla/5.0 (compatible; AstroImageFetcher/1.0)",
21+
Accept: "image/*",
22+
},
23+
});
24+
clearTimeout(timeoutId);
25+
26+
if (!response.ok) {
27+
throw new Error(`HTTP ${response.status} - ${response.statusText}`);
28+
}
29+
30+
return response;
31+
} catch (error) {
32+
clearTimeout(timeoutId);
33+
34+
if (retryCount < MAX_RETRIES) {
35+
console.warn(
36+
`Retrying fetch for ${url} (attempt ${retryCount + 1}/${MAX_RETRIES})...`
37+
);
38+
const backoffDelay = Math.pow(2, retryCount) * 1000;
39+
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
40+
return fetchWithTimeout(url, timeout, retryCount + 1);
41+
}
42+
43+
console.error(`Failed to fetch ${url}:`, error);
44+
throw error;
45+
}
46+
}
47+
48+
export async function getCachedImage(imageUrl: string): Promise<string> {
49+
// Skip SVG files
50+
if (imageUrl.trim().toLowerCase().endsWith(".svg")) {
51+
console.log(`Skipping SVG: ${imageUrl}`);
52+
return imageUrl;
53+
}
1754

18-
const response = await fetch(image_url);
19-
if (!response.ok) {
20-
throw new Error(
21-
`Failed to fetch image: ${response.status} ${response.statusText}`
22-
);
55+
if (imageUrl === "") {
56+
console.log(`Skipping empty URL: ${imageUrl}`);
57+
return imageUrl;
58+
}
59+
60+
const cacheDirectory = "./public/cache";
61+
const imageHash = crypto.createHash("md5").update(imageUrl).digest("hex");
62+
const imageFilename = `${imageHash}.jpg`;
63+
const cachedImagePath = `${cacheDirectory}/${imageFilename}`;
64+
const publicImagePath = `${"/cache"}/${imageFilename}`;
65+
66+
try {
67+
if (!fs.existsSync(cachedImagePath)) {
68+
if (!fs.existsSync(cacheDirectory)) {
69+
fs.mkdirSync(cacheDirectory, { recursive: true });
2370
}
2471

72+
const response = await fetchWithTimeout(imageUrl, TIMEOUT_MS);
2573
const arrayBuffer = await response.arrayBuffer();
26-
const buffer = Buffer.from(arrayBuffer);
74+
const imageBuffer = Buffer.from(arrayBuffer);
2775

2876
try {
29-
await Sharp(buffer).toFile(path);
77+
await Sharp(imageBuffer).toFile(cachedImagePath);
3078
} catch (err) {
3179
console.error("Error converting image with Sharp:", err);
3280
throw new Error("Image conversion failed");
3381
}
3482
}
3583

36-
const url = path.startsWith("./") ? path.slice(1) : path;
37-
return url;
84+
return publicImagePath;
3885
} catch (err) {
3986
console.error("Error in getCachedImage:", err);
4087
throw err;

0 commit comments

Comments
 (0)