Skip to content

Commit aa09d90

Browse files
authored
Cache Contentful data in Redis (#3266)
1 parent b1c3b38 commit aa09d90

File tree

15 files changed

+1571
-4036
lines changed

15 files changed

+1571
-4036
lines changed

.env.local.example

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
GTM_ID=''
2-
MONGODB_URI=MONGODB_URI=mongodb+srv://<USERNAME>:<PASSWORD>@<CLUSTER>.vfjpiol.mongodb.net/?retryWrites=true&w=majority
3-
GITHUB_TOKEN=''
4-
GITLAB_TOKEN=''
5-
VERCEL_ORG_ID=''
6-
LAST_FM_API_KEY=''
7-
SONARCLOUD_TOKEN=''
8-
VERCEL_PROJECT_ID=''
9-
NEXT_PUBLIC_GTM_ID=''
10-
CLOUDINARY_API_KEY=''
11-
SENDINBLUE_API_KEY=''
12-
CONTENTFUL_SPACE_ID=''
13-
CODACY_PROJECT_TOKEN=''
14-
CLOUDINARY_API_SECRET=''
15-
CLOUDINARY_CLOUD_NAME=''
16-
CONTENTFUL_ACCESS_TOKEN=''
17-
CONTENTFUL_PREVIEW_ACCESS_TOKEN=''
18-
NEXT_PUBLIC_HONEYPOT_VALUE=''
1+
# vercel env pull
2+
CONTENTFUL_ACCESS_TOKEN=""
3+
CONTENTFUL_PREVIEW_ACCESS_TOKEN=""
4+
CONTENTFUL_SPACE_ID=""
5+
EDGE_CONFIG=""
6+
GITHUB_TOKEN=""
7+
GITLAB_TOKEN=""
8+
GITLAB_TOKEN_ORG=""
9+
GTM_ID=""
10+
LAST_FM_API_KEY=""
11+
MONGODB_URI=""
12+
NEXT_PUBLIC_GTM_ID=""
13+
NEXT_PUBLIC_HONEYPOT_VALUE=""
14+
REDIS_URL=""
15+
SENDINBLUE_API_KEY=""
16+
VERCEL_OIDC_TOKEN=""
17+
CLOUDINARY_API_KEY=""
18+
CLOUDINARY_API_SECRET=""
19+
CLOUDINARY_CLOUD_NAME=""

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
yarn type-coverage
2525
env:
2626
GTM_ID: ${{ secrets.GTM_ID }}
27+
REDIS_URL: ${{ secrets.REDIS_URL }}
2728
MONGODB_URI: ${{ secrets.MONGODB_URI }}
2829
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
2930
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}

.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

README.md

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,31 @@
2020
2121
A fullstack progressive web app built with
2222

23-
- [Typescript](https://www.typescriptlang.org/)
24-
- [PostCSS](https://postcss.org/)
25-
- [Markdown](https://www.markdownguide.org/)/[MDX](https://mdxjs.com/)
26-
- [Javascript](https://www.javascript.com/)
27-
- [Bash](https://www.gnu.org/software/bash/)
28-
- [MJML](https://mjml.io/)
29-
- [Vercel](https://vercel.com/)
30-
- [NodeJS](https://nodejs.org/)
31-
- [TS Node](https://typestrong.org/ts-node/)
32-
- [NextJS](https://nextjs.org/)
33-
- [Cloudinary](https://cloudinary.com/)
34-
- [Puppeteer](https://pptr.dev/)
35-
- [MongoDB](https://www.mongodb.com/)
36-
- [React](https://reactjs.org/)
37-
- [D3](https://d3js.org/)
38-
- [BabylonJS](https://www.babylonjs.com/)
39-
- [Houdini](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Houdini)
40-
- [ITCSS](https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/)
41-
- [ESLint](https://eslint.org/)
42-
- [Stylelint](https://stylelint.io/)
43-
- [Prettier](https://prettier.io/)
44-
- [Jest](https://jestjs.io/)
45-
- [Husky](https://typicode.github.io/husky/)
46-
- [Contentful](https://www.contentful.com/)
23+
- [Typescript](https://www.typescriptlang.org/)
24+
- [PostCSS](https://postcss.org/)
25+
- [Markdown](https://www.markdownguide.org/)/[MDX](https://mdxjs.com/)
26+
- [Javascript](https://www.javascript.com/)
27+
- [Bash](https://www.gnu.org/software/bash/)
28+
- [MJML](https://mjml.io/)
29+
- [Vercel](https://vercel.com/)
30+
- [NodeJS](https://nodejs.org/)
31+
- [TS Node](https://typestrong.org/ts-node/)
32+
- [NextJS](https://nextjs.org/)
33+
- [Cloudinary](https://cloudinary.com/)
34+
- [Puppeteer](https://pptr.dev/)
35+
- [MongoDB](https://www.mongodb.com/)
36+
- [Redis](https://redis.io/)
37+
- [React](https://reactjs.org/)
38+
- [D3](https://d3js.org/)
39+
- [BabylonJS](https://www.babylonjs.com/)
40+
- [Houdini](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Houdini)
41+
- [ITCSS](https://www.xfive.co/blog/itcss-scalable-maintainable-css-architecture/)
42+
- [ESLint](https://eslint.org/)
43+
- [Stylelint](https://stylelint.io/)
44+
- [Prettier](https://prettier.io/)
45+
- [Jest](https://jestjs.io/)
46+
- [Husky](https://typicode.github.io/husky/)
47+
- [Contentful](https://www.contentful.com/)
4748

4849
## Visitor stats
4950

lib/cms-cache.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createClient } from 'contentful';
2+
3+
import { CMSType, RawCMSData } from '@scripts/cms';
4+
5+
import { getRedisClient } from './redis';
6+
7+
const contentfulClient = 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+
await redis.set(cacheKey, JSON.stringify(data), { EX: CACHE_TTL });
26+
27+
return data;
28+
}
29+
30+
export async function refreshCMSData(type: CMSType): Promise<RawCMSData> {
31+
const redis = await getRedisClient();
32+
const cacheKey = `contentful:${type}`;
33+
const data = await fetchFromContentful(type);
34+
35+
await redis.set(cacheKey, JSON.stringify(data), { EX: CACHE_TTL });
36+
37+
return data;
38+
}
39+
40+
async function fetchFromContentful(type: CMSType): Promise<RawCMSData> {
41+
const contentTypes = await contentfulClient.getContentTypes();
42+
const content_type = contentTypes.items.find(item => item.name.toLowerCase() === type)?.sys.id;
43+
44+
if (!content_type) {
45+
return {
46+
items: [],
47+
limit: 0,
48+
skip: 0,
49+
total: 0
50+
};
51+
}
52+
53+
return await contentfulClient.getEntries({ content_type, order: ['fields.index'] });
54+
}

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+
}

next-env.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3+
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
4+
/// <reference path="./.next/types/routes.d.ts" />
35

46
// NOTE: This file should not be edited
57
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

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+
};

0 commit comments

Comments
 (0)