DEV Community

Cover image for Express Slashing Syndrome, the trailing slash and other topics
Ayyash
Ayyash

Posted on • Originally published at garage.sekrab.com

Express Slashing Syndrome, the trailing slash and other topics

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:

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

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

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.

// 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`);
}
Enter fullscreen mode Exit fullscreen mode

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. We just need to adjust it to create route.html files instead. Have a look at the express route generated in StackBlitz.

It generates something similar to this:

|--client
|----projects
|------1.html
|----projects.html
|--index.html
Enter fullscreen mode Exit fullscreen mode

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

// 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
}));
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 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 to allow cleanUrls. In addition to that, the domain/posts displayed posts.html correctly without adding a trailing slash.

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

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. And Prerendering in Angular.

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

// 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});
});
Enter fullscreen mode Exit fullscreen mode

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)

// 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});
});
Enter fullscreen mode Exit fullscreen mode

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

RELATED POSTS

Top comments (0)