Skip to content

Commit 42f1e55

Browse files
committed
Add local image service.
1 parent c2caacf commit 42f1e55

File tree

4 files changed

+129
-7
lines changed

4 files changed

+129
-7
lines changed

astro.config.mjs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path";
2-
import { defineConfig, passthroughImageService } from "astro/config";
2+
import { defineConfig } from "astro/config";
33
import mdx from "@astrojs/mdx";
44
import sitemap from "@astrojs/sitemap";
55
import tailwind from "@astrojs/tailwind";
@@ -11,6 +11,7 @@ import pagefind from "astro-pagefind";
1111
import deleteUnusedImages from "astro-delete-unused-images";
1212
import preload from "astro-preload";
1313
import { execSync } from "node:child_process";
14+
import safeRemoteService from "./src/services/safe-remote.ts";
1415

1516
let gitVersion = String(process.env.GIT_VERSION ?? "").slice(0, 7);
1617

@@ -88,9 +89,6 @@ export default defineConfig({
8889
metaTags(),
8990
pagefind(),
9091
deleteUnusedImages(),
91-
(await import("astro-compress")).default({
92-
SVG: false,
93-
}),
9492
],
9593
output: "static",
9694
build: {
@@ -99,6 +97,6 @@ export default defineConfig({
9997
image: {
10098
remotePatterns: [{ protocol: "https" }],
10199
domains: ["programme.europython.eu", "placehold.co"],
102-
service: passthroughImageService(),
100+
service: safeRemoteService,
103101
},
104102
});

src/components/sections/speakers.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
import { getCollection } from "astro:content";
33
import type { CollectionEntry } from "astro:content";
4-
import Image from "@ui/SafeImage.astro";
4+
import { Image } from "astro:assets";
55
import Section from "@ui/Section.astro"
66
77
const allSpeakers = await getCollection("speakers");

src/pages/speaker/[slug].astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { getCollection, getEntries } from "astro:content";
33
import Layout from "@layouts/Layout.astro";
44
import Prose from "@ui/Prose.astro";
5-
import Image from "@ui/SafeImage.astro";
5+
import { Image } from "astro:assets";
66
import Markdown from "@ui/Markdown.astro";
77
import Headline from "@ui/Headline.astro";
88
import Section from "@ui/Section.astro"

src/services/safe-remote.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { LocalImageService, AstroConfig, ImageTransform } from "astro";
2+
import sharp from "sharp";
3+
import fs from "node:fs/promises";
4+
import path from "node:path";
5+
6+
type ImageFormat = "webp" | "png" | "jpeg" | "avif";
7+
8+
const PLACEHOLDER_PATH = path.resolve("public/images/placeholder.png");
9+
10+
async function getPlaceholderImage(): Promise<Uint8Array> {
11+
const buffer = await fs.readFile(PLACEHOLDER_PATH);
12+
return new Uint8Array(buffer);
13+
}
14+
15+
async function encodesImages(options: {
16+
buffer: Uint8Array;
17+
width?: number;
18+
height?: number;
19+
format?: ImageFormat;
20+
quality?: number;
21+
}): Promise<{ buffer: Uint8Array }> {
22+
const { buffer, width, height, format = "webp", quality = 80 } = options;
23+
24+
const output = await sharp(buffer)
25+
.resize(width, height)
26+
.toFormat(format, { quality })
27+
.toBuffer();
28+
29+
return {
30+
buffer: output as unknown as Uint8Array,
31+
};
32+
}
33+
34+
const service: LocalImageService = {
35+
getURL(options: ImageTransform, _imageConfig: AstroConfig["image"]) {
36+
const searchParams = new URLSearchParams();
37+
searchParams.append(
38+
"href",
39+
typeof options.src === "string" ? options.src : options.src.src
40+
);
41+
options.width && searchParams.append("w", options.width.toString());
42+
options.height && searchParams.append("h", options.height.toString());
43+
options.quality && searchParams.append("q", options.quality.toString());
44+
options.format && searchParams.append("f", options.format);
45+
return `/_image?${searchParams}`;
46+
},
47+
48+
parseURL(url: URL, _imageConfig) {
49+
const params = url.searchParams;
50+
return {
51+
src: params.get("href")!,
52+
width: params.has("w") ? parseInt(params.get("w")!) : undefined,
53+
height: params.has("h") ? parseInt(params.get("h")!) : undefined,
54+
format: params.get("f") as ImageFormat | undefined,
55+
quality: params.has("q") ? parseInt(params.get("q")!) : undefined,
56+
};
57+
},
58+
59+
async transform(
60+
buffer: Uint8Array,
61+
options: {
62+
src: string;
63+
width?: number;
64+
height?: number;
65+
format?: ImageFormat;
66+
quality?: number;
67+
},
68+
_imageConfig
69+
) {
70+
const { width, height, format, quality } = options;
71+
72+
try {
73+
const result = await encodesImages({
74+
buffer,
75+
width,
76+
height,
77+
format,
78+
quality,
79+
});
80+
81+
return {
82+
data: result.buffer,
83+
format: format ?? "webp",
84+
};
85+
} catch (err) {
86+
console.error(
87+
`Image transform failed for ${options.src}. Returning placeholder.`,
88+
err
89+
);
90+
const fallbackBuffer = await getPlaceholderImage();
91+
return {
92+
data: fallbackBuffer,
93+
format: "png", // Must match actual placeholder format
94+
};
95+
}
96+
},
97+
98+
getHTMLAttributes(options, _imageConfig) {
99+
let targetWidth = options.width;
100+
let targetHeight = options.height;
101+
if (typeof options.src === "object") {
102+
const aspectRatio = options.src.width / options.src.height;
103+
if (targetHeight && !targetWidth) {
104+
targetWidth = Math.round(targetHeight * aspectRatio);
105+
} else if (targetWidth && !targetHeight) {
106+
targetHeight = Math.round(targetWidth / aspectRatio);
107+
}
108+
}
109+
110+
const { src, width, height, format, quality, ...attributes } = options;
111+
112+
return {
113+
...attributes,
114+
width: targetWidth,
115+
height: targetHeight,
116+
loading: attributes.loading ?? "lazy",
117+
decoding: attributes.decoding ?? "async",
118+
};
119+
},
120+
121+
propertiesToHash: ["src", "width", "height", "format", "quality"],
122+
};
123+
124+
export default service;

0 commit comments

Comments
 (0)