diff --git a/Express Sla.txt b/Express Sla.txt new file mode 100644 index 0000000..e904145 --- /dev/null +++ b/Express Sla.txt @@ -0,0 +1,302 @@ +

+ + + 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:

+ + + +

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 +

+ + +