Angular 7 - Service Worker PWA + SSR not working on server

With a little "good-to-have" workaround to #13351 got this working on Angular 10 by quite little change in main.browser.ts

// Workaround for service worker, issue #13351.
document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .then(() => {
      if ('serviceWorker' in navigator && environment.production) {
        navigator.serviceWorker.register('./ngsw-worker.js');
      }
    })
    .catch((err) => console.error(err));
});

In the ngsw-config.json I've edited index to refer to index2.html instead of index.html. That's needed in my case to render SSR on Firebase/NestJS properly the root (/) path.

ngsw-config.json

{
  "$schema": "../../node_modules/@angular/service-worker/config/schema.json",      
  "index": "/index2.html",
  ...
}

When running with prod flag in the Chrome DevTools -> Application -> Service Workers can see it. View Page Source shows SSR. I can also add the install the application via Chrome on my desktop. In addition to that, the application works offline. Both, via mobile browser, and desktop app.

Can't why default mobile notification, i.e. Add to home screen isn't popping up.


It is actually not related to your server at all. The problem is, service worker is tried to be registered on the server side, which is not supported. What you can do is make it so that service worker will register on client side.

I haven't tried but this just may work for you. You need to put a separate script in the end of body in index.html to run on browser:

  <script>
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/ngsw-worker.js');
    }
  </script>

Be aware that this will only give you the basic caching functionality of service worker and will probably not work for SwPush or SwUpdate. You can check how service worker module works internally here.

Another thing you should know is, service workers are actually not suitable for SSR apps. The generated ngsw.json file will not include all the pages you have. You will have to either modify this file manually or serve it not as a static file but create it dynamically.

And Caching all the pages is not a thing you want to do (unless page contents are static). Because a cached page will always show the same thing since you are not doing an XHR request on client side. For example if your home page is cached for a client, that client will always see the same page regardless of the intended dynamic content.

At this point you should consider why you want SSR and Web APP at the same time. If you need SSR for SEO, you don't need a SW. Just do SSR when the user agent is of a crawler, and serve dynamic angular when the client is a normal user.

How to switch request based on User Agent

This is my thought and I don't know if anyone is doing this. But theoretically, it should work.

Firstly, you will have to build your app to two different directories (DIST_FOLDER/ssr and DIST_FOLDER/csr in my example) with server-side and client-side rendering configurations. You may exclude SW from SSR. You can use SW in CSR as usual.

I found this package that can detect crawler/bot user agents. For express and Angular, you would use it like:

app.get('*', (req, res) => {
  var CrawlerDetector = new Crawler(req)

  // check the current visitor's useragent
  if (CrawlerDetector.isCrawler())
  {
    // render the index html at server side
    res.render(path.join(DIST_FOLDER, 'ssr', 'index.html'), { req });
  }
  else {
    // serve static index.html file built without SSR and let the client render it
    res.sendFile(path.join(DIST_FOLDER, 'csr', 'index.html'));
  }
});

After implementing this, you can debug your application to see if it works correctly by changing your user agent to one that is written here (i.e. use Postman).