Skip to content

Commit dc7c738

Browse files
committed
Cache Contentful data in Redis
1 parent b1c3b38 commit dc7c738

File tree

11 files changed

+1513
-3994
lines changed

11 files changed

+1513
-3994
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Thumbs.db
2828
.env.local
2929
.type-coverage
3030
tsconfig.tsbuildinfo
31+
/cms
3132
/coverage
3233
/email/*.html
3334
/dist

lib/cms-cache.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createClient } from 'contentful';
2+
3+
import { CMSType, RawCMSData } from '@scripts/cms';
4+
5+
import { getRedisClient } from './redis';
6+
7+
const cfClient = createClient({
8+
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
9+
space: process.env.CONTENTFUL_SPACE_ID!
10+
});
11+
12+
const CACHE_TTL = 640000;
13+
14+
export async function getCMSData(type: CMSType): Promise<RawCMSData> {
15+
const redis = await getRedisClient();
16+
const cacheKey = `contentful:${type}`;
17+
const cached = await redis.get(cacheKey);
18+
19+
if (cached) {
20+
return JSON.parse(cached);
21+
}
22+
23+
const data = await fetchFromContentful(type);
24+
25+
// Cache in Redis
26+
await redis.set(cacheKey, JSON.stringify(data), { EX: CACHE_TTL });
27+
28+
return data;
29+
}
30+
31+
export async function refreshCMSData(type: CMSType): Promise<RawCMSData> {
32+
const redis = await getRedisClient();
33+
const cacheKey = `contentful:${type}`;
34+
const data = await fetchFromContentful(type);
35+
36+
await redis.set(cacheKey, JSON.stringify(data), { EX: CACHE_TTL });
37+
38+
return data;
39+
}
40+
41+
async function fetchFromContentful(type: CMSType): Promise<RawCMSData> {
42+
return await cfClient.getEntries({ content_type: type });
43+
}

lib/redis.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { createClient } from 'redis';
2+
3+
let redisClient: ReturnType<typeof createClient> | null = null;
4+
5+
export async function getRedisClient() {
6+
if (!redisClient) {
7+
redisClient = createClient({
8+
url: process.env.REDIS_URL // e.g. "redis://:<password>@<host>:<port>"
9+
});
10+
11+
redisClient.on('error', err => console.error('Redis Client Error', err));
12+
13+
await redisClient.connect();
14+
}
15+
return redisClient;
16+
}

package.json

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@
5353
},
5454
"dependencies": {
5555
"@apollo/client": "3.13.9",
56-
"@babylonjs/core": "8.22.2",
57-
"@babylonjs/gui": "8.22.2",
58-
"@babylonjs/loaders": "8.22.2",
59-
"@babylonjs/materials": "8.22.2",
56+
"@babylonjs/core": "8.23.2",
57+
"@babylonjs/gui": "8.23.2",
58+
"@babylonjs/loaders": "8.23.2",
59+
"@babylonjs/materials": "8.23.2",
6060
"@codersrank/activity": "0.9.14",
6161
"@codersrank/education": "0.9.12",
6262
"@codersrank/portfolio": "0.9.10",
@@ -73,7 +73,7 @@
7373
"animateme": "2.4.2",
7474
"async-array-prototype": "1.1.1",
7575
"attr-i18n": "1.0.0",
76-
"babylonjs-gltf2interface": "8.22.2",
76+
"babylonjs-gltf2interface": "8.23.2",
7777
"contentful": "11.7.15",
7878
"d3": "7.9.0",
7979
"date-fns": "4.1.0",
@@ -92,7 +92,7 @@
9292
"jsdom": "26.1.0",
9393
"lottie-web": "5.13.0",
9494
"mongodb": "6.18.0",
95-
"next": "15.4.6",
95+
"next": "15.5.0",
9696
"next-sitemap": "4.2.3",
9797
"npm-maintainer": "1.0.2",
9898
"pass-score": "2.0.0",
@@ -106,9 +106,10 @@
106106
"react-gtm-module": "2.0.11",
107107
"react-modal": "3.16.3",
108108
"react-round-carousel": "1.5.0",
109-
"react-slick": "0.30.3",
109+
"react-slick": "0.31.0",
110110
"react-svg-donuts": "3.0.0",
111111
"react-vertical-timeline-component": "3.6.0",
112+
"redis": "5.8.2",
112113
"round-carousel-component": "1.2.1",
113114
"scriptex-socials": "1.9.1",
114115
"scss-goodies": "2.2.0",
@@ -118,45 +119,45 @@
118119
"svg64": "2.0.0",
119120
"swiper": "11.2.10",
120121
"touchsweep": "2.2.0",
121-
"tough-cookie": "5.1.2",
122+
"tough-cookie": "6.0.0",
122123
"typed-usa-states": "2.1.0",
123124
"universal-github-client": "1.0.3",
124125
"use-interval": "1.4.0",
125126
"uuid": "11.1.0"
126127
},
127128
"devDependencies": {
128-
"@babel/core": "7.28.0",
129+
"@babel/core": "7.28.3",
129130
"@eslint/compat": "1.3.2",
130131
"@eslint/eslintrc": "3.3.1",
131-
"@eslint/js": "9.32.0",
132-
"@next/env": "15.4.6",
132+
"@eslint/js": "9.33.0",
133+
"@next/env": "15.5.0",
133134
"@svgr/webpack": "8.1.0",
134135
"@testing-library/dom": "10.4.1",
135-
"@testing-library/jest-dom": "6.6.4",
136+
"@testing-library/jest-dom": "6.7.0",
136137
"@testing-library/react": "16.3.0",
137138
"@types/d3": "7.4.3",
138139
"@types/jest": "30.0.0",
139140
"@types/markdown-it": "14.1.2",
140141
"@types/mjml": "4.7.4",
141-
"@types/node": "24.2.1",
142-
"@types/react": "19.1.9",
142+
"@types/node": "24.3.0",
143+
"@types/react": "19.1.10",
143144
"@types/react-copy-to-clipboard": "5.0.7",
144145
"@types/react-dom": "19.1.7",
145146
"@types/react-gtm-module": "2.0.4",
146147
"@types/react-modal": "3.16.3",
147148
"@types/react-slick": "0.23.13",
148149
"@types/react-vertical-timeline-component": "3.3.6",
149150
"@types/uuid": "10.0.0",
150-
"@typescript-eslint/eslint-plugin": "8.39.0",
151-
"@typescript-eslint/parser": "8.39.0",
151+
"@typescript-eslint/eslint-plugin": "8.40.0",
152+
"@typescript-eslint/parser": "8.40.0",
152153
"autoprefixer": "10.4.21",
153-
"browserslist": "4.25.2",
154+
"browserslist": "4.25.3",
154155
"cheerio": "1.1.2",
155156
"cloudinary": "2.7.0",
156157
"commander": "14.0.0",
157158
"dotenv": "17.2.1",
158-
"eslint": "9.32.0",
159-
"eslint-config-next": "15.4.6",
159+
"eslint": "9.33.0",
160+
"eslint-config-next": "15.5.0",
160161
"eslint-config-prettier": "10.1.8",
161162
"eslint-plugin-compat": "6.0.2",
162163
"eslint-plugin-import": "2.32.0",
@@ -201,7 +202,7 @@
201202
"postcss-utilities": "0.8.4",
202203
"postcss-watch-folder": "2.0.0",
203204
"prettier": "3.6.2",
204-
"puppeteer": "24.15.0",
205+
"puppeteer": "24.17.0",
205206
"sass": "1.90.0",
206207
"serwist": "9.1.1",
207208
"stylelint": "16.23.1",
@@ -215,11 +216,11 @@
215216
"ts-node": "10.9.2",
216217
"tsconfig-paths": "4.2.0",
217218
"tslib": "2.8.1",
218-
"tsx": "4.20.3",
219+
"tsx": "4.20.4",
219220
"type-coverage": "2.29.7",
220221
"typescript": "5.9.2",
221222
"universal-github-client": "1.0.3",
222-
"webpack": "5.101.0"
223+
"webpack": "5.101.3"
223224
},
224225
"private": true,
225226
"browserslist": [

renovate.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"assignees": ["@scriptex"],
3-
"enabled": false,
43
"extends": [
54
"config:base",
65
":automergePatch",

src/pages/api/refresh-cms/route.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { NextResponse } from 'next/server';
2+
3+
import { refreshCMSData } from '@lib/cms-cache';
4+
import { allCMSTypes } from '@scripts/cms';
5+
6+
export const POST = async () => {
7+
try {
8+
for (const type of allCMSTypes) {
9+
try {
10+
await refreshCMSData(type);
11+
} catch (err) {
12+
console.error(`Failed to refresh ${type}`, err);
13+
}
14+
}
15+
16+
return NextResponse.json({ refreshed: true });
17+
} catch (err) {
18+
console.error(err);
19+
return NextResponse.json({ error: 'Failed to refresh all content' }, { status: 500 });
20+
}
21+
};

src/scripts/cms.ts

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,36 @@ import type { Document } from '@contentful/rich-text-types';
33
import { Asset, createClient, EntryCollection, EntrySkeletonType } from 'contentful';
44

55
import type { ForceNode } from '@data/skills-list';
6+
import { getCMSData } from '@lib/cms-cache';
67

78
import type { Partner } from './types';
89

9-
type CMSType =
10-
| 'bio'
11-
| 'badge'
12-
| 'owner'
13-
| 'slide'
14-
| 'video'
15-
| 'titles'
16-
| 'article'
17-
| 'funding'
18-
| 'partner'
19-
| 'strength'
20-
| 'timeline'
21-
| 'education'
22-
| 'experience'
23-
| 'occupation'
24-
| 'certificate'
25-
| 'resume link'
26-
| 'resume more'
27-
| 'testimonial'
28-
| 'resume skills'
29-
| 'githubskyline';
30-
31-
type RawCMSData = EntryCollection<EntrySkeletonType, undefined, string>;
10+
export const allCMSTypes = [
11+
'bio',
12+
'badge',
13+
'owner',
14+
'slide',
15+
'video',
16+
'titles',
17+
'article',
18+
'funding',
19+
'partner',
20+
'strength',
21+
'timeline',
22+
'education',
23+
'experience',
24+
'occupation',
25+
'certificate',
26+
'resume link',
27+
'resume more',
28+
'testimonial',
29+
'resume skills',
30+
'githubskyline'
31+
] as const;
32+
33+
export type CMSType = (typeof allCMSTypes)[number];
34+
35+
export type RawCMSData = EntryCollection<EntrySkeletonType, undefined, string>;
3236

3337
export type BioEntry = {
3438
content: string;
@@ -205,7 +209,8 @@ const client = createClient({
205209

206210
const getHTMLString = <T extends Document>(data?: T): string => (data ? documentToHtmlString(data) : '');
207211

208-
const getCMSData = async (type: CMSType): Promise<RawCMSData> => {
212+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
213+
const _getCMSData = async (type: CMSType): Promise<RawCMSData> => {
209214
const contentTypes = await client.getContentTypes();
210215
const content_type = contentTypes.items.find(item => item.name.toLowerCase() === type)?.sys.id;
211216

0 commit comments

Comments
 (0)