Vue.jsとWordPressを共存させる（OGPにも対応）

地元のグルメ情報を紹介するWebアプリをVue.jsで作り、WordPressで運営しているサイトのサブディレクトリー上に公開しました。また。WebアプリのURLがSNSでシェアされた時に写真が展開されるよう、[OGP](https://ferret-plus.com/610)に対応させています。

この記事ではWordPressを使っているサイトにVue.jsのアプリを同居させ、プリレンダリングを使ってOGPに対応させる方法について紹介します。WordPressは https://example.com で運営され、そこにVueアプリを https://example.com/app-name/ で公開するものとします。

# 前提とする環境
[Vue CLI](https://cli.vuejs.org/)を使ってアプリを開発しています。

```console
$ npm install -g @vue/cli 
$ vue --version
@vue/cli 4.2.3
```

# ルート以外でVueアプリを公開する

開発したアプリはルートディレクトリ https://example.com/ で公開されるように作られています。これを、サブディレクトリ https://example.com/app-name/ で公開されるように変更します。

WordPressをサーバーのディレクトリー```/```にインストールしている場合、その下に新しく```/app-name```ディレクトリーを作り、Vueアプリの成果物をアップロードします。

最初はWordPressの動作をよく理解していなかったので、

- /app-name/ でアクセスできるページが必要？
- それなら「固定ページ」で app-name を作ればいいのでは？

と誤解していたのですが、そんなことは不要でした。

## vue.config.js を変更する

vue.config.js に publicPath [[公式](https://cli.vuejs.org/config/#publicpath)]、outputDir [[公式](https://cli.vuejs.org/config/#outputdir)] を追記します。

```javascript:vue.config.js
module.exports = {
  publicPath: '/app-name/',
  outputDir: 'dist/app-name/',
  ...
}
```

まず、開発用サーバーで正しく動作するかを確認します。

```console
$ npm run serve
...
 DONE  Compiled successfully in XXXXms

  App running at:
  - Local:   http://localhost:8080/app-name/ 
  - Network: http://XXX.XXX.XXX.XXX:8080/app-name/

  Note that the development build is not optimized.
  To create a production build, run npm run build.
```

Webブラウザーで http://localhost:8080/app-name/ にアクセスして、正しく動作することを確認します。

## public ディレクトリーに.htaccessを作成してビルド

WordPressはWebブラウザーが```/app-name```にアクセスした時に```/app-name/index.php```を読み込むように設定されています。その設定はサーバーの```/.htaccess```に書いてあります。
Vue.jsでは```/app-name```にアクセスした時に（index.phpではなく）```/app-name/index.html```を読み込むことを期待します。そこで、```/app-name/.htaccess```を準備して```/app-name```にアクセスしたときの動作を変更します。ここで作成する.htaccessファイルは開発マシンのpublicディレクトリーに作ります。

```apache:public/.htaccess
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /app-name/
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . index.html [L]
</IfModule>
```

参考：[WordPressのサブディレクトリでVueアプリを動かす](https://chmono.com/vue-apps-on-wordpress-subdirectory/)

次にビルドを実行して、dist/app-name/ に成果物が出力されることを確認します。なお、vue uiでbuildタスクを動かす場合、vue.config.js の outputDir が無視されます。UIからoutputを設定してください。

参考：[outputDir config not working #2639](https://github.com/vuejs/vue-cli/issues/2639)

```console
$npm run build
...
 DONE  Build complete. The dist/gourmet directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
```

## WordPressの.htaccessを書き換える

生成された dist/app-name をサーバーの```/app-name```としてアップロードします。ここで https://example.com/app-name にアクセスしても、まだVueアプリは動作しません。サーバー側の```/.htaccess```を修正して、```/app-name```へのアクセスだけは```/app-name/.htaccess```に書いたルールで動作させる必要があります。

修正を間違えるとWordPressが正しく動作しなくなるため、.htaccessファイルのコピーを別名で作ってから作業してください。

```apache:/.htaccess
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !(^/app-name/) # この行を追加
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
```

参考：[WordPressのサブディレクトリでVueアプリを動かす](https://chmono.com/vue-apps-on-wordpress-subdirectory/)

https://example.com/app-name にアクセスしてVueアプリが正しく動作することを確認してください。

# VueアプリをOGPに対応させる

OGPではシェアしたときに表示したい写真のURLなどをHTMLの<head>タグにある<meta>タグに記述します。そのページのURLがSNSでシェアされた時に、SNSのボットがそのURLにアクセスし、metaタグを解析することでSNSに表示する情報を決めています。

## VueアプリのトップページをOGPに対応させる

```public/index.html``` を編集して、OGPに対応させます。このファイルはビルドにより ```dist/app-name/index.html``` として出力されるのでそのままサーバーにアップロードして公開します。

```html:public/index.html
<!DOCTYPE html>
<html lang="ja">
  <head prefix="og:http://ogp.me/ns# fb:http://ogp.me/ns/fb# website:http://ogp.me/ns/website#">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="/favicon.ico">
    <title>[アプリ名]</title>
    <meta name="description" content="[アプリの説明]" />
    <meta property="og:type" content="website" />
    <meta property="og:site_name" content="[サイト名]" />
    <meta property="og:title" content="[アプリ名]" />
    <meta property="og:url" content="https://example.com/app-name/" />
    <meta property="og:image" content="https://example.com/app-name/img/app-name.jpg" />
    <meta property="og:description" content="[アプリの説明]" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@[アプリのTwitterアカウント名]" />
    <meta name="twitter:player" content="@[アプリのTwitterアカウント名]" />
  </head>
  <body>
    <noscript>
      <strong>We're sorry but vue_app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
```

公開後、OGPの情報が正しく設定されているかを確認するため、以下のツールにアプリのURLを入力して動作を確かめます。

- [Facebook シェアデバッガー](https://developers.facebook.com/tools/debug/)
- [Twitter Card Validator](https://cards-dev.twitter.com/validator)

## Vue Routerで作ったページをOGPに対応させる

OGP対応で問題になるのが、[Vue Router](https://router.vuejs.org/ja/)を使ったアプリです。

OGPの情報を収集するFacebookやTwitterのボットはJavaScriptに対応していません。なので、Vue Routerで生成したURLにボットがアクセスしてもOGPの情報を収集することがでず、そのURLをSNSでシェアしても、写真などが展開されることはありません。

そこでビルドの際にヘッドレスブラウザー[Puppeteer](https://github.com/puppeteer/puppeteer)を使ってプリレンダリングを実施し、Vue RouterのURLごとにindex.htmlを成果物として作成します。出来上がったindex.htmlをそのままサーバーにアップロードして公開すれば、ボットがOGPの情報を収集できるようになります。

### vue-meta を使ってURLごとにOGPを設定する

Nuxt.jsに含まれる[vue-meta](https://github.com/nuxt/vue-meta)を使えば、URLごとに異なるOGP情報を設定できます。

vue-metaを使う時に気をつける必要があるのは、変更する<meta>タグには data-vmid 属性を付与することです。

ここでは、```description```, ```og:title```, ```og:url```, ```og:image```, ```og:description``` の5つを変更するため、```public/index.html``` を次のように修正します。

```html:public/index.html
...
    <meta data-vmid="description" name="description" content="[アプリの説明]" />
    <meta data-vmid="og:title" property="og:title" content="[アプリ名]" />
    <meta data-vmid="og:url" property="og:url" content="https://example.com/app-name/" />
    <meta data-vmid="og:image" property="og:image" content="https://example.com/app-name/img/app-name.jpg" />
    <meta data-vmid="og:description" property="og:description" content="[アプリの説明]" />
...
```

その上で、vue-meta をインストールして、URLに応じて<meta>タグを書き換えるために metaInfo() を記述します。

```console
$ npm install --save vue-meta
```

```vuejs:Spot.vue
...
<script>
...
  data: {
    return: function() {
      id: '...',
      titie: '...',
      desc: '...'
    }
  },
  metaInfo() {
    return {
      title,
      meta: [
        { vmid: 'description', name: 'description', content: desc },
        { vmid: 'og:title', property: 'og:title', content: title },
        { vmid: 'og:description', property: 'og:description', content: desc },
        { vmid: 'og:url', property: 'og:url', content: `https://example.com/app-name/spots/${this.id}/` },
        { vmid: 'og:image', property: 'og:image',
          content: `https://example.com/app-name/img/${this.id}.jpg`
        }
      ]
    }
  }
}
</script>
```

参考：[メタタグが重複したときは？ - NuxtJS](https://ja.nuxtjs.org/faq/duplicated-meta-tags/)

### prerender-spa-plugin を使ってビルドの際にプリレンダリングを実施する

[prerender-spa-plugin](https://github.com/chrisvfritz/prerender-spa-plugin)を使ってビルド時に開発用サーバーを起動し、PupeteerでVueアプリにアクセスすることでHTMLを生成していきます。

ここではVueアプリのURLとして /app-name/spots/00001/ から /app-name/spots/00100/ が存在するとします。

```console
$ npm install --save prerender-spa-plugin
```

```staticDir``` は app-name なし、```indexPath```　は app-name ありで index.html まで指定することに気をつけてください。

なお、アクセスする先の /app-name/spots/:id は、アクセスしてから ```<div id="spot" />```　が表示されるまでに少し時間がかかるので、Puppeteer にそのタグが出るまで待たせるようにしています。皆さんのアプリには不要なので削除してご利用ください。

```javascript:vue.config.js
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer

module.exports = {
  publicPath: '/app-name/',
  outputDir: 'dist/app-name/',
  configureWebpack: () => {
    if (process.env.NODE_ENV === 'production') {
      const routes = []
      for (let i = 1; i <= 100; i++) {
        let id = String(i).padStart(5, '0')
        routes.push(`/app-name/spots/${id}/`)
      }
      return {
        plugins: [
          new PrerenderSPAPlugin({
            staticDir: path.join(__dirname, 'dist'),
            indexPath: path.join(__dirname, 'dist/app-name/index.html'),
            routes,
            renderer: new Renderer({
              renderAfterElementExists: '#spot'
            }),
          })
        ]
      }
    }
  }
}
```

あとはビルドを実行して、```dist/app-name/spots``` の下に 00001/index.html, ..., 00100/index.html がそれぞれ出力されて、<meta>タグに期待通りの情報が記述されていることを確認してください。

参考：[プリレンダリングを用いてVue.jsのSPAをビルドする導入から設定まで](https://www.ritolab.com/entry/182#aj_4)

問題がなければ```dist/app-name```をサーバーにアップロードし、Facebook デバッガーなどで https://example.com/app-name/spots/00001/ などが期待通りに動作するか確認してください。