Fixing the trailing slash in Static Site Generation
Tags: angular, webdev, staticwebapps, firebase
Published: 2022-11-07 12:37:44

An issue with prerendering, Angular or other, is that it creates an index.html file inside a route folder. The default behavior of different hosts is to add a trailing slash to serve that static file. Can we fix that?

The first call to our SPA preferred `domain.com/route` will **301 redirect** to `domain.com/route/` with a **trailing slash**. When hydrated, it would rewrite location back to the original, without registering any network redirects. According to a very old article on [Google Search Central](https://developers.google.com/search/blog/2010/04/to-slash-or-not-to-slash), it has no effect on searchability. Nevertheless, it made a lot of people [nervous](https://support.google.com/webmasters/thread/120740934/trailing-slash-301-redirect-issue-in-google-search-console?hl=en) since the search console displays the following when trying to crawl prerendered URLs.

![Search console dipping with redirects](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f2wjekrokh9caw7z5i82.png)

Let's address this issue and try to fix it.

### Prerendering

There are multiple ways to generate pre-rendered pages, one of them is through the built-in Angular builder,

`ng run prerender`

Another is through Express. Read about how to [Prerender Angular in Express](https://garage.sekrab.com/posts/prerender-routes-in-express-server-for-angular-part-i) in a previous article.

All of the normal ways create `index.html` pages with the content, inside folders reflecting the route:

`domain/{route}/index.html`

That in the world of hosting means if user browses to `domain/route` it automatically redirects to `domain/route/` and displays the index file. That redirect is a **301.**

![Image on Sekrab Garage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9864dssn41u3kfauhhzj.png)

Following are solutions to this problem

### Angular solution: out of the box

Here is how Angular defines the `server.ts` to take care of this problem:

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

// All regular routes use the Universal CommonEngine
server.get('*', (req, res) => {
  res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
```

If you pay close attention, this has a `server.get(*.*)` for all static files (JavaScript and CSS). The pre-rendered pages are served by the second rule.

In `ngExpressEngine`, the `CommonEngine` [checks if the page path already exists](https://github.com/angular/universal/blob/main/modules/common/engine/src/engine.ts#L72), it serves the file, else rolls back to SSR generated `main.js`. This is not always an option.

```ts
// CommonEngine code
async render(opts: RenderOptions): Promise<string> {
	  // ...
    if (opts.publicPath && opts.documentFilePath && opts.url !== undefined) {
       // ...
        if (pageExists) {
          // Serve pre-rendered page.
          return fs.promises.readFile(pagePath, 'utf-8');
        }
      }
    }
}
```

One instance that won't work is **cloud hosting that prefers static files first**. Like Firebase or Netlify.

> Note: in order to test the behavior, keep the Network tab open, and turn off `JavaScript` as well. You will spot the **301** right at the top.

Firebase solution: trailingSlash
--------------------------------

What happens is that it looks for the physical file, before it serves the `ssr` serverless function (or the general rewrite to index file if you are not using SSR). The default behavior is to add a trailing slash when it finds the `index.html`.

[Firebase hosting configuration:](https://firebase.google.com/docs/hosting/full-config#control_trailing_slashes) `trailingSlash` can take care of this issue.

```json
// firebase.json
{
  "hosting": {
    "public": "functions/public",
     // Removes trailing slashes from URLs
		 "trailingSlash": false
     // ...
  }
}
```

This displays `/route/index.html` as `/route`, without a **301**. But it would redirect `/route/` to `/route` with a **301**. If you never use `/route/` anywhere, not in your sitemaps, nor inline links, nor when you share on social media, you should be fine. If however it still bugs you, there is another solution to put this to rest, that solution works well in Express servers as well. We will discuss this solution in the next episode.

### Netlify solution, or lack thereof

Physical file preference will kick in first, before it runs the `netlify.toml` hosting file. The default is to add the `/` if it finds an `index.html`. According to their [documentation](https://docs.netlify.com/routing/redirects/redirect-options/#trailing-slash) there is a way to unify all links to have a trailing slash. In an Angular app, **this is *not* what we are looking for**. It rather is the other way around.

There is another default behavior of Netlify: to look for `/route.html` if user browses to `/route`. And **that is what we want**. In fact, that is the only way it's done on Netlify. The solution then is to rewrite our prerender function to generate `/route.html` instead of `/route/index.html`. This also will be covered in the next episode along with Express server solution.

> Note: Netlify documentation states that you need to enable Pretty Urls to make use of this behavior. I tried it. It works without tampering with settings.

Stay tuned for the next episode coming soon.

### RESOURCES

-   [Google Search Central Blog](https://developers.google.com/search/blog/2010/04/to-slash-or-not-to-slash)
-   [Trailing slash 301 redirect issue (In google search console )](https://support.google.com/webmasters/thread/120740934/trailing-slash-301-redirect-issue-in-google-search-console?hl=en)
-   [Angular CommonEngine](https://github.com/angular/universal/blob/main/modules/common/engine/src/engine.ts#L72)
-   [Firebase hosting configuration for trailing slash](https://firebase.google.com/docs/hosting/full-config#control_trailing_slashes)
-   [Netlify hosting configurations for trailing slash](https://docs.netlify.com/routing/redirects/redirect-options/#trailing-slash)

### RELATED POSTS

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

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