Running JavaScript code & Getting Result from Go using V8 Engine


Read {count} times since 2020

I have a VueJS app, I want to make the <title> and <meta> tags to be populated in page source itself for better SEO.

I could use Server Side Rendering but it was difficult, express.js keeps crashing and there's JavaScript inconsistencies (ES Module imports didn't work in node). More explanation at the very bottom.

To solve this problem, I did something wild.

I made a Go server, served the JS app. On each request, the HTML file is processed by Go and title, meta tags are dynamically populated, rest of the page is just another VueJS app.

The title and meta tags depend upon the page URL (subdomain to be exact). The logic of how title and meta tags should be is already in the VueJS app, I'd have to rewrite the logic in Go.

To avoid that I now call the JS function from Go, get the result and use it to make <title> and <meta>. Calling JavaScript from Go is done via V8 Engine. There is a Go library called v8go that has C bindings for V8 Engine.

Serving a Vue App From Go

I'm using echo library to make a server easier.

func main() {
  e := echo.New()

  e.GET("/", HandleIndex)
  e.Static("/", "dist")

  e.Logger.Fatal(e.Start(":1323"))
}

The dist folder has the output from building JS (yarn build). A script of /assets/index.js in webpage will be in filesystem at /dist/assets/index.js.

Dynamically Making title and meta Tags

Modify index.html used by VueJS app. Insert a placeholder for the things we need to modify via Go.

<html>
  <head>
    <title>${title}</title>
    <meta itemprop="name" content="${title}" />
    <meta itemprop="description" content="${description}" />
    <!-- other stuff -->
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Our Go server will replace ${title} and ${description}. Echo library has a template engine which can be used for this but its syntax conflicted with Vue's syntax and yarn build failed because of it.

The title and meta tags are depended on the request hostname (subdomain to be exact) :

type PageInfo struct {
	title       string
	description string
}

var html string

func HandleIndex(c echo.Context) error {
	hostname, _, _ := net.SplitHostPort(c.Request().Host)
	pageInfo := getPageInfo(hostname)

	formattedHTML := strings.Replace(html, "${title}", pageInfo.title, 50)
	formattedHTML = strings.Replace(formattedHTML, "${description}", pageInfo.description, 50)

	return c.HTML(http.StatusOK, formattedHTML)
}

HandleIndex will be called on every request to /. We don't ned to make a filesystem read of the HTML file on every request (that'd be slow) :

func loadHTML() {
	fileBytes, err := ioutil.ReadFile("dist/index.html")
	if err != nil {
		log.Fatalln(err.Error())
	}
	html = string(fileBytes)
}

func main() {
  loadHTML()
  // rest of it
}

Calling JavaScript from Go

getPageInfo() can be anything. Here I'm using V8 Engine to call a JavaScript function that will give the page info :

import (
  v8 "rogchap.com/v8go"
)

var jsContext *v8.Context

func getPageInfo(host string) PageInfo {
	jsContext.RunScript("var siteInfo = getSiteInfo('"+host+"')", "call.js")
	title, _ := jsContext.RunScript("siteInfo.title", "retrieval.js")
	description, _ := jsContext.RunScript("siteInfo.description", "retrieval.js")

	return PageInfo{title.String(), description.String()}
}

jsContext is a context inside a V8 JavaScript VM. We need to make this VM and context once only :

func makeJSContext() {
	scriptBytes, err := ioutil.ReadFile("dist/utils.js")
	if err != nil {
		log.Fatalln(err.Error())
	}

	iso := v8.NewIsolate()
	jsContext = v8.NewContext(iso)

	script := string(scriptBytes)

	// dummy module object
	jsContext.RunScript("var module = {}", "module.js")

	_, err = jsContext.RunScript(script, "index.js")
	if err != nil {
		fmt.Println(err)
	}
}

func main() {
  makeJSContext()
  // rest of it
}

dist/utils.js has the JavaScript function that we need to call. If that function depends on external libraries, you'd need to bundle the JS and then call the script. V8go doesn't know the ES syntax to import files.

We can use esbuild for bundling. Add a build script to package.json :

{
  "scripts": {
    "build:utils": "esbuild src/utils.js --bundle --outfile=dist/utils.js --format=cjs",
  }
}

Here's a rough sketch of how utils.js looks like :

import * as punycode from "punycode";
import pages from "./pages/index.json";

export function getSiteInfo(host) {
  // ... stuff
}

esbuilding this would bundle all the dependencies (punycode, that index.json file) into a single file dist/utils.js. Note that we're not minifying the output file, so getSiteInfo() will still be named the same in output file.

To make things easier for deployment, here's a simple build script :

yarn build
yarn build:utils
go build .

I also use a restart.sh script for convenience :

kill $(ps aux | grep '[k]ittum' | awk '{print $2}')
./kittum >> web.log &

Final Result

That's it. See these subdomains to see the final result :

You can see the full source code at https://github.com/subins2000/kittum.com

More Context To The Problem

I'm making this site കിട്ടും.com, it's built in VueJS. The <title> & <meta> tags of the page is dynamic in nature. One can put any subdomain to the site and it'll have a page with text that depend upon that subdomain.

As you may know changing <title> and <meta> dynamically after page load is not good for SEO.

To solve this problem, they have a thing called "Server Side Rendering" (SSR), Vue pages will be generated in server and the HTML is sent to the browser. It works just as before.

SSR runs via node.js and most solutions use express.js, the HTTP server framework in node. I tried this and it was difficult to set it up from scratch. There is this project called "NuxtJS" which is an easier solution to make an SSR app. But my project കിട്ടും.com is very simple and doesn't need the complexities of Nuxt, so I avoided it.

Anyway I made an SSR site. Here are the problems I faced :

I had enough with this and decided to make a simple Go server that serves the JS site and dynamically makes the <title> and <meta> tags.

Why Go ? Varnam API server is written in Go and it hasn't crashed even once (uptime 100%). It serves more than 1,000,000 requests every month on a small ARM server.

Show Comments