Embedding a built Vue app in a Hugo post

I have a toy Vue.js project built using node.js, Vue CLI, and Webpack. The Vue CLI build script outputs a folder (called dist/ by default) that contains an index.html file as well as js/ and css/ subdirectories that hold all the files index.html links to.

I tested two primary ways of embedding this type of Vue app into a Hugo blog post:

  1. Copying the guts of the app’s index.html and pasting them into the post’s .md file., and
  2. Using an HTML <object> or <iframe> element to embed the Vue app’s entire index.html in the page.


The folder containing the app can be kept either in the Hugo site’s static/ directory (or a subdirectory of it), or in the page bundle (i.e. the directory) containing the post.

The optional vue.config.js file can be used to set two handy options before building the Vue project:

These can also be changed by hand after the app is built.

Here is an example of a vue.config.js file used for one of the above scenarios:

// vue.config.js
module.exports = {
  publicPath: '/vue/chooser-horiz-scroll/',
  outputDir: 'chooser-horiz-scroll'

General workflow

1. Pasting the links and <app> element into the blog post

Using this method, the Hugo page becomes the document as far as the app’s code goes. This could cause unanticipated behaviour related to, e.g., CSS and event listeners, set in the Vue app.

  Using absolute path Using relative path
Location of chooser-horiz-scroll/ /static/vue/ Same directory as post’s .md file
publicPath option (set in vue.config.js) /vue/chooser-horiz-scroll/ ./chooser-horiz-scroll/
Notes   When the code is within the blog post, any relative links need to be with respect to the location of the post’s Markdown file.

Paste the needed content of index.html into the post in order to bring in the other files from the app. We don’t need the <head>, <body>, or <meta> tags, nor do we need the favicon link, but the other <script>, <link>, and <noscript> tags stay.

In my case the result looked like:

<link href=/vue/chooser-horiz-scroll/css/app.048bd1fd.css rel=preload as=style><link href=/vue/chooser-horiz-scroll/js/app.5614f1b8.js rel=preload as=script><link href=/vue/chooser-horiz-scroll/js/chunk-vendors.95b82d44.js rel=preload as=script><link href=/vue/chooser-horiz-scroll/css/app.048bd1fd.css rel=stylesheet><noscript><strong>We're sorry but chooser-horiz-scroll doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/vue/chooser-horiz-scroll/js/chunk-vendors.95b82d44.js></script><script src=/vue/chooser-horiz-scroll/js/app.5614f1b8.js></script>

Because the generated files have different names after every build, updating the post after a change to the app requires (1) copying chooser-horiz-scroll/ into the Hugo project, and (2) replacing the HTML within the Markdown file for the post. Perhaps one could write an elaborate shortcode using readFile to read index.html, strip it of the unwanted bits and incorporate it into the post.

2. Embedding index.html itself using <object> or <iframe>

Embedding a Vue.js app using <object> or <iframe> isolates the app in its own document.

I wrote Hugo shortcodes to use the HTML <object> element to embed the app.

For apps located in the /static/vue/ directory:

{{/* vueobject.html */}}
{{- $appDirName := .Get 0 -}}
{{- $fullPath := (printf "%s%s" "/vue/" $appDirName) -}}
<object data="{{$fullPath}}" class="objectdata-localvue">Warning: file {{$fullPath}} could not be included.</object>

In use, it looks like (assuming the Vue CLI output directory is chooser-horiz-scroll):

{{< vueobject "chooser-horiz-scroll" >}}

For apps located in a subdirectory of the post’s branch bundle:

{{/* relvue.html */}}
{{- $appDirName := .Get 0 -}}
{{- $fullPath := (printf "%s%s%s" "/" .Page.File.Dir $appDirName) -}}
<object data="{{$fullPath}}" class="objectdata-localvue">Warning: file {{$fullPath}} could not be included.</object>

Use it like this:

{{< relvue "chooser-horiz-scroll" >}}
  Using absolute path Using relative path
Location of chooser-horiz-scroll/ /static/vue/ Same directory as post’s .md file
publicPath option (set in vue.config.js) /vue/chooser-horiz-scroll/ ./
Shortcode vueobject relvue
Notes Requires calling the post file _index.md, making the post directory a Hugo branch bundle. Not ideal because this makes the post look an empty section in my menus. Further understanding of page resources in a leaf bundle might reveal a way to avoid this.

Updating after rebuilding the app means simply copying the folder into the Hugo project, with no changes to the post.

Using <iframe> instead just requires changing <object data="{{$fullPath}}" to <iframe src="{{$fullPath}}". I had both working, and it’s not yet clear to me which, if either, is better practice. Many Hugo shortcodes use <iframe> to embed external content (such as videos).