Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 239
Charly Avatar asked Oct 21 '25 20:10

Charly


1 Answers

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 }] });
   }
like image 70
Gavin Chebor Avatar answered Oct 24 '25 10:10

Gavin Chebor



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!