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

Skip to content

docs: add ISR docs #1578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,5 @@ Thumbs.db
# Generated Docusaurus files
.docusaurus/
.cache-loader/

.angular
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
/**/images/**/*
.docusaurus/
CHANGELOG.md

.angular
3 changes: 3 additions & 0 deletions apps/docs/docs/isr/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"label": "@rx-angular/isr"
}
7 changes: 7 additions & 0 deletions apps/docs/docs/isr/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
sidebar_label: API
sidebar_position: 10
title: API
---

TODO: Add API docs
12 changes: 12 additions & 0 deletions apps/docs/docs/isr/benefits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
sidebar_label: Benefits
sidebar_position: 8
title: Benefits
---

- ✅ Improved TTFB metric (Time to first byte)
- ✅ Less server resource usage because of caching
- ✅ Don’t do the same work twice
- ✅ Extendable API-s
- ✅ DX
- ✅ Open source library (MIT license)
230 changes: 230 additions & 0 deletions apps/docs/docs/isr/cache-handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
---
sidebar_label: Cache Handlers
sidebar_position: 6
title: Cache Handlers
---

## Cache Handlers

Cache handlers are classes that extend the `CacheHandler` abstract class. They are responsible for handling the cache of the pages.

### InMemoryCacheHandler (default)

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.

### FileSystemCacheHandler (prerendering on steroids)

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`.

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.

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`.

```typescript
const fsCacheHandler = new FileSystemCacheHandler({
cacheFolderPath: join(distFolder, '/cache'),
prerenderedPagesPath: distFolder,
addPrerenderedPagesToCache: true,
});
```

And then, to register the cache handler, you need to pass it to the `cache` field in ISRHandler:

```typescript
const isr = new ISRHandler({
...
// highlight-next-line
cache: fsCacheHandler,
});
```

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.

### Custom Cache Handler

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.

To do that, you need to extend the `CacheHandler` abstract class.

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**:

```typescript
import Redis from 'ioredis';
import { CacheData, CacheHandler, ISROptions } from '@rx-angular/isr/models';

type RedisCacheHandlerOptions = {
/**
* Redis connection string, e.g. "redis://localhost:6379"
*/
connectionString: string;
/**
* Redis key prefix, defaults to "isr:"
*/
keyPrefix?: string;
};

export class RedisCacheHandler extends CacheHandler {
private redis: Redis;

constructor(private readonly options: RedisCacheHandlerOptions) {
super();

this.redis = new Redis(this.options.connectionString);
console.log('RedisCacheHandler initialized 🚀');
}

add(
url: string,
html: string,
options: ISROptions = { revalidate: null }
): Promise<void> {
const htmlWithMsg = html + cacheMsg(options.revalidate);

return new Promise((resolve, reject) => {
const cacheData: CacheData = {
html: htmlWithMsg,
options,
createdAt: Date.now(),
};
const key = this.createKey(url);
this.redis.set(key, JSON.stringify(cacheData)).then(() => {
resolve();
});
});
}

get(url: string): Promise<CacheData> {
return new Promise((resolve, reject) => {
const key = this.createKey(url);
this.redis.get(key, (err, result) => {
if (err || result === null || result === undefined) {
reject('This url does not exist in cache!');
} else {
resolve(JSON.parse(result));
}
});
});
}

getAll(): Promise<string[]> {
console.log('getAll() is not implemented for RedisCacheHandler');
return Promise.resolve([]);
}

has(url: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const key = this.createKey(url);
resolve(this.redis.exists(key).then((exists) => exists === 1));
});
}

delete(url: string): Promise<boolean> {
return new Promise((resolve, reject) => {
const key = this.createKey(url);
resolve(this.redis.del(key).then((deleted) => deleted === 1));
});
}

clearCache?(): Promise<boolean> {
throw new Error('Method not implemented.');
}

private createKey(url: string): string {
const prefix = this.options.keyPrefix || 'isr';
return `${prefix}:${url}`;
}
}

const cacheMsg = (revalidateTime?: number | null): string => {
const time = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');

let msg = '<!-- ';

msg += `\n🚀 ISR: Served from Redis Cache! \n⌛ Last updated: ${time}. `;

if (revalidateTime) {
msg += `\n⏭️ Next refresh is after ${revalidateTime} seconds. `;
}

msg += ' \n-->';

return msg;
};
```

And then, to register the cache handler, you need to pass it to the `cache` field in ISRHandler:

```typescript title="server.ts"
const redisCacheHandler = new RedisCacheHandler({
connectionString: process.env['REDIS_CONNECTION_STRING'] || '' // e.g. "redis://localhost:6379"
});

const isr = new ISRHandler({
...
// highlight-next-line
cache: redisCacheHandler, // 👈 register the cache handler
});
```

And that's it! Now you have a custom cache handler that stores the cached pages in redis.

## Gotchas

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.

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.

Where can we get the build id? We can add it in our `environment.ts` (for dev/prod) file:

```typescript
export const environment = {
...
buildTimestamp: new Date().getTime(), // 👈 add this
};
```

And then, we can use it in our `ISRHandler` options:

```typescript
const isr = new ISRHandler({
...
// highlight-next-line
buildId: environment.buildTimestamp, // 👈 use it here
});
```

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.

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.

## Cache Handler API

The `CacheHandler` abstract class has the following API:

```typescript
export abstract class CacheHandler {
abstract add(url: string, html: string, options: ISROptions): Promise<void>;

abstract get(url: string): Promise<CacheData>;

abstract getAll(): Promise<string[]>;

abstract has(url: string): Promise<boolean>;

abstract delete(url: string): Promise<boolean>;

abstract clearCache?(): Promise<boolean>;
}
```

## Cache Data

The `CacheData` interface is used to store the cached pages in the cache handler. It has the following fields:

```typescript
export interface CacheData {
html: string;
options: ISROptions;
createdAt: number;
}
```
47 changes: 47 additions & 0 deletions apps/docs/docs/isr/cache-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
sidebar_label: Cache Hooks
sidebar_position: 7
title: Cache Hooks
---

## Cache Hooks

There are cases where you want to modify the html that is served from cache or the html that is generated on the fly.

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.

To do that, you can use the `modifyCachedHtml` and `modifyGeneratedHtml` callbacks.

### modifyCachedHtml

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.

### modifyGeneratedHtml

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.

### Example

```ts
server.get(
'*',
// Serve page if it exists in cache
async (req, res, next) =>
await isr.serveFromCache(req, res, next, {
modifyCachedHtml: (req, cachedHtml) => {
return `${cachedHtml}<!-- Hello, I'm a modification to the original cache! -->`;
},
}),
// Server side render the page and add to cache if needed
async (req, res, next) =>
await isr.render(req, res, next, {
modifyGeneratedHtml: (req, html) => {
return `${html}<!-- Hello, I'm modifying the generatedHtml before caching it! -->`;
},
})
);
```

:::caution **Important:**
Use these methods with caution as the logic written can increase the processing time.
:::
80 changes: 80 additions & 0 deletions apps/docs/docs/isr/error-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
sidebar_label: Error Handling
sidebar_position: 4
title: Error Handling
---

## How it works?

Errors are a part of web development. They can happen at any time, and they can be caused by a
variety of factors. When an error occurs, it's important to handle it appropriately to ensure
that your site remains accessible and functional. ISR has a feature that allows you to handle
errors during the regeneration or caching of your pages.

By default, when an **http error** occurs during the server-rendering of a page, we don't
cache the page but fall back to client-side rendering, because it probably will have error
messages or other content that is not intended to be cached.

## Configure error handling

To configure error handling, you can use the **skipCachingOnHttpError** flag in the ISR
configuration. By default, this flag is set to **true**.

In order to enable caching of pages with http errors, you should set this flag to **false**.

```typescript
const isr = new ISRHandler({
// other options
// highlight-next-line
skipCachingOnHttpError: false,
});
```

:::caution
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.
:::

In, order to see if the page has an error, you can check the errors property in the generated
html. Here's an example of a page with an error:

![ISR state of a page with an error](pathname:///img/isr/errors-in-html.png)

## Handle other errors

You can also handle other errors that are not http errors. For example, if you have a posts
page, but with no content, you can add an error the **errors** of the ISR state.

In order to do that, you can use the **addError** method of the **IsrService**.

```typescript
import { IsrService } from '@rx-angular/isr/browser';

@Component({})
export class PostSComponent {
private isrService = inject(IsrService);

loadPosts() {
this.otherService.getPosts().subscribe({
next: (posts) => {
if (posts.length === 0) {
// highlight-start
this.isrService.addError({
name: 'No posts',
message: 'There are no posts to show',
} as Error);
// highlight-end
}

// other logic
},
});
}
}
```

So, if we have a page with no posts, by adding the error to the **errors** property, we
will be able to skip the caching of the page and fall back to client-side rendering.

:::tip
You can use this feature to handle errors, or you can use it only to skip caching of pages.
:::
Loading