Express Slashing Syndrome, the trailing slash and other topics
Tags: webdev, javascript, productivity, angular
Published: 2022-11-09 12:00:00

Fix the trailing slash redirect once and for all
------------------------------------------------

The fix involves rethinking the static page generation to be named files, instead of folders. Following is how to go about it in Express, then later how to make use of it to fix the problem in Firebase and Netlify.

### Express the physical file

The original out-of-the-box Angular prerendering assumes that we have this line as a route rule:

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

// All regular routes use the Universal CommonEngine
server.get('*', (req, res) => {
  res.render(indexHtml, ...);
});
```

If we serve the browser-only version, then we cannot rely on the `res.render` to serve the static file. We need to manually do this.

```ts
// browser-only express server alternative (server.js)
// need to find file manually
server.get('*', (req,res) => {
	// construct the physical file path, removing any url params
	// root/client/{route}/index.html
	const static = config.rootPath + 'client/' + req.path.split(';')[0] + '/index.html';

	// using const existsSync = require('fs').existsSync;
  if (existsSync(static)) {
    res.sendFile(static);
    return;
  }
	// else send the usual index
	res.sendFile(config.rootPath + `client/index.html`);
}
```

This is the case with all browser-only applications, we always have to find the physical file first.

Note, always take notice that the `express.static` middleware is **not** serving our prerendered files. The `server.get` is essential in this setup.

### Express: extensions

I promised you a way to put this to sleep so you can sleep better. And that is by creating `route.html` files instead of `route/index.html` files. The pre-renderer cannot be Angular out-of-the-box builder, we previously spoke of creating our out builder, or [Express server to generate prerendered files](https://garage.sekrab.com/posts/prerender-routes-in-express-server-for-angular-part-i). We just need to adjust it to create `route.html` files instead. Have a look at the express route generated in [StackBlitz](https://stackblitz.com/edit/angular-express-prerender?file=host%2Fprerender%2Ffetch-route.js).

It generates something similar to this:

```
|--client
|----projects
|------1.html
|----projects.html
|--index.html
```

Now in our Express routes, we expose the client folder for all static assets, passing the `extensions` attribute to the static middleware.

```js
// we can now use the static middleware to serve assets
server.use(express.static(distFolder, {
  maxAge: '1y',
	extensions: ['html'],
	// also stop redirecting folders if found
	redirect: false
}));
```

### The curious case of express extensions and redirect

Normally, we have a single sub folder with static pages (blog posts) that we are eager to prerender. The URL pattern would be more consistent: `/posts/1`, `/post/2` and so on.

```
|--client
|----posts
|-------1.html
|-------2.html
// this one is trouble
|----posts.html
```

According to Express docs though, **the trailing slash will kick in first if the folder is found**. Thus, `domain/posts` will redirect to `domain/posts/`. Which does not exist. So it moves to the next rule.

To stop it from doing that, you'd think that adding `redirect: false` should do it. But it does not. It just stops redirecting to the trailing slash version, exhausting the static middleware and moves to the next rule, completely missing `/posts.html`.

**There is no solution to this issue.** Not by design.

Our way around it is to be selective. Why would we prerender the `posts` list that changes often? We don't need to. Or we may change the route to the posts list? If we think in terms of [standalone components](https://garage.sekrab.com/posts/how-to-turn-an-angular-app-into-standalone-part-i) that does not sound so horrible in Angular.

Firebase: cleanUrls
-------------------

For these pages to work properly in Firebase hosting, all we need is to change the `firebase.json` [configuration](https://firebase.google.com/docs/hosting/full-config#control_html_extensions) to allow `cleanUrls`. In addition to that, the `domain/posts` displayed `posts.html` correctly without adding a trailing slash.

```json
// firebase hosting config
{
  "hosting": [
  {
    "target": "web",
    "public": "client",
		// this will allow /posts/1.html to be served as /posts/1
    "cleanUrls": true,
    // ...
  }
}
```

Netlify
-------

The default behavior of Netlify is to look for `route.html` files before running rewrite rules in `netlify.toml` file. So the above solution works well. In addition to that, the `domain/posts` displayed `posts.html` correctly without adding a trailing slash.

> Neither Netlify nor Firebase suffer from the **Express Slashing Syndrome**. ESS. That's whatchamacallit.

Digressing
----------

Going back to our never ending quest to replace Angular localization with one that serves multiple languages in one build, let's try to rewrite some Express rules to fix the trailing slash issue.

Here are the four scenarios you would have read about in the previous series: [Twisting Angular Localization](https://garage.sekrab.com/posts/alternative-way-to-localize-in-angular). And [Prerendering in Angular](https://garage.sekrab.com/posts/prerender-routes-in-express-server-for-angular-part-i).

### Cookie driven app

For a browser-only app, or SSR app, when the language is based on a cookie, the prerendering rules in Express look like this

```js
// server/routes.js cookie driven multilingual
// serve assets first
app.get('*.*', express.static(rootPath + 'client'));
// use static middleware for all static language urls (generated for prerendering)
app.use(express.static(rootPath + 'client/en', {extensions: ['html']}));
app.use(express.static(rootPath + 'client/tr', {extensions: ['html']}));
// ...

// then serve normally
app.get('/*', (req, res) => {
  // serve index file for all urls for browser-only
  res.sendFile(rootPath + `index/index.${res.locals.lang}.html`);
  // or this for SSR
  res.render(rootPath + `index/index.${res.locals.lang}.html`, {
    req, res});
});
```

We cannot rely on the `CommonEngine` in this case because the route does not match the physical file path. The physical file is inside a `en` or `tr` physical folder, and it ends with an `html`. We can choose to fetch the physical file ourselves though, instead of the static middleware train.

### URL driven app

When the language is based on the URL, and the physical folder reflects the same URL, but not the file name (`route.html`)

```js
// server/routes.js url driven multilingual

// use static files in client, we cannot use get("*.*") here
app.use('/:lang', express.static(rootPath + 'client', {redirect: false}));

// to prerender /lang/route.html, open up client on root,
app.use(express.static(rootPath + 'client', {extensions: ['html'], redirect: false}));

// then serve languages as usual
app.get(config.languages.map(n => `/${n}/*`), (req, res) => {
  // browser-only
  res.sendFile(rootPath + `index/index.${res.locals.lang}.url.html`);
  // or this for ssr
  res.render(rootPath + `index/index.${res.locals.lang}.url.html`, {req, res});
});
```

Note: in all of our attempts to prerender, we rarely talked about the index homepage. The general rule is that it's acceptable to redirect the root to a trailing slash, because the Angular itself will treat the base of the app with an additional slash once hydrated. Thus the client app, and express behavior, are in sync.

Conclusion
----------

What we learned from this series:

-   Redirecting and showing up in Google search console as redirect error does not affect searchability
-   The `CommonEngine` in Angular Universal takes care of displaying the pre-rendered version in all the usual setups
-   In unusual setups, we have two options
    -   Create `route/index.html` files, and check physical file before serving root index
    -   Create `route.html` file, and create enough rules to serve it
    -   Use hosting configuration like `trailingSlash` and `clearnUrls`
-   ESS: Express Slashing Syndrome is a real thing y'all!

My final advice: Search Console has a mind of its own, don't sweat over it. Keep creating awesome content, and duck, duck, Go! 😉

### RESOURCES

-   [Firebase hosting configuration](https://firebase.google.com/docs/hosting/full-config#control_html_extensions)
-   [Express static middleware](https://expressjs.com/en/4x/api.html#express.static)
-   [Extensions vs Directory Express issue](https://github.com/expressjs/serve-static/issues/138)

### RELATED POSTS

-   [Prerender routes in Express server for Angular - Part I](https://garage.sekrab.com/posts/prerender-routes-in-express-server-for-angular-part-i)

-   [Serving multilingual Angular application with ExpressJS](https://garage.sekrab.com/posts/serving-multilingual-angular-application-with-expressjs)

-   [How to turn an Angular app into standalone - Part I](https://garage.sekrab.com/posts/how-to-turn-an-angular-app-into-standalone-part-i)