Skip to content

Commit 0d72a86

Browse files
committed
feat: dynamic static images
1 parent f77ad0c commit 0d72a86

9 files changed

Lines changed: 252 additions & 64 deletions

File tree

.github/workflows/nextjs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
- name: Install dependencies
9595
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
9696
- name: Build with Next.js
97-
run: ${{ steps.detect-package-manager.outputs.runner }} next build
97+
run: ${{ steps.detect-package-manager.outputs.manager }} run build
9898
- name: Upload artifact
9999
uses: actions/upload-pages-artifact@v5
100100
with:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ yarn-error.log*
4141
next-env.d.ts
4242

4343
/src/generated/prisma
44+
/public/optimized/

next.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const isGitHubPages = process.env.GITHUB_PAGES === 'true';
66
const nextConfig: NextConfig = {
77
output: 'export',
88
images: {
9-
unoptimized: true,
9+
loader: 'custom',
10+
loaderFile: './src/lib/image-loader.ts',
1011
},
1112
basePath: isProd && isGitHubPages ? '/preferred-ai-nextjs' : '',
1213
assetPrefix: isProd && isGitHubPages ? '/preferred-ai-nextjs' : '',

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
7-
"build": "next build",
6+
"dev": "npx tsx scripts/optimize-all-images.ts && next dev",
7+
"build": "npx tsx scripts/optimize-all-images.ts && next build",
88
"start": "next start",
99
"lint": "eslint",
10-
"export": "next build",
10+
"export": "npx tsx scripts/optimize-all-images.ts && next build",
1111
"crawl:publications": "npx tsx scripts/crawl-publications.ts"
1212
},
1313
"dependencies": {

scripts/optimize-all-images.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import sharp from "sharp";
4+
5+
const TARGET_WIDTHS = [256, 384, 640, 1080, 1920];
6+
const PUBLIC_DIR = path.join(process.cwd(), "public");
7+
const OPTIMIZED_DIR = path.join(PUBLIC_DIR, "optimized");
8+
9+
// Directories to scan recursively for images
10+
const DIRS_TO_SCAN = ["team", "uploads"];
11+
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png"];
12+
13+
function getFilesRecursively(dir: string, fileList: string[] = []): string[] {
14+
const absoluteDir = path.join(PUBLIC_DIR, dir);
15+
if (!fs.existsSync(absoluteDir)) return fileList;
16+
17+
const files = fs.readdirSync(absoluteDir);
18+
for (const file of files) {
19+
const relativePath = path.join(dir, file);
20+
const absolutePath = path.join(PUBLIC_DIR, relativePath);
21+
if (fs.statSync(absolutePath).isDirectory()) {
22+
getFilesRecursively(relativePath, fileList);
23+
} else {
24+
const ext = path.extname(file).toLowerCase();
25+
if (IMAGE_EXTENSIONS.includes(ext)) {
26+
fileList.push(relativePath);
27+
}
28+
}
29+
}
30+
return fileList;
31+
}
32+
33+
async function optimizeImage(srcRelativePath: string) {
34+
const srcAbsolutePath = path.join(PUBLIC_DIR, srcRelativePath);
35+
const srcStats = fs.statSync(srcAbsolutePath);
36+
const srcMtime = srcStats.mtimeMs;
37+
38+
for (const width of TARGET_WIDTHS) {
39+
const destRelativePath = path.join("optimized", String(width), srcRelativePath);
40+
const destAbsolutePath = path.join(PUBLIC_DIR, destRelativePath);
41+
42+
// Incremental cache check: Skip if optimized file exists and is newer than source
43+
if (fs.existsSync(destAbsolutePath)) {
44+
const destStats = fs.statSync(destAbsolutePath);
45+
if (destStats.mtimeMs >= srcMtime) {
46+
continue;
47+
}
48+
}
49+
50+
// Create target directory if it doesn't exist
51+
const destDir = path.dirname(destAbsolutePath);
52+
fs.mkdirSync(destDir, { recursive: true });
53+
54+
try {
55+
const ext = path.extname(srcRelativePath).toLowerCase();
56+
const image = sharp(srcAbsolutePath);
57+
58+
// Perform high-quality resizing
59+
// withoutEnlargement: true ensures we don't upscale small original images
60+
let transformer = image.resize({ width, withoutEnlargement: true });
61+
62+
if (ext === ".png") {
63+
transformer = transformer.png({ compressionLevel: 8, palette: true });
64+
} else {
65+
transformer = transformer.jpeg({ quality: 80, progressive: true });
66+
}
67+
68+
await transformer.toFile(destAbsolutePath);
69+
console.log(`✅ Optimized: /${srcRelativePath} ➡️ /${destRelativePath}`);
70+
} catch (error) {
71+
console.error(`❌ Failed to optimize /${srcRelativePath} at width ${width}:`, error);
72+
}
73+
}
74+
}
75+
76+
async function main() {
77+
console.log("🚀 Scanning public directories for image optimization...");
78+
let allImages: string[] = [];
79+
for (const dir of DIRS_TO_SCAN) {
80+
allImages = allImages.concat(getFilesRecursively(dir));
81+
}
82+
83+
console.log(`🔍 Found ${allImages.length} images to optimize.`);
84+
85+
const startTime = Date.now();
86+
for (const relativePath of allImages) {
87+
await optimizeImage(relativePath);
88+
}
89+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
90+
console.log(`🎉 Image optimization pipeline finished in ${duration}s.`);
91+
}
92+
93+
main().catch((err) => {
94+
console.error("FATAL: Image optimization script failed:", err);
95+
process.exit(1);
96+
});

0 commit comments

Comments
 (0)