Vue.js SSR meta tags for .NET Core and JavascriptServices

This post is an addition to "Vue.js server side rendering with ASP.NET Core", written by Mihály Gyöngyösi. Check it out before reading this post!

There are good reasons to have your meta tags resolved through SSR. Most prominently SEO, but also a good alternative to the people without javascript enabled in their browsers.

To enable meta tags in your server rendered application you need to do a couple of things.

  • npm install vue-meta and register the package.
  • Expose the resolved meta object in the render.js file.
  • Load that object through ViewBag.

Adding vue-meta to the app

We'll be using the vue-meta package. This package gives you the possibility to add a property in the vue component to define the tags that belong in the head tag.

<template>
  ...
</template>

<script>
  export default {
    metaInfo: {
      title: 'My Example App', // set a title
      titleTemplate: '%s - Yay!', // title is now "My Example App - Yay!"
      htmlAttrs: {
        lang: 'en',
        amp: undefined // "amp" has no value
      }
    }
  }
</script>

Very powerful. To utilize this in the application you must register this package in vue.js.

import Vue from 'vue';
import VueMeta from 'vue-meta'

Vue.use(VueMeta);

You can read more about this package on the vue-meta github page. The above code will insert $meta into your vue.js instance. The client side rendering will be handled in the module without any additional configuration. To make this work with SSR, however, will require some extra setup.

Hooking up vue-meta to the server.js file

Now we have $meta exposed in our instance, we may access it from the server loaded files.

import { app, router, store } from './app';
const meta = app.$meta();

export default context => {
    return new Promise((resolve, reject) => {
        router.push(context.url);
        context.meta = meta;
        router.onReady(() => {
            const matchedComponents = router.getMatchedComponents();
            if (!matchedComponents.length) {
                return reject({ code: 404 });
            }
            Promise.all(matchedComponents.map(component => {
                if (component.asyncData) {
                    return component.asyncData({
                        store,
                        route: router.currentRoute
                    });
                }
            })).then(() => {
                context.state = store.state;
                resolve(app);
            }).catch(reject);
        },
        reject);
    });
};

$meta is initialized and delivered to the context. The context now contains a "meta" property. Now we can add that to the module that renders the markup and delivers it to the controller.

process.env.VUE_ENV = 'server';

const fs = require('fs');
const path = require('path');

const filePath = path.join(__dirname, '../wwwroot/dist/main-server.js');
const code = fs.readFileSync(filePath, 'utf8');

const bundleRenderer = require('vue-server-renderer').createBundleRenderer(code);
const createServerRenderer = require('aspnet-prerendering').createServerRenderer;

module.exports = createServerRenderer(function (params) {
    return new Promise(function (resolve, reject) {
        const context = { url: params.url };  

        bundleRenderer.renderToString(context, (err, resultHtml) => {
            if (err) {
                reject(err.message);
            }
            const {
                title,
                link,
                style,
                script,
                noscript,
                meta
            } = context.meta.inject();

            const metadata = `${meta.text()}${title.text()}${link.text()}${style.text()}${script.text()}${noscript.text()}`;

            resolve({
                html: resultHtml,
                globals: {
                    __INITIAL_STATE__: context.state,
                    Metadata: metadata
                }
            });
        });
        
    });
});

We generate a section of markup that vue-meta returns in a string. This value will be resolved and injected into JavascriptServices.

Render the markup!

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.Prerendering;

namespace MySite.Controllers
{
    public class HomeController : Controller
    {
        private readonly ISpaPrerenderer _spaPrerenderer;

        public HomeController(ISpaPrerenderer spaPrerenderer)
        {
            _spaPrerenderer = spaPrerenderer;
        }
        public async Task<IActionResult> Index()
        {
            var result = await _spaPrerenderer.RenderToString("./Vue/render");

            ViewData["PrerenderedHtml"] = result.Html;
            ViewData["PrerenderedGlobals"] = result.CreateGlobalsAssignmentScript();
            ViewData["PrerenderedMetadata"] = result.Globals["Metadata"];

            return View();
        }
    }
}

The part where this post deviates from the original is delivering the rendered html and initial state in the Controller. In addition we expose the meta property in the ViewBag. Now we can add this property in the "_Layout.cshtml" view. Replace the asp-prerender tags with the resolved rendered html.

<!doctype html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" />
    @Html.Raw(ViewData["PrerenderedMetadata"])
    @RenderSection("styles", required: false)
</head>
<body>
    @RenderBody()
    @RenderSection("scripts", required: false)
</body>
</html>

Remember to add the rendered app as well!

<div id="app-root">@Html.Raw(ViewData["PrerenderedHtml"])</div>
<script>@Html.Raw(ViewData["PrerenderedGlobals"])</script>

Now you can reload the page and see that the tags in the head is rendered!

That concludes our vue-meta .NET Core SSR tutorial. I hope you found this post useful :)