ngx-isr

Incremental Static Regeneration for Angular

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
ngx-isr
103130.5.1a month agoa year agoMinified + gzip package size for ngx-isr in KB

Readme

Incremental Static Regeneration for Angular
A library that enables Angular Universal applications to generate static pages at runtime and then update them incrementally on demand or on a schedule.
­čô░ Read the blog post
How to use it?
  1. Install npm package
npm install ngx-isr

Make sure you have Angular Universal in your app.

  1. Initialize ISRHandler inside server.ts
const isr = new ISRHandler({
  indexHtml,
  invalidateSecretToken: 'MY_TOKEN', // replace with env secret key ex. process.env.REVALIDATE_SECRET_TOKEN
  enableLogging: !environment.production
});

  1. Add invalidation url handler
server.use(express.json());
server.post("/api/invalidate", async (req, res) => await isr.invalidate(req, res));

  1. Replace Angular default server side rendering with ISR rendering

Replace
server.get('*',
  (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  }
);
with
server.get('*',
  // Serve page if it exists in cache
  async (req, res, next) => await isr.serveFromCache(req, res, next),
  // Server side render the page and add to cache if needed
  async (req, res, next) => await isr.render(req, res, next),
);

You can also pass providers to each of the ISRHandler methods.
server.get('*',
  ...
    async (req, res, next) => await isr.render(req, res, next, {
      providers: [
        { provide: APP_BASE_HREF, useValue: req.baseUrl }, // <-- Needs to be provided when passing providers
        { provide: CUSTOM_TOKEN, useValue: 'Hello from ISR' },
        CustomService
      ]
    }),
);

It is also possible to pass a modifyCachedHtml or modifyGeneratedHtml callbacks to the ISRHandler methods. These methods provide a way to modify the html served from cache or the html that is generated on the fly.
Important: Use these methods with caution as the logic written can increase the processing time.
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! -->`
    }
  }),
);

ISRHandler provides APP_BASE_HREF by default. And if you want pass providers into the methods of ISRHandler, you will also have to provide APP_BASE_HREF token.
  1. Add revalidate key in route data

Example:
{
  path: "example",
  component: ExampleComponent,
  data: { revalidate: 5 },
}

NOTE: Routes that don't have revalidate key in data won't be handled by ISR. They will fallback to Angular default server side rendering pipeline.

  1. Add NgxIsrModule in AppServerModule imports
import { NgxIsrModule } from 'ngx-isr'; // <-- Import module from library

@NgModule({
  imports: [
    ...
    NgxIsrModule.forRoot()  // <-- Use it in module imports
  ]
})
export class AppServerModule {}

When importing the module, NgxIsrService will be initialized and will start to listen to route changes, only on the server side, so the browser bundle won't contain any extra code.
Play with demo
git clone https://github.com/eneajaho/ngx-isr.git
npm install
npm run dev:ssr

  1. Disable Javascript (in order to not load Angular after the page loads)
  2. Navigate to different pages and check how the cache gets regenerated in terminal logs
How it works?

The first time a user opens a pages, we server-side render that page, and save its result in cache.
Next time when a user requests the same page he will be served the first cached response.
To handle Incremental Static Regeneration, we need to configure it from our route data.
For example:
const routes: Routes = [
  {
    path: "one",
    component: PageOneComponent,
  },
  {
    path: "two",
    component: PageTwoComponent,
    data: { revalidate: 5 },
  },
  {
    path: "three",
    component: PageThreeComponent,
    data: { revalidate: 0 },
  },
];

  • Path /one won't be cached at all, and everytime it is requested it will be server-rendered and then will be served to the user.

  • Path /two on the first request will be server-rendered and then will be cached. On the second request to it, the user will be served the cache that we got on the first request.
The url will be added to a regeneration queue, in order to re-generate the cache after 5 seconds. On the third request to the same url, if the regeneration was finished the user will be served the regenerated page otherwise he will be served with the old cached page.
  • Path /three after the first request that is server-rendered, the page will be added to cache and the cache will never be deleted automatically as in path /two. So after the first request, all the other ones will come from the cache.

Changelog

  • Version 0.5.0

#### Breaking Changes: The invalidate method of IsrHandler now is converted to be a POST request.
```ts server.post('/api/invalidate', async (req, res) =>
await isr.invalidate(req, res)
); ```
It accepts a body with the following structure:
```ts
{
  token: string; // The secren token 
  urlsToInvalidate: string[]; // The urls to invalidate ex. ['/one', '/two']
}
```
Now you also need to add server.use(express.json()); in your server.ts file in order to parse the body of the request.
#### Changes: feat: added modifyCachedHtml and modifyGeneratedHtml callbacks to provide a mechanism to change html on the fly feat: Invalidate/regenerate multiple urls with one request
  • Version 0.4.0
Now ngx-isr will support only project in v15 and above. If you want to use it in older versions of Angular, please use v0.3.1.
The reason for this is because now we use ╔ÁSERVER_CONTEXT token in order to set the rendering context that now will be shown as: ng-server-context="ngx-isr". And this token is only available in v15 and above.
Changes:
* chore: Updated the project to v15
* feat: Added server context provider
* feat: Added RedisCacheHandler class usage in the demo app (experimental)
* chore: Started to convert the demo app in a documentation page for the library

License

MIT