Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ba676b0

Browse files
BioPhotoneneajahoLayZeeDK
authored
docs: add ISR docs (#1578)
* docs: self-close img element * docs: use Markdown images * docs: disable file existence checking for Markdown images --------- Co-authored-by: Enea Jahollari <[email protected]> Co-authored-by: Lars Gyrup Brink Nielsen <[email protected]>
1 parent aabb85b commit ba676b0

23 files changed

+1312
-475
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ Thumbs.db
4747
# Generated Docusaurus files
4848
.docusaurus/
4949
.cache-loader/
50+
51+
.angular

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
/**/images/**/*
77
.docusaurus/
88
CHANGELOG.md
9+
10+
.angular

apps/docs/docs/isr/_category_.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"label": "@rx-angular/isr"
3+
}

apps/docs/docs/isr/api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
sidebar_label: API
3+
sidebar_position: 10
4+
title: API
5+
---
6+
7+
TODO: Add API docs

apps/docs/docs/isr/benefits.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
sidebar_label: Benefits
3+
sidebar_position: 8
4+
title: Benefits
5+
---
6+
7+
- ✅ Improved TTFB metric (Time to first byte)
8+
- ✅ Less server resource usage because of caching
9+
- ✅ Don’t do the same work twice
10+
- ✅ Extendable API-s
11+
- ✅ DX
12+
- ✅ Open source library (MIT license)

apps/docs/docs/isr/cache-handlers.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
---
2+
sidebar_label: Cache Handlers
3+
sidebar_position: 6
4+
title: Cache Handlers
5+
---
6+
7+
## Cache Handlers
8+
9+
Cache handlers are classes that extend the `CacheHandler` abstract class. They are responsible for handling the cache of the pages.
10+
11+
### InMemoryCacheHandler (default)
12+
13+
The default cache handler is the `InMemoryCacheHandler`. It stores the cached pages in memory (RAM). It uses the `Map` data structure to store the pages.
14+
15+
### FileSystemCacheHandler (prerendering on steroids)
16+
17+
There are cases where you want to store the cached pages in the file system. For example, if you want to deploy your app to a serverless environment, you can't use the `InMemoryCacheHandler` because the memory is not persistent. In this case, you can use the `FileSystemCacheHandler`.
18+
19+
The `FileSystemCacheHandler` stores the cached pages in the file system. It uses the `fs` module to read and write files. It stores the cached pages in the directory that you provide in the `cacheFolderPath` field.
20+
21+
The `FileSystemCacheHandler` has a field called `addPrerenderedPagesToCache`. If you set it to `true`, it will add the prerendered pages (from the path that you provide in the `prerenderedPagesPath` field) to the cache. If you set it to `false`, it will only add the pages that are cached using normal ISR. The default value is `false`.
22+
23+
```typescript
24+
const fsCacheHandler = new FileSystemCacheHandler({
25+
cacheFolderPath: join(distFolder, '/cache'),
26+
prerenderedPagesPath: distFolder,
27+
addPrerenderedPagesToCache: true,
28+
});
29+
```
30+
31+
And then, to register the cache handler, you need to pass it to the `cache` field in ISRHandler:
32+
33+
```typescript
34+
const isr = new ISRHandler({
35+
...
36+
// highlight-next-line
37+
cache: fsCacheHandler,
38+
});
39+
```
40+
41+
Now, the prerendered pages will be added to the cache. This means that the first request to a page will be served from the cache. And then, ISR will take over and revalidate the cache based on the `revalidate` field in the routes.
42+
43+
### Custom Cache Handler
44+
45+
The cache handling in ISR is **pluggable**. This means that you can use any cache handler that you want. You can also create your own cache handler.
46+
47+
To do that, you need to extend the `CacheHandler` abstract class.
48+
49+
To give you an idea of how to create a custom cache handler, let's take a look at this example of a custom cache handler that stores the cached pages in **redis**:
50+
51+
```typescript
52+
import Redis from 'ioredis';
53+
import { CacheData, CacheHandler, ISROptions } from '@rx-angular/isr/models';
54+
55+
type RedisCacheHandlerOptions = {
56+
/**
57+
* Redis connection string, e.g. "redis://localhost:6379"
58+
*/
59+
connectionString: string;
60+
/**
61+
* Redis key prefix, defaults to "isr:"
62+
*/
63+
keyPrefix?: string;
64+
};
65+
66+
export class RedisCacheHandler extends CacheHandler {
67+
private redis: Redis;
68+
69+
constructor(private readonly options: RedisCacheHandlerOptions) {
70+
super();
71+
72+
this.redis = new Redis(this.options.connectionString);
73+
console.log('RedisCacheHandler initialized 🚀');
74+
}
75+
76+
add(
77+
url: string,
78+
html: string,
79+
options: ISROptions = { revalidate: null }
80+
): Promise<void> {
81+
const htmlWithMsg = html + cacheMsg(options.revalidate);
82+
83+
return new Promise((resolve, reject) => {
84+
const cacheData: CacheData = {
85+
html: htmlWithMsg,
86+
options,
87+
createdAt: Date.now(),
88+
};
89+
const key = this.createKey(url);
90+
this.redis.set(key, JSON.stringify(cacheData)).then(() => {
91+
resolve();
92+
});
93+
});
94+
}
95+
96+
get(url: string): Promise<CacheData> {
97+
return new Promise((resolve, reject) => {
98+
const key = this.createKey(url);
99+
this.redis.get(key, (err, result) => {
100+
if (err || result === null || result === undefined) {
101+
reject('This url does not exist in cache!');
102+
} else {
103+
resolve(JSON.parse(result));
104+
}
105+
});
106+
});
107+
}
108+
109+
getAll(): Promise<string[]> {
110+
console.log('getAll() is not implemented for RedisCacheHandler');
111+
return Promise.resolve([]);
112+
}
113+
114+
has(url: string): Promise<boolean> {
115+
return new Promise((resolve, reject) => {
116+
const key = this.createKey(url);
117+
resolve(this.redis.exists(key).then((exists) => exists === 1));
118+
});
119+
}
120+
121+
delete(url: string): Promise<boolean> {
122+
return new Promise((resolve, reject) => {
123+
const key = this.createKey(url);
124+
resolve(this.redis.del(key).then((deleted) => deleted === 1));
125+
});
126+
}
127+
128+
clearCache?(): Promise<boolean> {
129+
throw new Error('Method not implemented.');
130+
}
131+
132+
private createKey(url: string): string {
133+
const prefix = this.options.keyPrefix || 'isr';
134+
return `${prefix}:${url}`;
135+
}
136+
}
137+
138+
const cacheMsg = (revalidateTime?: number | null): string => {
139+
const time = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
140+
141+
let msg = '<!-- ';
142+
143+
msg += `\n🚀 ISR: Served from Redis Cache! \n⌛ Last updated: ${time}. `;
144+
145+
if (revalidateTime) {
146+
msg += `\n⏭️ Next refresh is after ${revalidateTime} seconds. `;
147+
}
148+
149+
msg += ' \n-->';
150+
151+
return msg;
152+
};
153+
```
154+
155+
And then, to register the cache handler, you need to pass it to the `cache` field in ISRHandler:
156+
157+
```typescript title="server.ts"
158+
const redisCacheHandler = new RedisCacheHandler({
159+
connectionString: process.env['REDIS_CONNECTION_STRING'] || '' // e.g. "redis://localhost:6379"
160+
});
161+
162+
const isr = new ISRHandler({
163+
...
164+
// highlight-next-line
165+
cache: redisCacheHandler, // 👈 register the cache handler
166+
});
167+
```
168+
169+
And that's it! Now you have a custom cache handler that stores the cached pages in redis.
170+
171+
## Gotchas
172+
173+
When using ISR, you need to be aware of the following gotcha. When storing the cached pages in a redis storage, you need to separate cache pages based on the build id. This is because the build id is different for each build. If you don't do that, you will get javascript loading errors in the browser. This is because the browser will try to load the javascript files from the previous build, which don't exist anymore.
174+
175+
To solve this issue, we can use the `buildId` field in the `ISRHandler` options. This field is used to separate the cached pages based on the build id.
176+
177+
Where can we get the build id? We can add it in our `environment.ts` (for dev/prod) file:
178+
179+
```typescript
180+
export const environment = {
181+
...
182+
buildTimestamp: new Date().getTime(), // 👈 add this
183+
};
184+
```
185+
186+
And then, we can use it in our `ISRHandler` options:
187+
188+
```typescript
189+
const isr = new ISRHandler({
190+
...
191+
// highlight-next-line
192+
buildId: environment.buildTimestamp, // 👈 use it here
193+
});
194+
```
195+
196+
The buildId will help us separate the cached pages based on the build id. The moment the user ask for a page that we had cached in the previous build, we check if the build id of the cached page is the same as the current build id. If it's not, we server-side render the page again and cache it. If it's the same, we serve the cached page.
197+
198+
And that's it! Now you have a working ISR with a custom cache handler that stores the cached pages in redis and separates them based on the build id.
199+
200+
## Cache Handler API
201+
202+
The `CacheHandler` abstract class has the following API:
203+
204+
```typescript
205+
export abstract class CacheHandler {
206+
abstract add(url: string, html: string, options: ISROptions): Promise<void>;
207+
208+
abstract get(url: string): Promise<CacheData>;
209+
210+
abstract getAll(): Promise<string[]>;
211+
212+
abstract has(url: string): Promise<boolean>;
213+
214+
abstract delete(url: string): Promise<boolean>;
215+
216+
abstract clearCache?(): Promise<boolean>;
217+
}
218+
```
219+
220+
## Cache Data
221+
222+
The `CacheData` interface is used to store the cached pages in the cache handler. It has the following fields:
223+
224+
```typescript
225+
export interface CacheData {
226+
html: string;
227+
options: ISROptions;
228+
createdAt: number;
229+
}
230+
```

apps/docs/docs/isr/cache-hooks.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
sidebar_label: Cache Hooks
3+
sidebar_position: 7
4+
title: Cache Hooks
5+
---
6+
7+
## Cache Hooks
8+
9+
There are cases where you want to modify the html that is served from cache or the html that is generated on the fly.
10+
11+
For example, you might want to add some tracking scripts to the html that is served from cache. Or you might want to add some custom html to the html that is generated on the fly.
12+
13+
To do that, you can use the `modifyCachedHtml` and `modifyGeneratedHtml` callbacks.
14+
15+
### modifyCachedHtml
16+
17+
The `modifyCachedHtml` callback is called when the html is served from cache (on every user request). It receives the request and the cached html as parameters. It should return the modified html.
18+
19+
### modifyGeneratedHtml
20+
21+
The `modifyGeneratedHtml` callback is called when the html is generated on the fly (before the cache is stored). It receives the request and the generated html as parameters. It should return the modified html.
22+
23+
### Example
24+
25+
```ts
26+
server.get(
27+
'*',
28+
// Serve page if it exists in cache
29+
async (req, res, next) =>
30+
await isr.serveFromCache(req, res, next, {
31+
modifyCachedHtml: (req, cachedHtml) => {
32+
return `${cachedHtml}<!-- Hello, I'm a modification to the original cache! -->`;
33+
},
34+
}),
35+
// Server side render the page and add to cache if needed
36+
async (req, res, next) =>
37+
await isr.render(req, res, next, {
38+
modifyGeneratedHtml: (req, html) => {
39+
return `${html}<!-- Hello, I'm modifying the generatedHtml before caching it! -->`;
40+
},
41+
})
42+
);
43+
```
44+
45+
:::caution **Important:**
46+
Use these methods with caution as the logic written can increase the processing time.
47+
:::

apps/docs/docs/isr/error-handling.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
sidebar_label: Error Handling
3+
sidebar_position: 4
4+
title: Error Handling
5+
---
6+
7+
## How it works?
8+
9+
Errors are a part of web development. They can happen at any time, and they can be caused by a
10+
variety of factors. When an error occurs, it's important to handle it appropriately to ensure
11+
that your site remains accessible and functional. ISR has a feature that allows you to handle
12+
errors during the regeneration or caching of your pages.
13+
14+
By default, when an **http error** occurs during the server-rendering of a page, we don't
15+
cache the page but fall back to client-side rendering, because it probably will have error
16+
messages or other content that is not intended to be cached.
17+
18+
## Configure error handling
19+
20+
To configure error handling, you can use the **skipCachingOnHttpError** flag in the ISR
21+
configuration. By default, this flag is set to **true**.
22+
23+
In order to enable caching of pages with http errors, you should set this flag to **false**.
24+
25+
```typescript
26+
const isr = new ISRHandler({
27+
// other options
28+
// highlight-next-line
29+
skipCachingOnHttpError: false,
30+
});
31+
```
32+
33+
:::caution
34+
Be aware that this may cause some issues with your site. And you should handle these errors appropriately to ensure that your site remains accessible and functional.
35+
:::
36+
37+
In, order to see if the page has an error, you can check the errors property in the generated
38+
html. Here's an example of a page with an error:
39+
40+
![ISR state of a page with an error](pathname:///img/isr/errors-in-html.png)
41+
42+
## Handle other errors
43+
44+
You can also handle other errors that are not http errors. For example, if you have a posts
45+
page, but with no content, you can add an error the **errors** of the ISR state.
46+
47+
In order to do that, you can use the **addError** method of the **IsrService**.
48+
49+
```typescript
50+
import { IsrService } from '@rx-angular/isr/browser';
51+
52+
@Component({})
53+
export class PostSComponent {
54+
private isrService = inject(IsrService);
55+
56+
loadPosts() {
57+
this.otherService.getPosts().subscribe({
58+
next: (posts) => {
59+
if (posts.length === 0) {
60+
// highlight-start
61+
this.isrService.addError({
62+
name: 'No posts',
63+
message: 'There are no posts to show',
64+
} as Error);
65+
// highlight-end
66+
}
67+
68+
// other logic
69+
},
70+
});
71+
}
72+
}
73+
```
74+
75+
So, if we have a page with no posts, by adding the error to the **errors** property, we
76+
will be able to skip the caching of the page and fall back to client-side rendering.
77+
78+
:::tip
79+
You can use this feature to handle errors, or you can use it only to skip caching of pages.
80+
:::

0 commit comments

Comments
 (0)