When running an Angular SPA as public web-site of a company or organization you will usually come to the point to improve your SEO footprint. This will most probably end up in converting the Angular SPA (Single-Page-Application) into an Angular SPA with SSR - server side rending. This was called Angular Universal in the past. The update includes some chances and challenges not covered by this article - but we are eager to help you with that. The objective of this article is to convert existing re-directions when using Apache Web-Server or NGIX for previous versions of the site.

In course of converting an existing Angular-SPA to use latest benefits of SSR, you - as a developer - will ask yourself how to migrate existing re-directions specified typically in the '.htaccess'-file of an Apache Web-Server.

Well, in principal the answer is simple. Your Angular SSR application is using 'express.js' as web server. And 'express.js' is well suited to handle re-directions, probably with less friction than with e.g. traditional .htaccess-files of an Apache Web-Server.

Getting started

The best approach for a deep dive into the topic is tor create a new Angular project with SSR enabled.

  • Please make sure you have installed a recent version Angular CLI globally.
  • At the end you may not want to create a new project but to convert an existing Angular SPA into an Angular SPA with SSR, using the Angular CLI: ng add @angular/ssr command.
  • But for the sake of simplicity, let's create a new Angular project with SSR enabled.
ng n --routing --ssr --style=scss ng-starter-ssr

This will create a new Angular project with SSR enabled. And in the root directory of the project you will find the file server.ts. It includes the express-server and the Angular engine to render the pages. This piece of code is the heart of the SSR application. It includes the markant part:

export function app(): express.Express {
...

 // Serve static files from /browser
  server.get('**', express.static(browserDistFolder, {
    maxAge: '1y',
    index: 'index.html',
  }));

  // All regular routes use the Angular engine
  server.get('**', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });

  return server;
}

It is used to render the pages of the Angular application. inf function 'run()' it is used as

function run(): void {
  const port = process.env['PORT'] || 4000;

  // Start up the Node server
  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

What happens here is that the Angular engine is called to render the page or to deliver static content. In the first part of the function app() the static content is delivered. In the second part of the function app() the Angular engine is called to render the page.

The question

Where is the place to implement re-directions in the Angular SSR application?

  • We aim that re-directions are handled before the Angular engine is called to render the page or to deliver static content.

Well, the answer is simple: express supports middleware functions. And this is the place to implement re-directions. We will provide a middleware function to the express-server. This function will check the requested URL and decide if a re-direction is needed. If so, the function will apply the re-direction and return the result to the client. Otherwise the function will call the Angular engine to render the page or to deliver static content by calling 'next()' - following the pipeline concept of 'Express'.

The middleware function

We expect that you may have a bunch of re-directions to implement. And we want to handle them in a structured way, and not in a spaghetti code.

Therefore we define re-directions in a property object defined below - similar to a Map. The key of the map is the old URL and the value provides the redirectUrl and the statusCode.

const REDIRECTION_MAP: {
  [originalUrl: string]: {
    redirectUrl: string,
    statusCode: number
  }
} = {
  '/old-url': { redirectUrl: '/new-url', statusCode: 301 },
  '/old-url2': { redirectUrl: '/new-url2', statusCode: 301 },
  '/old-url-temporary-redirected': { redirectUrl: '/new-url-temporary-redirected', statusCode: 302 }
};

With that in place we can implement the middleware function to handle the re-directions. If the requested URL is found in the REDIRECTION_MAP the re-direction is applied. Otherwise the Angular engine is called to render the page or to deliver static content by calling next().

/**
 * Middleware to apply re-directs
 */
const appRedirects = (req: express.Request, res: express.Response, next: express.NextFunction) => {
  if (REDIRECTION_MAP[originalUrl]) {
    res.redirect(REDIRECTION_MAP[originalUrl].statusCode, REDIRECTION_MAP[originalUrl].redirectUrl);
  }
  next();
}

Final point is to add the middleware function to the express-server.

export function app(): express.Express {
  const server = express();
  ...
  // Apply re-directions
  server.use(appRedirects);
  // Serve static files from /browser
  server.get('**', express.static(browserDistFolder, {
    maxAge: '1y',
    index: 'index.html',
  }));

  // All regular routes use the Angular engine
  server.get('**', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: browserDistFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })
      .then((html) => res.send(html))
      .catch((err) => next(err));
  });
}

You just need to add the middleware function appRedirects to the express-server before the Angular engine is called to render the page or to deliver static content.

Testing re-directions

To test your re-directions, you will need to run:

  • npm run build to create a build in dist/<your project>/
  • npm run serve:ssr:<your project>

After that, you can test your re-directions - e.g. in another shell - using curl.

curl -v http://localhost:4000/old-url
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 4000 (#0)
> GET /old-url HTTP/1.1
> Host: localhost:4000
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< X-Powered-By: Express
< Location: /new-url
< Vary: Accept
< Content-Type: text/plain; charset=utf-8
< Content-Length: 42
< Date: Tue, 22 Oct 2024 18:38:45 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
Moved Permanently. Redirecting to /new-url

The code has been verified using some Angular v18 versions and release candidates of Angular v19.

Thanks for reading.