Repository navigation
Expand file tree
/
Copy pathmod.ts
More file actions
152 lines (126 loc) · 5.36 KB
/
Copy pathmod.ts
File metadata and controls
152 lines (126 loc) · 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
* Steno plugin for bulk image optimization, resizing, and automatic HTML lazy-loading injection.
*/
import type { SiteConfig, StenoPlugin } from "steno";
import { basename, dirname, extname, join } from "@std/path";
import { walk } from "@std/fs";
import sharp from "sharp";
/** Options for generated image variants and HTML image enhancement. */
export interface ImagePluginOptions {
/** Target widths generated for images large enough to support them. */
widths?: number[];
/** Output formats generated at each target width. */
formats?: Array<"webp" | "jpeg" | "png">;
/** Encoder quality from 1 to 100. */
quality?: number;
/** Whether to add dimensions, lazy loading, and an LQIP to local images. */
injectLazyLoad?: boolean;
}
/** Creates a Steno plugin that optimizes images in the generated site. */
export default function imagePlugin(
options: ImagePluginOptions = {},
): StenoPlugin {
const widths = options.widths ?? [640, 1024, 1280];
const formats = options.formats ?? ["webp"];
const quality = options.quality ?? 80;
const injectLazyLoad = options.injectLazyLoad ?? true;
const SUPPORTED_EXTENSIONS = [".jpg", ".jpeg", ".png"];
return {
name: "steno-plugin-image",
async afterBuild(config: SiteConfig): Promise<void> {
const outputDir = config.output ?? "dist";
const imageMetadataMap = new Map<
string,
{ width: number; height: number; lqip?: string }
>();
// 1. Process and Optimize Physical Images
for await (const entry of walk(outputDir, { includeDirs: false })) {
const ext = extname(entry.path).toLowerCase();
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
continue;
}
const dir = dirname(entry.path);
const name = basename(entry.path, ext);
const imageBuffer = await Deno.readFile(entry.path);
const metadata = await sharp(imageBuffer).metadata();
if (!metadata.width || !metadata.height) continue;
// Cache metadata for HTML injection phase
const relativePath = entry.path.replace(outputDir, "").replace(
/\\/g,
"/",
);
// Generate a tiny Base64 LQIP (16px wide) for instant loading preview
const lqipBuffer = await sharp(imageBuffer)
.resize(16)
.blur(1)
.toBuffer();
const lqipBase64 = `data:image/${ext.slice(1)};base64,${
lqipBuffer.toString("base64")
}`;
imageMetadataMap.set(relativePath, {
width: metadata.width,
height: metadata.height,
lqip: lqipBase64,
});
// Write optimized files (widths & formats)
for (const format of formats) {
for (const width of widths) {
if (width > metadata.width) continue;
const outputFilename = `${name}-${width}.${format}`;
const outputPath = join(dir, outputFilename);
let transform = sharp(imageBuffer).resize({ width });
if (format === "webp") {
transform = transform.webp({ quality });
} else if (format === "jpeg") {
transform = transform.jpeg({ quality, mozjpeg: true });
} else if (format === "png") {
transform = transform.png({ quality });
}
const processedBuffer = await transform.toBuffer();
await Deno.writeFile(outputPath, processedBuffer);
}
}
}
// 2. Scan and Inject lazy-loading attributes into HTML files
if (injectLazyLoad) {
for await (const entry of walk(outputDir, { includeDirs: false })) {
if (extname(entry.path).toLowerCase() !== ".html") {
continue;
}
let htmlContent = await Deno.readTextFile(entry.path);
let modified = false;
// Regex to match <img> tags
const imgRegex = /<img\s+([^>]*?)src="([^"]+?)"([^>]*?)>/gi;
htmlContent = htmlContent.replace(
imgRegex,
(match, before, src, after) => {
// Normalize path to map to our metadata registry
const cleanSrc = src.startsWith("/") ? src : `/${src}`;
const meta = imageMetadataMap.get(cleanSrc);
if (!meta) return match; // Skip if we don't have matching local metadata
// Check if attributes are already defined to prevent duplicates
const hasWidth = /width=/i.test(match);
const hasHeight = /height=/i.test(match);
const hasLoading = /loading=/i.test(match);
let extraAttrs = "";
if (!hasWidth) extraAttrs += ` width="${meta.width}"`;
if (!hasHeight) extraAttrs += ` height="${meta.height}"`;
if (!hasLoading) extraAttrs += ` loading="lazy"`;
// Apply low-quality blur-up style if LQIP is available
const hasStyle = /style=/i.test(match);
if (!hasStyle && meta.lqip) {
extraAttrs +=
` style="background-image: url('${meta.lqip}'); background-size: cover; filter: blur(4px); transition: filter 0.3s;" onload="this.style.filter='none'"`;
}
modified = true;
return `<img ${before}src="${src}"${after}${extraAttrs}>`;
},
);
if (modified) {
await Deno.writeTextFile(entry.path, htmlContent);
}
}
}
},
};
}