Skip to content

Commit 60c43d4

Browse files
committed
feat: og-images: Implement caching for OG image generation
This commit introduces caching to the OG image generation process to improve build times. It uses hashes of content and assets to determine when to regenerate images, skipping unchanged ones. Stale images are also removed.
1 parent 53807ba commit 60c43d4

File tree

5 files changed

+97
-31
lines changed

5 files changed

+97
-31
lines changed

.github/workflows/main.yml

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Deploy Hugo site to Pages
22

33
on:
44
push:
5-
branches: ["main", "design-adjustments"]
5+
branches: ["main", "cache-images"]
66

77
permissions:
88
contents: read
@@ -17,23 +17,19 @@ env:
1717

1818
jobs:
1919
build:
20-
# Switched to a standard x86_64 runner for better package compatibility
2120
runs-on: ubuntu-24.04
2221
steps:
2322
- name: Install Hugo
2423
run: |
25-
# Updated to use amd64 for the new runner
2624
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
2725
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
2826
2927
- name: Install Go
3028
run: |
31-
# Updated to use amd64 for the new runner
3229
wget -O ${{ runner.temp }}/go.tar.gz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz \
3330
&& sudo tar -C /usr/local -xzf ${{ runner.temp }}/go.tar.gz \
3431
&& sudo ln -s /usr/local/go/bin/go /usr/local/bin/go
3532
36-
3733
- name: Checkout
3834
uses: actions/checkout@v4.2.2
3935
with:
@@ -44,15 +40,29 @@ jobs:
4440
id: pages
4541
uses: actions/configure-pages@v5
4642

47-
# Best practice: Use setup-node with caching
4843
- name: Setup Node.js
4944
uses: actions/setup-node@v4
5045
with:
5146
node-version: ${{ env.NODE_VERSION }}
5247
cache: 'npm'
5348

5449
- name: Install npm dependencies
55-
run: npm ci # Use npm ci for faster, more reliable CI builds
50+
run: npm ci
51+
52+
- name: Cache and Restore OG Images
53+
uses: actions/cache@v4
54+
id: og-cache
55+
with:
56+
path: |
57+
content/**/*-og.jpg
58+
static/images/og-image.jpg
59+
tmp/og-cache-manifest.json
60+
key: ${{ runner.os }}-og-images-${{ hashFiles('**/content/**/index.md', '**/content/**/_index.md', 'assets/og-template/template.html', 'assets/images/ONM-logo.png') }}
61+
restore-keys: |
62+
${{ runner.os }}-og-images-
63+
64+
- name: Generate OG Images
65+
run: npm run og-images
5666

5767
- name: Determine Base URL
5868
id: base_url
@@ -63,7 +73,6 @@ jobs:
6373
BASE_URL="https://open-neuromorphic.github.io/refactor-preview/" # Example preview URL
6474
else
6575
REPO_NAME=$(echo "${{ github.repository }}" | cut -d '/' -f 2)
66-
# For forks or other repositories, adjust as needed
6776
BASE_URL="https://${{ github.repository_owner }}.github.io/${REPO_NAME}/"
6877
fi
6978
echo "BASE_URL=$BASE_URL" >> $GITHUB_ENV
@@ -75,12 +84,12 @@ jobs:
7584
sed -i "s|baseURL = .*|baseURL = \"$BASE_URL\"|" hugo.toml
7685
echo "hugo.toml after modification:"
7786
cat hugo.toml
78-
79-
- name: Build site
87+
88+
- name: Build Hugo Site
8089
run: |
81-
echo "Starting site build..."
82-
npm run build
83-
echo "Site build completed."
90+
echo "Starting Hugo site build..."
91+
npm run hugo-build
92+
echo "Hugo site build completed."
8493
echo "Contents of public directory after build:"
8594
ls -la public
8695
@@ -132,7 +141,6 @@ jobs:
132141
environment:
133142
name: github-pages
134143
url: ${{ steps.deployment.outputs.page_url }}
135-
# Switched to a standard x86_64 runner for consistency
136144
runs-on: ubuntu-24.04
137145
needs: build
138146
steps:

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ yarn.lock
2020
/tmp/ogImageData.json
2121
/tmp/output_full.txt
2222
/tmp/
23+
24+
# Ignore generated OG images and cache files
25+
content/**/*-og.jpg
26+
static/images/og-image.jpg
27+
tmp/og-cache-manifest.json

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"author": "VisionInit.dev",
77
"scripts": {
88
"dev": "hugo serve --buildFuture",
9-
"build": "npm run og-images && rm public -fr && hugo --gc --minify --buildFuture --templateMetrics --templateMetricsHints --forceSyncStatic -e production",
9+
"hugo-build": "hugo --gc --minify --buildFuture --templateMetrics --templateMetricsHints --forceSyncStatic -e production",
10+
"build": "npm run og-images && npm run hugo-build",
1011
"build-preview": "hugo server --disableFastRender --buildFuture --navigateToChanged --templateMetrics --templateMetricsHints --forceSyncStatic -e production --minify",
1112
"og-images": "node scripts/collectOgData.js && node scripts/generateOgImages.js"
1213
},

scripts/collectOgData.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const { join, dirname, basename } = require('path');
33
const { readdir, readFile, stat, mkdir, writeFile } = require('fs/promises');
44
const mimeTypes = require('mime-types');
5+
const crypto = require('crypto');
56

67
// --- Configuration ---
78
const PROJECT_ROOT = process.cwd();
@@ -12,7 +13,8 @@ const TMP_DIR = join(PROJECT_ROOT, 'tmp');
1213
const OUTPUT_JSON_PATH = join(TMP_DIR, 'ogImageData.json');
1314
const OUTPUT_FORMAT = 'jpg';
1415
const LOGO_PATH_IN_ASSETS = 'images/ONM-logo.png';
15-
const BACKGROUND_IMAGE_PATH_IN_ASSETS = 'images/ONM.png'; // Added this line
16+
const BACKGROUND_IMAGE_PATH_IN_ASSETS = 'images/ONM.png';
17+
const OG_TEMPLATE_PATH = join(PROJECT_ROOT, 'assets', 'og-template', 'template.html');
1618
const HOMEPAGE_TITLE = "Advancing Neuromorphic Computing, Together.";
1719
const HOMEPAGE_DESCRIPTION = "Open Neuromorphic (ONM) is a global community fostering education, research, and open-source collaboration in brain-inspired AI and hardware.";
1820

@@ -30,6 +32,10 @@ async function ensureDir(dirPath) {
3032
catch (err) { if (err.code !== 'EEXIST') throw err; }
3133
}
3234

35+
function createHash(data) {
36+
return crypto.createHash('sha256').update(data).digest('hex');
37+
}
38+
3339
async function findMarkdownFiles(dir) {
3440
let entries;
3541
try { entries = await readdir(dir); } catch (err) { console.warn(`Could not read directory ${dir}: ${err.message}`); return []; }
@@ -83,37 +89,41 @@ async function getImageDataUri(filePath) {
8389

8490
// --- Main Script ---
8591
async function collectData() {
86-
console.log('📊 Collecting OG image data...');
92+
console.log('📊 Collecting OG image data and calculating hashes...');
8793
await ensureDir(TMP_DIR);
8894

95+
const templateBuffer = await readFile(OG_TEMPLATE_PATH);
96+
const logoBuffer = await readFile(join(ASSETS_DIR, LOGO_PATH_IN_ASSETS));
97+
const backgroundBuffer = await readFile(join(ASSETS_DIR, BACKGROUND_IMAGE_PATH_IN_ASSETS));
98+
const globalHash = createHash(Buffer.concat([templateBuffer, logoBuffer, backgroundBuffer]));
99+
89100
const absoluteLogoPath = join(ASSETS_DIR, LOGO_PATH_IN_ASSETS);
90101
const logoDataUri = await getImageDataUri(absoluteLogoPath);
91102
if (!logoDataUri) {
92103
console.error(`❌ Critical: Could not load logo from ${absoluteLogoPath}. Cannot proceed.`);
93104
process.exit(1);
94105
}
95106

96-
// --- Add logic for background image ---
97107
const absoluteBackgroundPath = join(ASSETS_DIR, BACKGROUND_IMAGE_PATH_IN_ASSETS);
98108
const backgroundDataUri = await getImageDataUri(absoluteBackgroundPath);
99-
if (!backgroundDataUri) {
100-
console.warn(`⚠️ Background image not found at ${absoluteBackgroundPath}, will proceed without it.`);
101-
}
102109

103110
const outputData = {
111+
globalHash,
104112
logoDataUri,
105-
backgroundDataUri: backgroundDataUri || '', // Add background URI
113+
backgroundDataUri: backgroundDataUri || '',
106114
pages: [],
107115
};
108116

109117
// 1. Add Homepage Data
118+
const homepageContentHash = createHash(HOMEPAGE_TITLE + HOMEPAGE_DESCRIPTION);
119+
const homepageFinalHash = createHash(homepageContentHash + globalHash);
110120
const homepageOgDir = join(STATIC_DIR, 'images');
111121
await ensureDir(homepageOgDir);
112122
outputData.pages.push({
113123
title: HOMEPAGE_TITLE,
114124
description: HOMEPAGE_DESCRIPTION,
115125
outputPath: join(homepageOgDir, `og-image.${OUTPUT_FORMAT}`),
116-
tempHtmlPath: join(TMP_DIR, `homepage-temp-og.html`),
126+
finalHash: homepageFinalHash
117127
});
118128

119129
// 2. Process Content Pages
@@ -128,14 +138,17 @@ async function collectData() {
128138
continue;
129139
}
130140

141+
const contentHash = createHash(title + description);
142+
const finalHash = createHash(contentHash + globalHash);
143+
131144
const parentDirName = basename(pageDirectory);
132145
const ogImageFilename = `${parentDirName}-og.${OUTPUT_FORMAT}`;
133146

134147
outputData.pages.push({
135148
title,
136149
description,
137150
outputPath: join(pageDirectory, ogImageFilename),
138-
tempHtmlPath: join(TMP_DIR, `${parentDirName}-${Date.now()}-temp-og.html`),
151+
finalHash
139152
});
140153
} catch (err) {
141154
console.error(`❌ Error processing ${mdFile}: ${err.message}`);

scripts/generateOgImages.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// scripts/generateOgImages.js
12
const puppeteer = require('puppeteer');
23
const { join } = require('path');
34
const { readFile, writeFile, unlink, stat } = require('fs/promises');
@@ -7,14 +8,15 @@ const PROJECT_ROOT = process.cwd();
78
const TMP_DIR = join(PROJECT_ROOT, 'tmp');
89
const TEMPLATE_PATH = join(PROJECT_ROOT, 'assets', 'og-template', 'template.html');
910
const INPUT_JSON_PATH = join(TMP_DIR, 'ogImageData.json');
11+
const CACHE_MANIFEST_PATH = join(TMP_DIR, 'og-cache-manifest.json');
1012
const JPEG_QUALITY = 90;
1113

1214
async function pathExists(path) {
1315
try { await stat(path); return true; } catch { return false; }
1416
}
1517

1618
async function generateImages() {
17-
console.log('🖼️ Starting OG Image Generation with Puppeteer...');
19+
console.log('🖼️ Starting OG Image Generation with Caching...');
1820

1921
// --- Pre-flight checks ---
2022
if (!(await pathExists(INPUT_JSON_PATH))) {
@@ -24,9 +26,18 @@ async function generateImages() {
2426
throw new Error(`❌ OG template not found: ${TEMPLATE_PATH}`);
2527
}
2628

27-
// --- Read data and template ---
29+
// --- Read data, template, and cache manifest ---
2830
const templateContent = await readFile(TEMPLATE_PATH, 'utf8');
2931
const jsonData = JSON.parse(await readFile(INPUT_JSON_PATH, 'utf8'));
32+
33+
let oldCache = {};
34+
try {
35+
oldCache = JSON.parse(await readFile(CACHE_MANIFEST_PATH, 'utf8'));
36+
} catch (e) {
37+
console.log('ℹ️ No existing cache manifest found. Will generate all images.');
38+
}
39+
40+
const newCache = {};
3041

3142
if (!jsonData || !jsonData.pages || jsonData.pages.length === 0) {
3243
console.warn('⚠️ No pages to process.');
@@ -41,16 +52,25 @@ async function generateImages() {
4152

4253
let successCount = 0;
4354
let errorCount = 0;
55+
let skippedCount = 0;
4456

4557
// --- Process each page ---
4658
for (const pageData of jsonData.pages) {
47-
const { title, description, outputPath } = pageData;
59+
const { title, description, outputPath, finalHash } = pageData;
4860

4961
try {
62+
const fileAlreadyExists = await pathExists(outputPath);
63+
const isCacheValid = oldCache[outputPath] === finalHash;
64+
65+
if (fileAlreadyExists && isCacheValid) {
66+
skippedCount++;
67+
newCache[outputPath] = finalHash; // Keep valid entry in new cache
68+
continue;
69+
}
70+
5071
const page = await browser.newPage();
5172
await page.setViewport({ width: 1200, height: 630 });
5273

53-
// Populate the template with page data
5474
const htmlContent = templateContent
5575
.replace('LOGO_SRC', jsonData.logoDataUri)
5676
.replace('BACKGROUND_URL', jsonData.backgroundDataUri || '')
@@ -59,7 +79,6 @@ async function generateImages() {
5979

6080
await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
6181

62-
// Take the screenshot
6382
await page.screenshot({
6483
path: outputPath,
6584
type: 'jpeg',
@@ -69,6 +88,7 @@ async function generateImages() {
6988
const stats = await stat(outputPath);
7089
console.log(`✅ Generated: ${outputPath.replace(PROJECT_ROOT, '')} (${(stats.size / 1024).toFixed(1)} KB)`);
7190
successCount++;
91+
newCache[outputPath] = finalHash; // Add new entry to cache
7292

7393
await page.close();
7494
} catch (err) {
@@ -77,9 +97,28 @@ async function generateImages() {
7797
errorCount++;
7898
}
7999
}
80-
100+
81101
await browser.close();
82-
console.log(`\n✨ Generation complete! Succeeded: ${successCount}, Failed: ${errorCount}.`);
102+
103+
// --- Cleanup stale images and cache entries ---
104+
const validOutputPaths = new Set(jsonData.pages.map(p => p.outputPath));
105+
let cleanedCount = 0;
106+
for (const oldPath in oldCache) {
107+
if (!validOutputPaths.has(oldPath)) {
108+
if (await pathExists(oldPath)) {
109+
await unlink(oldPath);
110+
cleanedCount++;
111+
}
112+
}
113+
}
114+
if (cleanedCount > 0) {
115+
console.log(`🗑️ Cleaned up ${cleanedCount} stale OG image(s).`);
116+
}
117+
118+
// --- Write the new cache manifest ---
119+
await writeFile(CACHE_MANIFEST_PATH, JSON.stringify(newCache, null, 2));
120+
121+
console.log(`\n✨ Generation complete! Succeeded: ${successCount}, Skipped: ${skippedCount}, Failed: ${errorCount}.`);
83122
if (errorCount > 0) process.exit(1);
84123
}
85124

0 commit comments

Comments
 (0)