Search code examples
node.jsangularexpressangular-universalprerender

Best Practice: Angular SSR Partial Pre-rendering with dynamic fallback


Background: Using Angular Universal to perform pre-rendering but not all routes will be rendered (query parametered or authenticated-only pages for the most part), so wanting to fallback to the express renderer as needed.

Quick Replication (bash):

npm install -g @angular/cli@next
ng new partial-prerender -s -t --minimal --routing --interactive=false
cd partial-prerender/
ng add @nguniversal/express-engine@'^9.0.0-rc.1'
ng g m child --route child --module app
cat << 'EOF' > src/app/app.component.ts
import { Component } from '@angular/core';

@Component({
  template: `<div [routerLink]="['/']">Root</div><div [routerLink]="['child']">Child</div><router-outlet></router-outlet>`,
})
export class AppComponent {}
EOF
npm run prerender
npm run serve:ssr

This quick replication will produce the app, universal implementation, a child page, and replace the app html to give 2 links and a router outlet, then build/pre-render. Both routes will be pre-rendered, but this is good enough for discussing the issue.

Problem: Dynamic SSR is performed as the Express server will pick up the request rather than serving the pre-rendered static file. URLs are normally accessed without the /index.html specified.

Note the static files can be found at /dist/partial-prerender/browser/index.html and .../child/index.html. For testing, I've replaced the contents of these files with garbage, just to be sure which is being loaded at a glance.

Can also add a console.log('DYNAMIC'); to the server.ts:

server.get('*', (req, res) => {
    console.log('DYNAMIC');
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});

When making a request to localhost:4000 or localhost:4000/child, the 'DYNAMIC' will be printed and the dynamically rendered version is produced, not giving me my mangled pre-rendered files.

When making a request to localhost:4000/index.html or localhost:4000/child/index.html, the

server.get('*.*', express.static(distFolder, { maxAge: '1y' }));

picks up and serves the mangled files.

All makes sense and why it's happening, but I want to be able to just hit a given url (without the /index.html and receive the pre-rendered files (when available), then fall back to putting SSR to work.


Potential Solution: Modify server.ts to test for file existence matching the given request path + /index.html and serve them, falling back to the res.render(...

  • Is this the best way?
    • If so, why wouldn't this be default functionality? My only guess is flexibility of you doing this with your reverse proxy while not adding this overhead of checking.
  • What's the best way to do so?
    • Haven't used Express heavily in maybe 6 years, but feel like express.static should be utilized in some way over fs
    • If fs is the answer, would it make sense to cache the pre-rendered files in memory?

If it helps, I produce an alpine-node container and deploy to K8s with an Nginx ingress. Only mention this as maybe there's a magical try-files-like functionality that can be done to 'attempt' a file + /index.html retrieval from the node container, then fallback without the /index.html, but seems highly unlikely.


Solution

  • You could use an if statement inside the get request like this

    const fullPath = join(distFolder, req.originalUrl);
      if (existsSync(fullPath)) {
        console.log('STATIC Exists');
        return res.sendFile(join(distFolder, req.originalUrl));
      } else {
         //Dynamic
         res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
       }