Server-Side Rendering Meta Tags with Firebase

In this article, I’m going to discuss how and why I implemented server-side rendering with Firebase for my project marquest.io.

Why server-side rendering (SSR)? And why just for meta tags?

Common claims about SSR are that it improves performance and SEO. Those may be true in some circumstances, but depending on the type of site, SEO may not be important. Performance is almost always important, but SSR may not benefit the performance metrics that matter for your site. As always, you want to consider the tradeoffs of implementing any feature, and one big tradeoff with SSR is the complexity it can introduce.

This isn’t an article about performance or SEO necessarily. My point is that in this instance I don’t care to have SSR for performance or SEO reasons. For marquest.io, I only care about rendering meta tags for social sharing reasons. This seems a common scenario to me, so I wanted to share how I set it up with Firebase.

Meta Tags

Meta tags are HTML tags that define metadata about the document. They go in the head of the HTML document and look as follows:

I need SSR because I have user-generated content intended for sharing on social media. When they share their content it should have whatever title and description they give it. Not some generic text.

<meta name="title" content="{title}">
<meta name="description" content="{description}">

<meta itemprop="name" content="{title}">
<meta itemprop="description" content="SSR for social meta tags">
<meta itemprop="image" content="https://www.srmullen.com/images/{image}">

<meta property="og:type" content="website">
<meta property="og:url" content="https://srmullen.com/articles/firebase-social-meta">
<meta property="og:title" content="Server Side Rendering (SSR) for Meta Tags">
<meta property="og:description" content="SSR for social meta tags">
<meta property="og:image" content="https://www.srmullen.com/images/{image}">

<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://www.srmullen.com/articles/firebase-social-meta">
<meta property="twitter:title" content="Server Side Rendering (SSR) for Meta Tags">
<meta property="twitter:description" content="SSR for social meta tags">
<meta property="twitter:image" content="https://www.srmullen.com/images/{image}">

These are some of the tags I’m using on this page. They describe the URL, title, description, and image. Everything a social media site would need to create a nicely formatted card for the link you shared. Other attributes can be used, but these are the ones I care about. There are a few variations because different sites will expect different tags. Not surprisingly, Twitter reads the “twitter:” prefixed attributes. The “og:” attributes are defined by the Open Graph Protocol, and are used by Facebook and probably many other sites. Rather than just displaying a boring text URL, social media sites might show something like the following image.

meta tag card

That would get more clicks than just some text, for sure! This image was created using Hey Meta. A great site for previewing and generating meta tags. I use it to test out that meta tags for my site appear as I’d expect.

Social media sites scrape the meta tags from your site to create these cards. They likely aren’t going to execute any javascript on your page, so even if you wrote code that changes the meta tags at runtime, it won’t make a difference for the preview cards. That means the HTML needs to be generated server-side. Users of marquest.io share questionnaires that they created, and they need them to display on social media with the correct titles and descriptions. Here’s how I achieve that with Firebase.

Setting up Firebase

The majority of marquest.io is client-side rendered using Svelte. The static files are built and deployed to firebase hosting. Routing is done on the client, so in the firebase hosting config, rewrites are set up so all paths return the index.html file.

/* firebase.json */
{
  "hosting": {
    {
      "source": "**",
      "destination": "/index.html"
    }
  }
}

We need to use Firebase-functions to generate the metatags, so start by enabling functions for your project. The HTML will be generated when certain paths on the website are visited. In the case of marquest.io this path is /q/:id, where :id is replaced with the id of the questionnaire. This is the path a user would go to provide answers to a questionnaire. We need to add this to the hosting configuration, so instead of the index.html file being delivered, the cloud function will run instead. It will now look like this…

{
  "hosting": {
    {
      "source": "/q/*",
      "function": "addsocialmeta"
    },
    {
      "source": "**",
      "destination": "/index.html"
    }
  }
}

It is placed before the catchall "**" route. Otherwise the "**" would match this path as well and return index.html. That’s not what we want. Rather than provide a destination to return, we give function, addsocialmeta, that will be called instead. Let’s write that function now.

The cloud function

The cloud function is an onRequest function because it is called directly with an HTTP call rather than using the Firebase SDK. Let’s add it to the index.js file in the cloud function directory.

exports.addsocialmeta = function.https.onRequest(async (req, res) => {
  // Render the html
});

Let’s start by generating just the meta tags. It will be a function called createTags that takes an id that we’ll use to get the meta tags content. This id is in the request path. I use an npm module called path-parser (npm install path-parser in the functions directory) to get the id. It looks like this…

const { Path } = require('path-parser');

async function createTags(id) {
  const { title, description, link, image } = await gettingTheDataFromFirestore(id);

  return `<meta property="og:type" content="website">
  <meta property="og:url" content="${link}">
  <meta property="og:title" content="${title}">
  <meta property="og:description" content="${description}">
  <meta property="og:image" content="${image}">`;
}

exports.addsocialmeta = function.https.onRequest(async (req, res) => {
  const path = new Path('/q/:id');
  const { id } = path.test(req.path);
  const tags = await createTags(id);
});

I’m only showing the “open graph” tags here to save space. Adding the other tags is straightforward.

Now, all we need to do is insert the tags into some HTML. The HTML template is the same as the index.html file the rest of the application uses, except that the social meta tags are replaced with just a placeholder ___META_TAGS___. Replacing the placeholder is as simple as using a String’s replace method. And we’re ready to send the HTML on its way!

const template = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset='utf-8'>
  <meta name='viewport' content='width=device-width,initial-scale=1'>

  <!-- insert meta tags here -->
  ___META_TAGS___

  <!-- Rest of head content -->
</head>

<body>

  <script defer type="module" src="/_dist_/main.js"></script>
</body>
</html>`;

exports.addsocialmeta = function.https.onRequest(async (req, res) => {
  // Use the path-parser module to get the id
  const path = new Path('/q/:id');
  const { id } = path.test(req.path);
  // Generate the meta tags
  const tags = await createTags(id);
  // Insert the meta tags into the html template
  const html = index.replace('___MATA_TAGS___', tags);
  res.send(html);
});

Now the tags are being generated on the server and will display correctly on social media sites. But there’s one last step to take, and that’s caching the response.

Caching the response

There’s no reason to generate the HTML for every subsequent request for the same page. Doing that would be slower to respond to requests and cost more money because the cloud function would spend more time running. Firebase allows the function response to be cached by setting a Cache-Control header on the function’s response.

// Cache the response for 10 minutes
res.set('Cache-Control', 'public, max-age=600, s-maxage=1200');
res.send(html);

And we’re done. User-generated content can now display with more accurate information when shared on social media sites.