Adding server side Open Graph and SEO to an existing SPA
Tags: webdev, javascript, tutorial, ssr
Published: 2023-04-15 21:19:02

## Introduction

Imagine the following – you have a single-page application and you want the `<head>` to be dependent on the url. A common case would be that you want to have localised Open Graph meta tags depending on what language is in the query of the url. For example pasting [my-spa.com?hl=en](https://kopyl.dev) into Slack would give a title and description in English, but [my-spa.com?hl=fr](https://kopyl.dev) should be in French. 

This is the exact position I was put in, having a massive Vue website that was already built and receiving a request to localise the previews it provides when pasted into other websites or apps.

Your first instinct may be to say that this isn't something that can be done with Vue or that it requires a rewrite in an SSR framework: NEXT if you're on React, or NUXT if on Vue. But there is a simpler way if all you need to change are just change a couple of tags for SEO.

The idea is that we will serve our built SPA from a node server, but change some of the served page on the fly.

## Tutorial

First of all let's install some dependencies:

```sh
yarn add koa koa-static
```

And create `entryServer.js` at the root of your project and serve the app from it. In my case I had a Vue app built in the `dist` folder.
```javascript
// entryServer.js

const Koa = require('koa');
const fs = require('fs');

const app = new Koa();

app.use(async (ctx, next) => {
  const distHtml = fs.readFileSync('dist/index.html', 'utf8');
  ctx.body = distHtml;
});

const PORT = 3000;
console.log(`Listening on port ${PORT}`);
app.listen(PORT);
```

If you run `node entryServer.js` you will get an empty page on http://localhost:3000, because all of the javascript and assets can't be accessed, and are replaced with `dist/index.html`. To fix this lets serve them as static assets with [`koa-static`](https://github.com/koajs/static).

```javascript
// entryServer.js

const Koa = require('koa');
const fs = require('fs');
const serve = require('koa-static');

const app = new Koa();

app.use(async (ctx, next) => {
  // If a file is being requested then 
  // we skip to the middleware that handles 
  // static assets
  if (ctx.request.url.includes('.')) {
    await next();
    return;
  }

  const distHtml = fs.readFileSync('dist/index.html', 'utf8');
  ctx.body = distHtml;
});

// Serve all of the dist folder
app.use(serve('dist'));

const PORT = 3000;
console.log(`Listening on port ${PORT}`);
app.listen(PORT);
```

Now your app should behave as normal, but still just like an SPA. Let's add some server side logic. Here I have created several html files that all contain stuff that I want to change server side.

```html
<!-- head/en.html -->

<title>My cool website</title>
<meta name="description" content="With a cool description">
```

```javascript
// entryServer.js

const Koa = require('koa');
const fs = require('fs');
const serve = require('koa-static');

// Some files that contain only the 
// tags we are looking to localise, 
// e.g. <title>, <meta>
const heads = {
  en: fs.readFileSync('./head/en.html').toString(),
  de: fs.readFileSync('./head/de.html').toString(),
  fr: fs.readFileSync('./head/fr.html').toString()
};

const app = new Koa();

app.use(async (ctx, next) => {
  // If a file is being requested then 
  // we skip to the middleware that handles 
  // static assets
  if (ctx.request.url.includes('.')) {
    await next();
    return;
  }

  // Here we look at hl query param and 
  // select the respective seo tags, 
  // with English being the default
  const lang = ctx.request.query.hl;
  let head = heads.en;
  if (Object.keys(heads).includes(lang)) {
    head = heads[lang];
  }

  const distHtml = fs.readFileSync('dist/index.html', 'utf8');
  const body = distHtml.replace('</head>', `${head}\n</head>`);
  ctx.body = body;
});

// Serve all of the dist folder
app.use(serve('dist'));

const PORT = 3000;
console.log(`Listening on port ${PORT}`);
app.listen(PORT);
```

Now we have the finished server! If a user pastes [my-spa.com?hl=fr](https://kopyl.dev) into a website that parses Open Graph protocol the preview will be in French. And we didn't even need to waste hundreds of hours migrating to an SSR framework! Feel free to expand on this and add SEO for key pages like `/about`, `/contact`, etc.