The client calls for an early 7:00 am meeting and says they've talked with the SEO experts, and our site ranks really low on SEO, so we're not getting any interactions on Google search. This happened after a month of deploying our application. The client bluntly says, "What's the point of the business if people can't find us on Google?" That was indeed a very true statement, and I agreed with the notion. So, a cup of coffee later, I started digging around to find out how to make my Angular 18 application SEO-friendly.
After some digging, I came across Server-Side Rendering (SSR) and two packages, namely "@angular/platform-server" and "@angular/ssr".
If you look at angular.dev in the SSR section, they'll show you the three server files that will look something like this:
my-app
|-- server.ts # application server
└── src
|-- app
| └── app.config.server.ts # server application configuration
└── main.server.ts # main server application bootstrapping
So I went ahead and created the three server files in my application.
Before I go ahead and show the contents of the three server files, there was another change that I needed to make in my app.config.ts file.
I had to modify my providers to add two new Factory Providers, namely provideZoneChangeDetection, which comes from the '@angular/core' package, and provideClientHydration, which comes from the '@angular/platform-browser' package.
The provideZoneChangeDetection is self-explanatory in the sense that we're configuring the Angular application to use Zone.js for change detection, and the eventCoalescing option ensures that change detection runs only once when multiple events of the same type are triggered.
The provideClientHydration is solely for Angular Universal, which tells the application to reuse the server-rendered HTML on the client side when the Angular application starts up, instead of completely re-rendering the application
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideClientHydration()
...rest of the Factory providers
]
With these changes, we now move on to the server files.
We start with app.config.server.ts by defining the config like this:
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
The config passed into the mergeApplicationConfig is both the new server config as well as the previous config that we've changed to provide change detection and client hydration.
With these changes, we move to the main.server.ts.
The code in main.server.ts looks like this:
const bootstrap = () =>
bootstrapApplication(AppComponent, {
providers: config.appConfig.providers,
});
export default bootstrap;
The exported bootstrap module is going to be imported into the server.ts file, and server.ts will be our main server that serves our application for SSR.
It will look something like this.
export function app(): express.Express {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const indexHtml = join(serverDistFolder, 'index.server.html');
const commonEngine = new CommonEngine();
server.set('view engine', 'html');
server.set('views', browserDistFolder);
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;
}
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}`);
});
}
run();
at this point you may have seen that we are using express inside the server.ts file and that is correct , we are indeed using express to create a server and serve the application from there.
You can also see the bootstrap module getting passed into the render function of the common engine which comes from the angular/ssr package that we've installed in the beginning.
In terms of config we're 90% there . we now will have to make changes to the angular.json file to make sure that the builds will be ssr supported.
In your build section of angular.json after the scripts section add this
"extractLicenses": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true,
"server": "src/main.server.ts",
"prerender": true,
"ssr": {
"entry": "server.ts"
}
with this piece in place the configuration is now complete.
Now when you run your build script , you'll see that it will create another folder inside dist called server. you can create a script to serve the application from the dist folder like
"serve:ssr": "node dist/your-app-name/server/server.mjs",
You can notice that the view engine is mjs, I will not go into details of that piece of code , feel free to look at the server.ts file to modify that but with this you should have ssr ready.
Now when you use meta from @angular/platform-browser you should be able to see the tags created when you go and view the particular page source
something like this
import { Meta } from '@angular/platform-browser';
private metaService = inject(Meta);
this.metaService.addTag({
name: 'description',
content: 'Welcome to our page !',
});
The second piece is hosting the application using Firebase. Its really straight forward all we need to do is change the firebase.json file to look something like this.
"hosting": {
"public": "dist/your-application-name/browser",
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
we still want to deploy the client code, not the server code in firebase if you're using firebase deploy command like I was, so we're pointing to browser rather than server in our config. you should still be able to see the meta tags in the client bundle , due to the configuration change we've made in angular.json file. And with these steps I was able to see the meta tags and the content that was rendered in browser into the page source.
This has been my experience making Angular application SEO friendly. Thankyou for reading through the post.
Acknowledgements
This article was inspired by the Angular SSR implementations found on the following GitHub pages:
"ganatan/angular-ssr" (https://github.com/ganatan/angular-ssr)
"angular-university/angular-ssr-course" (https://github.com/angular-university/angular-ssr-course)
I would like to express my gratitude to the contributors of both projects for their valuable insights and code examples, which have greatly assisted me in creating this content.
Top comments (4)
I just tried that approach and unfortunately it does not cover dynamically updated tags. For example, I have blog app and need to get the data from server before I can show the title metadata. It is not present in page source, and I get the default metadata.
Yes, I've been having mixed behavior with that approach, i.e. loading up the meta info from a db. It is not consistent. For example, when I load a page, it takes the default data, but when I navigate to other pages, it loads up the metadata from the db. If I find a workaround for that, I'll definitely share it.
For now I'm using resolvers to set the dynamic data and it works as expected with no problems.
Wrong title, there is nothing about Firebase Deployment in this article.