Skip to content
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

Impossible to use dynamic routes with Angular Universal?! #1817

Closed
tommueller opened this issue Aug 25, 2020 · 12 comments
Closed

Impossible to use dynamic routes with Angular Universal?! #1817

tommueller opened this issue Aug 25, 2020 · 12 comments

Comments

@tommueller
Copy link

tommueller commented Aug 25, 2020

Proposal

What is the summary of the proposal?

It should be possible to use dynamically generated routes with Angular Universal.

What is the proposal?

In the docs it clearly states that it is necessary for Angular Universal to work to use initialNavigation=enabled on the router. This, as the name already states, leads to (if I understand correctly) Angular performing the first navigation before i.e. the APP_INITIALIZERs are loaded (which seems strange to me in the first place).

Now I assume that a common use case for using Angular Universal is to add server side rendering to pages powered by some sort of headless CMS. That is also what I am trying to achieve. Without initialNavigation=enabled I had no problem to create the routes dynamically, after fetching page information from our backend. I basically execute this code within an APP_INITIALIZER in my app.modules.ts:

this.backendService.getStaticLinks().pipe(
  tap(routes => {
    const config = this.router.config;
    config.unshift(
      ...routes.map(route => ({
        path: route.path,
        component: StaticContentComponent,
        data: {
          id: route._id
        }
      }))
    );
    this.router.resetConfig(config);
  })
)

Now with initialNavigation=enabled I always receive a 404 because I have no way of adding the routes, before the app tries to navigate there. The app still works with Universal support if I do not use initialNavigation=enabled, but I get bad flickering, which is not acceptable to deliver it like this to the user.

Is there any way this problem can be solved?

edit: I am not entirely sure if this belongs to the Universal or the Angular Core team, so please feel free to move the ticket accordingly or let me know if I should open it somewhere else.

@tommueller
Copy link
Author

tommueller commented Sep 28, 2020

After a lot of reading and trial-and-error I now found a solution to this issue. I feel that it is a little bit hacky, so I would really love some feedback on it. Also I still believe that this should be possible with Angular Universal, without hacking into server.ts and main.ts.

Basically what I do now, is to fetch the data about the pages, both in the server and the app, before the Angular app gets bootstrapped at all. It look more or less like this:

server.ts:

// All regular routes use the Universal engine
  server.get('*', (req, res) => {
    // fetch dynamic routes
    // /!\ duplicate code to src/main.ts
    fetch('http://static.content/')
      .then(response => response.json())
      .then(resp => {
        const routes = resp.entries.map(route => ({
          path: route.path,
          component: StaticContentComponent,
          data: {
            id: route._id,
            name: route.name
          }
        }));

        res.render(indexHtml, {
          req,
          providers: [
            { provide: APP_BASE_HREF, useValue: req.baseUrl },
            { provide: DYNAMIC_ROUTES, useValue: routes }
          ]
        });
      });
  });

  return server;
}

and basically the same in main.ts:

document.addEventListener('DOMContentLoaded', () => {
  // fetch dynamic routes
  // /!\ duplicate code to server.ts
  fetch('http://static.content/')
    .then(response => response.json())
    .then(resp => {
      const routes = resp.entries.map(route => ({
        path: route.path,
        component: StaticContentComponent,
        data: {
          id: route._id,
          name: route.name
        }
      }));

      platformBrowserDynamic([
        { provide: DYNAMIC_ROUTES, useValue: routes }
      ])
        .bootstrapModule(AppModule)
        .catch(err => console.error(err));
    });
});

And then in my app-routing.module.ts I add the data provided in DYNAMIC_ROUTES to the routes:

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      initialNavigation: 'enabled'
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {
  constructor(@Inject(DYNAMIC_ROUTES) private dynamicRoutes, private router: Router) {
    const config = router.config;
    config.unshift(...this.dynamicRoutes);
    this.router.resetConfig(config);
  }
}

So this does actually work. I am a bit unhappy about having to make the call twice (but couldn't get it working otherwise). Also I would have preferred to avoid hacking into server.ts and main.ts.

Any ideas on how to improve this? Or do you see any issues to take this into production?

@nicolae-olariu
Copy link

nicolae-olariu commented Oct 7, 2020

@tommueller It works perfect! Only one thing I've refactored was to move the duplicate functionality for creating routes inside a service in a static method so we can call that from both server.ts and main.ts files. Other than that, I think we can live with the 2 initial calls until Universal will merge this into an official release version.

Thank you so much for your idea!

@mustordont
Copy link

Currently we are using approach:

  1. Use empty root CanActivate guard to load application wide settings instead of APP_INITIALIZER
  2. Match any route by wildcard ** route with component which can build content dynamically
  3. Load appropriate page info (collection of components) for the requested route at CanActivate guard from our backend
  4. Add custom route reuse strategy for do not reload components on fragment/ query params changing
export const ROUTES: Routes = [
    {
        path: '',
        canActivate: [SettingsGuard],
        children: [
            {
                path: '404',
                component: ErrorNotFoundComponent
            },
            {
                path: '',
                component: MainComponent,
            },
            {
                path: '**',
                loadChildren: () => import('./modules/dynamic-page/dynamic-page.module').then((m) => m.DynamicPageModule),
                runGuardsAndResolvers: 'pathParamsChange',
                resolve: {
                    pageData: PageGuard
                },
                canActivate: [PageGuard],
            },
        ]
    },
];

@NgModule({
    imports: [
        RouterModule.forRoot(ROUTES, {
            initialNavigation: 'enabledBlocking',
            // enableTracing: true,
            relativeLinkResolution: 'legacy'
        })
    ],
    exports: [RouterModule],
    providers: [
        // for wildcard ** routes reuse
        {
            provide: RouteReuseStrategy,
            useClass: CustomReuseStrategy
        },
    ]
})
export class AppRoutingModule {}
```

@dmitry-kostin
Copy link

dmitry-kostin commented Feb 16, 2021

that's my approach

  1. Disable initial navigation
  2. Fetch routes in APP_INITIALIZER and update router config
  3. call manually router.initialNavigation() when you ready and resolve initializer
  4. profit

@EduardoMM17
Copy link

After a lot of reading and trial-and-error I now found a solution to this issue. I feel that it is a little bit hacky, so I would really love some feedback on it. Also I still believe that this should be possible with Angular Universal, without hacking into server.ts and main.ts.

Basically what I do now, is to fetch the data about the pages, both in the server and the app, before the Angular app gets bootstrapped at all. It look more or less like this:

server.ts:

// All regular routes use the Universal engine
  server.get('*', (req, res) => {
    // fetch dynamic routes
    // /!\ duplicate code to src/main.ts
    fetch('http://static.content/')
      .then(response => response.json())
      .then(resp => {
        const routes = resp.entries.map(route => ({
          path: route.path,
          component: StaticContentComponent,
          data: {
            id: route._id,
            name: route.name
          }
        }));

        res.render(indexHtml, {
          req,
          providers: [
            { provide: APP_BASE_HREF, useValue: req.baseUrl },
            { provide: DYNAMIC_ROUTES, useValue: routes }
          ]
        });
      });
  });

  return server;
}

and basically the same in main.ts:

document.addEventListener('DOMContentLoaded', () => {
  // fetch dynamic routes
  // /!\ duplicate code to server.ts
  fetch('http://static.content/')
    .then(response => response.json())
    .then(resp => {
      const routes = resp.entries.map(route => ({
        path: route.path,
        component: StaticContentComponent,
        data: {
          id: route._id,
          name: route.name
        }
      }));

      platformBrowserDynamic([
        { provide: DYNAMIC_ROUTES, useValue: routes }
      ])
        .bootstrapModule(AppModule)
        .catch(err => console.error(err));
    });
});

And then in my app-routing.module.ts I add the data provided in DYNAMIC_ROUTES to the routes:

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      initialNavigation: 'enabled'
    })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {
  constructor(@Inject(DYNAMIC_ROUTES) private dynamicRoutes, private router: Router) {
    const config = router.config;
    config.unshift(...this.dynamicRoutes);
    this.router.resetConfig(config);
  }
}

So this does actually work. I am a bit unhappy about having to make the call twice (but couldn't get it working otherwise). Also I would have preferred to avoid hacking into server.ts and main.ts.

Any ideas on how to improve this? Or do you see any issues to take this into production?

Hello. I am facing this problem now in Angular 9. What is StaticContentComponent?

@tommueller
Copy link
Author

@Marcuss17 StaticContentComponent is my Angular component which renders the pages coming from the CMS. They are static content pages, hence the name.
It is a very simple component, which basically fetches the data from the CMS and renders it to HTML.

@Buyukcaglar
Copy link

Hi,
Does anyone successfully apply the proposed solution here to 'prerender'?

This solution works fine for SSR and ClientApp but Prerenderer requires DYNAMIC_ROUTES to be provided from 'app.server.module.ts' which seems to be impossible unfortunately. So as a result of this we are unable to prerender dynamic urls.

Any ideas of help would greatly appreciated.

Thanks,
Onur

@tommueller
Copy link
Author

@Buyukcaglar Doesn't the whole idea of prerendering contradict with dynamic urls? At least in the way we use this, the content of the pages comes from somewhere else and is hence not static. Prerendering happens while compiling the project, so it does not make sense to prerender pages, that have dynamically changing content.

Or am I missing your point?

@Buyukcaglar
Copy link

@Buyukcaglar Doesn't the whole idea of prerendering contradict with dynamic urls? At least in the way we use this, the content of the pages comes from somewhere else and is hence not static. Prerendering happens while compiling the project, so it does not make sense to prerender pages, that have dynamically changing content.

Or am I missing your point?

@tommueller Hi,

The project currently I am working on is an e-commerce platform, so strict SEO requirements and site performance score (Google LightHouse/PageSpeed Insights) is essential. Although 'Product Detail' pages has fixed url prefix so I can define them within the 'routes' array so they are perfectly fine in prerendering. Unfortunately, 'Product Categories' (aka Category Product Listing Pages) are totally dynamic and therefore be their urls must be 'fetched' from backend API. And yes it whenever a product is added/modified or removed from a category that listing page hastto be prerendered again. On the other hand SSR on Category Listing Pages works like charm but Google Pagespeed score is total disaster. Lets say we got consistent 55 out 100 while using SSR but on the same listing page score is 95 out of 100 when page is prerendered.

Hope this clarifies the situtation.

Kind regards,
Onur

@damienwebdev
Copy link

damienwebdev commented May 23, 2022

For those curious, I can say with very high confidence that dynamically inserted, server-side rendered routes are possible and do not require API changes in Universal. We've been working on/with our @daffodil/external-router package for two years now in production environments starting from ng9 through ng13 (even with Clover @alan-agius4 ) and we can say with high certainty that this issue can be closed.

@tommueller
Copy link
Author

Yes, as I stated, I found a solution for this as well which is running fine in our setup for ~2 years now. The proposed library was not around when I asked this question :)

Anyways, fine by me to close this 👍

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Jun 23, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants