angular / angular/angular-cli

Prerendering of concrete server route doesn't work, when using wildcard client route

Ouverte
#32,490 2 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
area: @angular/ssr freq1: low severity3: broken type: bug/fix
Langage dominant
TypeScript
Étoiles
27k
Forks
11.8k
Merge moyen
14 h 23 min
PR mergées (30 j)
162

Description

### Command

build

### Is this a regression?

- [ ] Yes, this behavior used to work in the previous version

### The previous version in which this bug was not present was

_No response_

### Description

With `outputMode: server`,
when client Routes contain just a wildcard route (e.g. because rendering is CMS-driven and looks up the current URL dynamically to fetch the page structure):
```ts
export const routes: Routes = [
{
path: '**',
component: CmsDrivenComponent,
},
];
```
and when we want to prerender only some concrete routes, defined as `ServerRoute` objects in the `app.server.config.ts`:
```ts
export const serverRoutes: ServerRoute[] = [
{
path: 'some-concrete-page',
renderMode: RenderMode.Prerender,
},
{
path: '**',
renderMode: RenderMode.Server,
},
];
```

Unfortunately we get the error during the `ng build`:
`✘ [ERROR] The 'some-concrete-page' server route does not match any routes defined in the Angular routing configuration (typically provided as a part of the 'provideRouter' call). Please make sure that the mentioned server route is present in the Angular routing configuration.`

Note: Before `outputMode: server` was introduced, it was possible for feed Angular builder with a `routesFile` TXT list of concrete URL paths to prerender (and `discoverRoutes: false`).

IMHO it should be still possible to feed Angular builder with a `routesFile` TXT list of concrete URL paths to prerender, even with `outputMode: server` - to benefit from the new `AngularNodeAppEngine` instead of deprecated `CommonEngine`.

### Minimal Reproduction

Minimal reproduction repo. For more, see its readme file:
https://github.com/Platonn/test-ng21-prerender-concrete-server-route-doesnt-work-with-client-wildcard-route

### Exception or Error

```text
`✘ [ERROR] The 'some-concrete-page' server route does not match any routes defined in the Angular routing configuration (typically provided as a part of the 'provideRouter' call). Please make sure that the mentioned server route is present in the Angular routing configuration.`
```

### Your Environment

```text
ng version

_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/

Angular CLI : 21.1.2
Angular : 21.1.2
Node.js : 24.8.0
Package Manager : npm 11.6.0
Operating System : darwin arm64

┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 21.1.2 │ ^21.1.1 │
│ @angular/cli │ 21.1.2 │ ^21.1.1 │
│ @angular/common │ 21.1.2 │ ^21.1.0 │
│ @angular/compiler │ 21.1.2 │ ^21.1.0 │
│ @angular/compiler-cli │ 21.1.2 │ ^21.1.0 │
│ @angular/core │ 21.1.2 │ ^21.1.0 │
│ @angular/forms │ 21.1.2 │ ^21.1.0 │
│ @angular/platform-browser │ 21.1.2 │ ^21.1.0 │
│ @angular/platform-server │ 21.1.2 │ ^21.1.0 │
│ @angular/router │ 21.1.2 │ ^21.1.0 │
│ @angular/ssr │ 21.1.2 │ ^21.1.1 │
│ rxjs │ 7.8.2 │ ~7.8.0 │
│ typescript │ 5.9.3 │ ~5.9.2 │
│ vitest │ 4.0.18 │ ^4.0.8 │
│ zone.js │ 0.16.0 │ ~0.16.0 │
```

### Anything else relevant?

**It would be great to allow for an option to prerender an arbitrary list of routes passed via `routesFile` TXT back again**. The current requirement to expose all paths for prerendering in the `app.config.server.ts` has the following limitations:

1. All those paths need to be formulated as client `Routes` objects (with component,resolvers,guards) to avoid error in build time ("`server route does not match any routes defined in the Angular routing configuration`"), while we'd like to have just one generic `**` cms-driven route defined once.
2. We've been using for long time a custom `UrlSerializer` to persist "site context params" in the URL, behind the "eyes" of the Routes, e.g. `site.com////<........ regular angular route can see only here ........>` and 2-way synchronize those "site context params" persisted in the url with the application's state.
So I'd like to prerender the following list of paths:
```
/electronics-spa/en/USD/product/111
/electronics-spa/en/USD/product/222
/electronics-spa/de/USD/product/111
/electronics-spa/de/USD/product/222
/electronics-spa/de/EUR/product/111
/electronics-spa/de/EUR/product/222

/apparel-uk-spa/en/USD/product/888
/apparel-uk-spa/en/USD/product/999
/apparel-uk-spa/de/USD/product/888
/apparel-uk-spa/de/USD/product/999
/apparel-uk-spa/de/EUR/product/888
/apparel-uk-spa/de/EUR/product/999
```
but the actual client Routes are just:
```ts
[
{ path: 'product/:productCode', component: CmsDrivenComponent },
/* ... */
{ path: '**', component: CmsDrivenComponent },
]
```
please note that the prefix `////` is invisible to the Angular Router thanks to the custom `UrlSerializer` which intercepts those params and synchronizes with the app's state of active brand, language and currency. Let me paste below just a brief implementation idea of our custom `UrlSerializer` (but for full implementation in Spartacus, our open-source Angular meta-framework for building ecommerce sites, see [here](https://github.com/SAP/spartacus/blob/3f6dba920c9d167c9c957f0a530b665f556fe1f1/projects/core/src/site-context/services/site-context-url-serializer.ts))
```ts
/**
* Values of the site context parameters encoded in the URL.
*/
export interface SiteContextUrlParams {
brand?: string,
language?: string,
currency?: string
}

/**
* UrlTree decorated with a custom property `siteContext`
* for storing the values of the site context parameters.
*/
export interface UrlTreeWithSiteContext extends UrlTree {
siteContext?: SiteContextUrlParams;
}

/**
* Angular URL Serializer aware of Spartacus site context parameters encoded in the URL.
*/
@Injectable()
export class SiteContextUrlSerializer extends DefaultUrlSerializer {
/**
* @override Recognizes the site context parameters encoded in the prefix segments
* of the given URL.
*
* It returns the UrlTree for the given URL shortened by the recognized params, but saves
* the params' values in the custom property of UrlTree: `siteContext`.
*/
parse(url: string): UrlTreeWithSiteContext {
const urlWithParams = this.urlExtractContextParameters(url);
const parsed = super.parse(urlWithParams.url) as UrlTreeWithSiteContext;
this.urlTreeIncludeContextParameters(parsed, urlWithParams.params);
return parsed;
}

/**
* Recognizes the site context parameters encoded in the prefix segments of the given URL.
*
* It returns the recognized site context params as well as the
* URL shortened by the recognized params.
* @example For `/electronics-spa/USD/en/checkout` it returns
* `{ url: `/checkout`, params: { brand: 'electronics-spa', currency: 'USD', langauge: 'en' } }`
* ```
*/
urlExtractContextParameters(url: string): {
url: string;
params: SiteContextUrlParams;
} {
/*...*/
}

/**
* Saves the given site context parameters in the custom property
* of the given UrlTree: `siteContext`.
*/
protected urlTreeIncludeContextParameters(
urlTree: UrlTreeWithSiteContext,
params: SiteContextUrlParams
): void {
urlTree.siteContext = params;
}

/**
* @override Serializes the given UrlTree to a string and prepends
* to it the current values of the site context parameters.
*/
serialize(tree: UrlTreeWithSiteContext): string {
const params = this.urlTreeExtractContextParameters(tree);
const url = super.serialize(tree);
const serialized = this.urlIncludeContextParameters(url, params);
return serialized;
}

/**
* Returns the site context parameters stored in the custom property
* of the UrlTree: `siteContext`.
*/
urlTreeExtractContextParameters(
urlTree: UrlTreeWithSiteContext
): SiteContextUrlParams {
return urlTree.siteContext ?? {};
}

/**
* Prepends the current values of the site context parameters to the given URL.
* @example
* For `{ url: `/checkout`, params: { brand: 'electronics-spa', currency: 'USD', langauge: 'en' } }`
* it returns `/electronics-spa/USD/en/checkout`
*/
protected urlIncludeContextParameters(
url: string,
params: SiteContextUrlParams
): string {
const contextRoutePart = `${brand}/${currency}/${langauge}`

return contextRoutePart + url;
}
}
```

Guide de contribution

Ouvrir le guide de contribution

Piste de recherche

Commencez par la reproduction minimale liée et exécutez son `ng build` pour reproduire l’erreur de validation des routes du serveur. Lisez la configuration de `serverRoutes` dans `app.server.config.ts` et le contexte `routesFile` de l’issue, puis retracez le comportement du builder qui compare les routes du serveur à la route wildcard du client. C’est terminé lorsque les routes concrètes peuvent être pré-rendues sans l’erreur de mismatch, tandis que la route wildcard continue d’être rendue côté serveur.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
angular, typescript
Domaine
build-system, web-dev
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
À l'abandon
Clarté
Plutôt claire
Accessibilité débutants
30/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.