Skip to content

Commit e1a96fc

Browse files
authored
Merge pull request #1059 from datum-cloud/fix/blog-landing-loading-time
Chore(strapi): Strapi and Blog cache optimization
2 parents ae69cc6 + 470e44f commit e1a96fc

File tree

14 files changed

+277
-199
lines changed

14 files changed

+277
-199
lines changed

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ STRAPI_CACHE_TTL=3000
3434
# Generate: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
3535
STRAPI_WEBHOOK_SECRET=your-shared-secret
3636

37-
# Request timeout in seconds before falling back to the persistent stale cache (default: 10)
38-
STRAPI_TIMEOUT=10
37+
# Request timeout in seconds before falling back to the persistent stale cache (default: 3)
38+
STRAPI_TIMEOUT=3

astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export default defineConfig({
4949
},
5050
image: {
5151
layout: 'constrained',
52+
domains: ['grateful-excitement-dfe9d47bad.media.strapiapp.com'],
5253
},
5354
integrations: [
5455
announcement({

src/components/GithubStargazerValue.astro

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { Cache } from '@libs/cache';
33
44
const cache = new Cache('.cache');
55
6-
let starCount: number = 0;
7-
8-
if (cache.has('stargazerCount')) {
9-
starCount = cache.get<number>('stargazerCount') as number;
6+
let starCount = 0;
7+
if (await cache.has('stargazerCount')) {
8+
starCount = (await cache.get<number>('stargazerCount')) ?? 0;
109
}
1110
---
1211

src/components/blog/BlogStrapi.astro

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,10 @@ const { page = 1 } = Astro.props as Props;
1717
// Fetch articles from Strapi (cached list, without rich-text blocks)
1818
const rawArticles: StrapiArticle[] = await fetchStrapiArticles();
1919
20-
// Normalize and sort articles
20+
// Normalize articles (already sorted by newest first in fetchStrapiArticles)
2121
let articles: NormalizedStrapiArticle[] = [];
2222
if (rawArticles.length > 0) {
2323
articles = rawArticles.map(normalizeArticle);
24-
// Sort by date (newest first) - should already be sorted but ensure consistency
25-
articles.sort((a, b) => {
26-
return new Date(b.data.date).getTime() - new Date(a.data.date).getTime();
27-
});
2824
}
2925
3026
// Pagination
@@ -58,8 +54,8 @@ const nextUrl = currentPage < totalPages ? `${baseUrl}/${currentPage + 1}/` : un
5854
<>
5955
{featuredPosts.length > 0 && (
6056
<div class="featured-posts-section">
61-
{featuredPosts.map((post) => (
62-
<FeaturedPostStrapi post={post} baseUrl={baseUrl} />
57+
{featuredPosts.map((post, index) => (
58+
<FeaturedPostStrapi post={post} baseUrl={baseUrl} priority={index === 0} />
6359
))}
6460
</div>
6561
)}

src/components/blog/FeaturedPostStrapi.astro

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
---
22
// src/components/blog/FeaturedPostStrapi.astro
3+
import { Image } from 'astro:assets';
34
import { formatISODate } from '@utils/dateUtils';
45
import type { NormalizedStrapiArticle } from '@/src/types/strapi';
56
6-
const { post, baseUrl = '/blog' } = Astro.props as {
7+
const {
8+
post,
9+
baseUrl = '/blog',
10+
priority = false,
11+
} = Astro.props as {
712
post: NormalizedStrapiArticle;
813
baseUrl?: string;
14+
priority?: boolean;
915
};
1016
1117
// Use pre-calculated reading time from normalized article
@@ -19,12 +25,17 @@ const readingTimeText = `${readingTimeMinutes} minute read`;
1925
{
2026
post.data.thumbnail && (
2127
<a href={`${baseUrl}/${post.id}/`} title={post.data.title}>
22-
<img
28+
<Image
2329
src={post.data.thumbnail}
2430
alt={post.data.title}
2531
width={717}
2632
height={454}
27-
loading="lazy"
33+
widths={[400, 717, 1200]}
34+
sizes="(max-width: 768px) 400px, (max-width: 1200px) 717px, 1200px"
35+
format="webp"
36+
loading={priority ? 'eager' : 'lazy'}
37+
decoding={priority ? 'sync' : 'async'}
38+
fetchpriority={priority ? 'high' : 'auto'}
2839
/>
2940
</a>
3041
)

src/libs/cache.ts

Lines changed: 82 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,140 @@
11
import type { AstroGlobal } from 'astro';
2-
import fs from 'node:fs';
2+
import fs from 'node:fs/promises';
33
import path from 'node:path';
44

55
export class Cache {
66
private cacheDir: string;
77

88
constructor(cacheDir: string) {
99
this.cacheDir = cacheDir;
10-
if (!fs.existsSync(cacheDir)) {
11-
fs.mkdirSync(cacheDir, { recursive: true });
12-
}
10+
}
11+
12+
private async ensureDir(): Promise<void> {
13+
await fs.mkdir(this.cacheDir, { recursive: true });
1314
}
1415

1516
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16-
set(key: string, data: any, expiresIn?: number): void {
17-
// Ensure cache directory exists in case it was deleted while the dev server is running
18-
if (!fs.existsSync(this.cacheDir)) {
19-
fs.mkdirSync(this.cacheDir, { recursive: true });
20-
}
17+
async set(key: string, data: any, expiresIn?: number): Promise<void> {
18+
await this.ensureDir();
2119

2220
const filePath = path.join(this.cacheDir, `${key}.json`);
23-
fs.writeFileSync(filePath, JSON.stringify(data), 'utf-8');
21+
await fs.writeFile(filePath, JSON.stringify(data), 'utf-8');
2422

2523
if (expiresIn) {
2624
const expirationTime = Date.now() + expiresIn;
27-
fs.writeFileSync(
25+
await fs.writeFile(
2826
filePath.replace('.json', '.expires'),
2927
JSON.stringify(expirationTime),
3028
'utf-8'
3129
);
3230
}
3331
}
3432

35-
get<T>(key: string): T | null {
33+
async get<T>(key: string): Promise<T | null> {
3634
const filePath = path.join(this.cacheDir, `${key}.json`);
3735
const expiresPath = path.join(this.cacheDir, `${key}.expires`);
3836

39-
if (fs.existsSync(filePath)) {
40-
try {
41-
const data = fs.readFileSync(filePath, 'utf-8');
37+
try {
38+
const data = await fs.readFile(filePath, 'utf-8');
4239

43-
// Check if file is empty or whitespace only
44-
if (!data.trim()) {
45-
console.warn(`Cache file for key "${key}" is empty, clearing cache`);
46-
this.clear(key);
47-
return null;
48-
}
40+
if (!data.trim()) {
41+
console.warn(`Cache file for key "${key}" is empty, clearing cache`);
42+
await this.clear(key);
43+
return null;
44+
}
4945

50-
let expirationTime: number | null = null;
51-
if (fs.existsSync(expiresPath)) {
52-
try {
53-
const expiresData = fs.readFileSync(expiresPath, 'utf-8');
54-
expirationTime = JSON.parse(expiresData);
55-
} catch (error) {
56-
console.warn(`Invalid expiration file for key "${key}", clearing cache:`, error);
57-
this.clear(key);
58-
return null;
59-
}
60-
}
46+
let expirationTime: number | null = null;
47+
try {
48+
const expiresData = await fs.readFile(expiresPath, 'utf-8');
49+
expirationTime = JSON.parse(expiresData);
50+
} catch {
51+
// No expires file or invalid — treat as no expiry
52+
}
6153

62-
if (expirationTime && Date.now() > expirationTime) {
63-
this.clear(key);
64-
return null;
65-
}
54+
if (expirationTime && Date.now() > expirationTime) {
55+
await this.clear(key);
56+
return null;
57+
}
6658

67-
return JSON.parse(data) as T;
68-
} catch (error) {
69-
console.warn(`Failed to parse cache file for key "${key}":`, error);
70-
this.clear(key);
59+
return JSON.parse(data) as T;
60+
} catch (err) {
61+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
7162
return null;
7263
}
64+
console.warn(`Failed to parse cache file for key "${key}":`, err);
65+
await this.clear(key);
66+
return null;
7367
}
74-
return null;
7568
}
7669

77-
clear(key: string): void {
70+
async clear(key: string): Promise<void> {
7871
const filePath = path.join(this.cacheDir, `${key}.json`);
7972
const expiresPath = path.join(this.cacheDir, `${key}.expires`);
80-
if (fs.existsSync(filePath)) {
81-
fs.unlinkSync(filePath);
73+
try {
74+
await fs.unlink(filePath);
75+
} catch {
76+
// Ignore ENOENT
8277
}
83-
if (fs.existsSync(expiresPath)) {
84-
fs.unlinkSync(expiresPath);
78+
try {
79+
await fs.unlink(expiresPath);
80+
} catch {
81+
// Ignore ENOENT
8582
}
8683
}
8784

88-
clearAll(): void {
89-
const files = fs.readdirSync(this.cacheDir);
90-
for (const file of files) {
91-
fs.unlinkSync(path.join(this.cacheDir, file));
85+
async clearAll(): Promise<void> {
86+
try {
87+
const files = await fs.readdir(this.cacheDir);
88+
await Promise.all(files.map((file) => fs.unlink(path.join(this.cacheDir, file))));
89+
} catch (err) {
90+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
91+
throw err;
92+
}
9293
}
9394
}
9495

95-
has(key: string): boolean {
96+
async has(key: string): Promise<boolean> {
9697
const filePath = path.join(this.cacheDir, `${key}.json`);
9798
const expiresPath = path.join(this.cacheDir, `${key}.expires`);
9899

99-
if (fs.existsSync(filePath)) {
100+
try {
101+
const data = await fs.readFile(filePath, 'utf-8');
102+
if (!data.trim()) {
103+
await this.clear(key);
104+
return false;
105+
}
106+
100107
try {
101-
// Check if file is empty
102-
const data = fs.readFileSync(filePath, 'utf-8');
103-
if (!data.trim()) {
104-
this.clear(key);
108+
const expiresData = await fs.readFile(expiresPath, 'utf-8');
109+
const expirationTime = JSON.parse(expiresData);
110+
if (Date.now() > expirationTime) {
111+
await this.clear(key);
105112
return false;
106113
}
107-
108-
if (fs.existsSync(expiresPath)) {
109-
try {
110-
const expirationTime = JSON.parse(fs.readFileSync(expiresPath, 'utf-8'));
111-
if (Date.now() > expirationTime) {
112-
this.clear(key);
113-
return false;
114-
}
115-
} catch (error) {
116-
console.warn(`Invalid expiration file for key "${key}", clearing cache:`, error);
117-
this.clear(key);
118-
return false;
119-
}
120-
}
121-
return true;
122-
} catch (error) {
123-
console.warn(`Error checking cache for key "${key}":`, error);
124-
this.clear(key);
114+
} catch {
115+
// No expires file — treat as valid
116+
}
117+
return true;
118+
} catch (err) {
119+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
125120
return false;
126121
}
122+
console.warn(`Error checking cache for key "${key}":`, err);
123+
await this.clear(key);
124+
return false;
127125
}
128-
return false;
129126
}
130127

131-
getAllKeys(): string[] {
132-
const files = fs.readdirSync(this.cacheDir);
133-
return files.map((file) => path.basename(file, '.json'));
128+
async getAllKeys(): Promise<string[]> {
129+
try {
130+
const files = await fs.readdir(this.cacheDir);
131+
return files.map((file) => path.basename(file, '.json'));
132+
} catch (err) {
133+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
134+
return [];
135+
}
136+
throw err;
137+
}
134138
}
135139

136140
setCacheHeader(type: 'long' | 'short') {

0 commit comments

Comments
 (0)