Skip to content

Commit 363c41a

Browse files
feat(og-image-worker): add a meta worker to generate OG images asynchronously
1 parent a174b8a commit 363c41a

5 files changed

Lines changed: 100 additions & 6 deletions

File tree

README.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ services:
9898
restart: unless-stopped
9999
volumes:
100100
- /path/to/your/public:/app/public
101+
og-image-worker:
102+
image: ghcr.io/deepanshkhurana/ode:latest
103+
depends_on:
104+
- ode
105+
command: ["sh", "-c", "vite-node build/run-og-image-worker.ts"]
106+
restart: on-failure
107+
volumes:
108+
- /path/to/your/public:/app/public
101109
```
102110
103111
> [!TIP]
@@ -115,6 +123,8 @@ Your site will be available at `http://localhost:8080`. Restart the container to
115123
docker compose restart
116124
```
117125

126+
The `og-image-worker` runs asynchronously after indexes are available and generates OG images into `public/generated`.
127+
118128
### Other Deployment Options
119129

120130
> [!NOTE]
@@ -124,9 +134,7 @@ Once you have your Fork or branch ready, you can deploy the app but the reader p
124134

125135
#### Deploy to Vercel
126136

127-
You can directly to Vercel below. `vercel.json` already has the fixes Vercel will need.
128-
129-
https://github.com/DeepanshKhurana/ode/blob/46873b31df3d4b02bbb375d4389173a1b6ac3f6b/vercel.json#L1-L12
137+
You can directly deploy to Vercel below. See [vercel.json](https://github.com/DeepanshKhurana/ode/blob/main/vercel.json) for the required configuration.
130138

131139
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FDeepanshKhurana%2Fode)
132140

@@ -227,8 +235,16 @@ This will:
227235
3. Paginate pieces for reader mode
228236
4. Calculate word/piece statistics
229237
5. Generate RSS feed
230-
6. Generate body of work archive
231-
7. Build the production bundle
238+
6. Generate sitemap
239+
7. Generate body of work archive
240+
8. Generate meta pages
241+
9. Build the production bundle
242+
243+
OG images are generated separately using:
244+
245+
```bash
246+
npm run build:og
247+
```
232248

233249
### Preview Production Build
234250

@@ -246,6 +262,8 @@ Run individual build scripts:
246262
- `npm run build:stats` - Calculate statistics
247263
- `npm run build:rss` - Generate RSS feed
248264
- `npm run build:sitemap` - Generate Sitemap
265+
- `npm run build:og` - Generate OG images
266+
- `npm run build:meta` - Generate meta pages
249267

250268
## Tech Stack
251269

build/generate-og-images.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ async function generateOgImage(
207207
}
208208

209209
async function main() {
210+
console.log('[og-images]: starting generation...');
211+
210212
let fontData: ArrayBuffer | null = null;
211213

212214
try {
@@ -224,16 +226,19 @@ async function main() {
224226
const siteAuthor = config.site.author;
225227
const siteTagline = config.site.tagline || '';
226228

229+
console.log('[og-images]: generating og/index.png');
227230
await generateOgImage(siteTitle, siteTagline, 'index', fontData);
228231
console.log('[og-images]: generated og/index.png');
229232

230233
const piecesPath = path.join(generatedDir, 'index', 'pieces.json');
231234
if (fs.existsSync(piecesPath)) {
232235
const pieces: Piece[] = JSON.parse(fs.readFileSync(piecesPath, 'utf-8'));
236+
console.log(`[og-images]: generating ${pieces.length} piece images...`);
233237

234238
for (let i = 0; i < pieces.length; i += BATCH_SIZE) {
235239
const batch = pieces.slice(i, i + BATCH_SIZE);
236240
for (const piece of batch) {
241+
console.log(`[og-images]: generating og/${piece.slug}.png (${i + batch.indexOf(piece) + 1}/${pieces.length})`);
237242
const subtitle = piece.collections?.length
238243
? `${piece.collections[0]} · ${siteAuthor}`
239244
: siteAuthor;
@@ -249,10 +254,12 @@ async function main() {
249254
const pagesPath = path.join(generatedDir, 'index', 'pages.json');
250255
if (fs.existsSync(pagesPath)) {
251256
const pages: Page[] = JSON.parse(fs.readFileSync(pagesPath, 'utf-8'));
257+
console.log(`[og-images]: generating ${pages.length} page images...`);
252258

253259
for (let i = 0; i < pages.length; i += BATCH_SIZE) {
254260
const batch = pages.slice(i, i + BATCH_SIZE);
255261
for (const page of batch) {
262+
console.log(`[og-images]: generating og/${page.slug}.png (${i + batch.indexOf(page) + 1}/${pages.length})`);
256263
await generateOgImage(page.title, siteTitle, page.slug, fontData);
257264
}
258265
if (i + BATCH_SIZE < pages.length) {

build/run-og-image-worker.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { spawn } from 'child_process';
4+
5+
const publicDir = path.join(process.cwd(), 'public');
6+
const requiredFiles = [
7+
path.join(publicDir, 'config.yaml'),
8+
path.join(publicDir, 'generated', 'index', 'pieces.json'),
9+
path.join(publicDir, 'generated', 'index', 'pages.json')
10+
];
11+
12+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
13+
14+
async function waitForFiles(timeoutMs: number, intervalMs: number): Promise<void> {
15+
console.log('[og-image-worker]: waiting for index files...');
16+
const start = Date.now();
17+
let lastLogTime = 0;
18+
while (Date.now() - start < timeoutMs) {
19+
const missingFiles = requiredFiles.filter(filePath => !fs.existsSync(filePath));
20+
if (missingFiles.length === 0) {
21+
console.log('[og-image-worker]: all index files found');
22+
return;
23+
}
24+
const elapsed = Math.floor((Date.now() - start) / 1000);
25+
if (elapsed - lastLogTime >= 30) {
26+
console.log(`[og-image-worker]: still waiting... (${elapsed}s, missing: ${missingFiles.map(f => path.basename(f)).join(', ')})`);
27+
lastLogTime = elapsed;
28+
}
29+
await sleep(intervalMs);
30+
}
31+
throw new Error('Timed out waiting for index files');
32+
}
33+
34+
async function run(): Promise<void> {
35+
console.log('[og-image-worker]: starting worker...');
36+
await waitForFiles(600000, 2000);
37+
console.log('[og-image-worker]: running build:og...');
38+
await new Promise<void>((resolve, reject) => {
39+
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
40+
const child = spawn(command, ['run', 'build:og'], { stdio: 'inherit' });
41+
child.on('error', reject);
42+
child.on('close', code => {
43+
if (code === 0) {
44+
console.log('[og-image-worker]: worker complete');
45+
resolve();
46+
return;
47+
}
48+
reject(new Error(`build:og exited with code ${code}`));
49+
});
50+
});
51+
}
52+
53+
run()
54+
.then(() => {
55+
process.exit(0);
56+
})
57+
.catch(error => {
58+
console.error('[og-image-worker]: error running worker:', error);
59+
process.exit(1);
60+
});

docker-compose.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,11 @@ services:
66
restart: unless-stopped
77
volumes:
88
- ./public:/app/public
9+
ode-og-image-worker:
10+
image: ghcr.io/deepanshkhurana/ode:latest
11+
depends_on:
12+
- ode
13+
command: ["sh", "-c", "./node_modules/.bin/vite-node build/run-og-image-worker.ts"]
14+
restart: "no"
15+
volumes:
16+
- ./public:/app/public

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
"scripts": {
77
"dev": "vite",
88
"build": "npm run build:index && vite build",
9-
"build:index": "vite-node build/ensure-defaults.ts && vite-node build/generate-502-page.ts && vite-node build/index-pieces.ts && vite-node build/index-pages.ts && vite-node build/paginate-pieces.ts && vite-node build/calculate-stats.ts && vite-node build/generate-rss.ts && vite-node build/generate-sitemap.ts && vite-node build/generate-body-of-work.ts && vite-node build/generate-og-images.ts && vite-node build/generate-meta-pages.ts",
9+
"build:index": "vite-node build/ensure-defaults.ts && vite-node build/generate-502-page.ts && vite-node build/index-pieces.ts && vite-node build/index-pages.ts && vite-node build/paginate-pieces.ts && vite-node build/calculate-stats.ts && vite-node build/generate-rss.ts && vite-node build/generate-sitemap.ts && vite-node build/generate-body-of-work.ts && vite-node build/generate-meta-pages.ts",
1010
"build:pieces": "vite-node build/index-pieces.ts",
1111
"build:pages": "vite-node build/index-pages.ts",
1212
"build:paginate": "vite-node build/paginate-pieces.ts",
1313
"build:stats": "vite-node build/calculate-stats.ts",
1414
"build:rss": "vite-node build/generate-rss.ts",
1515
"build:sitemap": "vite-node build/generate-sitemap.ts",
1616
"build:502": "vite-node build/generate-502-page.ts",
17+
"build:og-meta": "vite-node build/generate-og-images.ts && vite-node build/generate-meta-pages.ts",
1718
"build:og": "vite-node build/generate-og-images.ts",
1819
"build:meta": "vite-node build/generate-meta-pages.ts",
1920
"lint": "eslint .",

0 commit comments

Comments
 (0)