Express Slashing Syndrome, the trailing slash and other topics
Ayyash
Posted on November 9, 2022
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, ...);
});
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`);
}
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
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
}));
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 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,
// ...
}
}
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});
});
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});
});
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
andclearnUrls
- Create
- 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
Posted on November 9, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.