Change header color 🎲 🎲

Jake Paris

in Maine, in 2024

Using a Vue app within a WordPress plugin

Creating a full-fledged Vue.js application inside a WordPress plugin takes a little bit of special setup to the two code libraries working together. Here I’ll show the way I’ve been solving this, though I’m sure it’s not the only way.

I’m going to skip a number of prerequisites: I’ll assume you already have the vue-cli installed, and have a basic understanding of WordPress plugin-related functionality.

General plugin layout

I’ve structured the plugin like this:

  • Main Plugin Directory
    • main-plugin-file.php
    • package.json
    • /app
      • app.php
      • package.json
      • vue.config.js
      • /src
        • main.js
        • App.vue
        • …etc (all the Vue app stuff: /components, etc)
      • /build

Setting up

The first thing I do is use the Vue cli to install an application into the /app directory, which will setup some of the above structure. You will probably need to add the important vue.config.js file yourself, and we’ll get to what’s in there shortly.

Note that there are two package.json files; one in the plugin root, and one in the vue app directory. That will become useful later if/when we want to create WordPress Blocks. For now, add to the root-level package.json file some alias scripts that will run things within the vue directory:

...
"scripts": {
  "postinstall": "npm install --prefix ./app",
  "build:app": "npm run build --prefix ./app",
  "watch:app": "npm run serve --prefix ./app",
  ...
},
...

The –prefix flag tells npm to run as if inside the given directory, which has the effect of running the given scripts in the “app” directory’s package.json. But you can run them from the root directory, without having to fool around with changing directories. More information on npm folders.

The postinstall script tells npm to automatically install the subdirectory project after install the main project.

Vue.config.js

There are a few defaults in the vue-cli-service webpack script (which handles compiling the app) that are not helpful in the WordPress environment. We can override those in the vue.config.js file. This file will not get automatically created when creating a vue app with the cli, so just go ahead and create it in the ./app directory (root of the vue app).

Start out with

module.exports = {
}

and we’ll enter some useful keys. Let’s go over each one.

filenameHashing: false

This will ensure that the compiled javascript file will always have the same name. We need because WordPress is always going to look in the same place for the file, and we don’t want to have to update our wp_enqueue_scripts call every time the file updates.

publicPath: './'

This will force webpack to use relative paths, which is what is needed an embedded WordPress plugin.

outputDir: 'build'

This specifies what directory the built files should be placed in. This is not exactly needed, but I like to change it so I can have consistency across built Vue apps and WordPress blocks.

chainWebpack: config => {
  config.plugins.delete('html');
  config.plugins.delete('preload');
  config.plugins.delete('prefetch');
},

This stops webpack from building a full html file. Since we’re going to be injecting our app inside a WordPress page, a full html template is a waste at best.

devServer: {
  writeToDisk: true,
  // hot: false,

  // Optional next few lines depending on where/how your dev WordPress is setup
  https: true,
  disableHostCheck: true,
  headers: {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  }
}

Setting the devServer.writeToDisk key to true tells webpack to not just hot update the browser when running the watch/serve command, but instead to actually write the changes to the file. We’ll need this to happen if want to see our changes in the WordPress environment immediately on save.

devServer.hot false will save webpack from trying to refresh a page. I’ve found that this “hot reload” feature doesn’t work well or at all if the app is embedded on another page. Update May 2021: Getting the CORS/SSL issues worked out as described below caused the hot reload to actually work correctly, even when the app is embedded in a WordPress page.

The devServer keys from https down to the headers are not required. I have my local dev instance set up with a domain name like https://mydev.local and the webpack dev server doesn’t like talking to that for CORS reasons, and my dev domain doesn’t want to talk to the webpack dev server if it’s not over ssl. Adding these lines prevent a lot of naggy console errors.

productionSourceMap: false

Not totally needed, but stops create source maps for the production build. That gives you a faster build time and smaller bundle size at the cost of no dev tool troubleshooting in the prod environment.

css: {
  extract: true,
}

Normally this is only true for production builds (so not when in watch mode), but we need this to be true always so that our css file is always re-compiled when our templates change.

So here’s the complete vue.config.js:

module.exports = {
  filenameHashing: false,
  publicPath: './',
  outputDir: 'build',
  chainWebpack: config => {
    config.plugins.delete('html');
    config.plugins.delete('preload');
    config.plugins.delete('prefetch');
  },
  devServer: {
    writeToDisk: true,
    hot: false,
  },
  productionSourceMap: false,
  css: {
    extract: true,
  },
}

Including the app in WordPress

Now I create a php function which will load the app and it’s respective files. Then I can run that function inside a shortcode or in the block callback as desired.

function jp_doVueApp ( $atts=[] ) {
  // coming soon...
}
Note!

If you are including Lodash in your app, that might not play nicely with the Block Editor. Even though I inserted my app as a shortcode in the shortcode block (which doesn’t attempt to render anything in the admin, the lodash in my app overwrote the lodash in the block editor and I got The White Screen.

So be sure to use this at the top of your php function:

if( is_admin() )
  return;

Now we’ll get to actually including the app files. Webpack will place the vue lib (and lodash or whatever other dependencies you’ve got) into a chunk-vendors.js file in the output directory. First you’ve got to enqueue that.

wp_enqueue_script( 'my-vue-app-vendors',
  plugins_url( '/app/build/js/chunk-vendors.js', __FILE__ ),
  array(),
  '1.0.0'
);

Then include your actual app file using the chunk vendors as a dependency to ensure it gets loaded first.

wp_enqueue_script( 'my-vue-app',
  plugins_url( '/app/build/js/app.js', __FILE__ ),
  array( 'my-vue-app-vendors' ),
  '1.0.0'
);

And if you have styles, enqueue those too:

wp_enqueue_style( 'my-vue-app',
  plugins_url( '/app/build/css/app.css', __FILE__),
  array(),
  '1.0.0'
);

Lastly, return the html that the vue app will hook onto:

return '<div id="app"></div>';

You can change the id, just make sure it matches the app given to the Vue constructor in the app/src/main.js file.

Optionally Include WordPress Data

If you are including any data from the server that should be accessible to the app, you can do that by using wp_localize_script() above the return statement. Something like this:

$data = array(
  // get whatever data here
);
wp_localize_script('my-vue-app', 'my_vue_app_data', $data );

An alternate way to do this is to use the wp_add_inline_script function (docs), which is consider to be a better practice — or at least more semantically clear. That would look like this:

$js = <<<JS
window.my_vue_app_data = {
  someVar: true,
  someThings: [ 'foo', 'bar' ],
};
JS;
wp_add_inline_script( 'my-vue-app', $js, 'before' );

Be sure to include the third parameter of “before” so your variables are available to the app.

Whichever method you use, your $data object will be available as window.my_vue_app_data (or whatever variable name you used) in the vue app.

Lots of options open up here. You could also use some of the parameters from the function (or shortcode attributes), and pass those to the vue app using this method.

Of course, you can also do this after the fact by using the ajax admin url and setting up some Ajax functions to get the data.

Full php function

Here’s the full function, and I’ve just hooked it up to a shortcode for now.

function jp_doVueApp ( $atts=[] ) {
  if( is_admin() )
    return;

  wp_enqueue_script( 'my-vue-app-vendors',
	  plugins_url( '/app/build/js/chunk-vendors.js', __FILE__ ),
	  array(),
	  '1.0.0'
	);
  wp_enqueue_script( 'my-vue-app',
    plugins_url( '/app/build/js/app.js', __FILE__ ),
    array( 'my-vue-app-vendors' ),
    '1.0.0'
  );
  wp_enqueue_style( 'my-vue-app',
    plugins_url( '/app/build/css/app.css', __FILE__),
    array(),
    '1.0.0'
  );

  return '<div id="app"></div>';
}
add_shortcode( 'jp-vue-app', 'jp_doVueApp' );

Development & Production

To develop your app, be sure the plugin is enabled in WordPress. Then just run the standard npm run watch:app from the plugin’s root directory. Now simply insert your shortcode/block in the page, and your vue app will be there on your public page. Refresh the page manually as you make changes to your app.

When ready for production, don’t forgot to run npm run build:app to get the minified production build.

Hooking the Vue app up to a WordPress Block

You can also use a Block to set attributes which will get passed to the Vue app. Create your block, with whatever attributes you want, but I didn’t try to render the actual vue app itself inside the block editor (not even a serverSideRender) because I didn’t want to try to bootstrap Vue with React already in the mix. Instead I just displayed the controls in the block area.

Let the Block’s save function return null, and in the php register_block_type() function, in the render_callback property, return the jp_doVueApp function, passing along to it the $atts parameter directly.


Hat tip to Rocky Kev for pointing out a missing directory level. Thanks!