head要素内のtitleやmeta情報を一括反映できるスクリプトを書いてみた

大量のHTMLにtitleやdescriptionを反映する機会があり、試しに一括で反映可能なスクリプトを書いてみました。

#実現できること
・エクセルに記載されているtitleやdescriptionを抽出し、該当するHTMLファイルに一括反映
・og関連やcanonicalタグなど、ページ固有の設定の一括反映

#サンプルのディレクトリ構成

> root /
├ public /
├ config.xlsx
├ package.json
└ head.js


- public 〜 html/css/jsなどの公開用のディレクトリ
- config.xlsx 〜　title、meta情報を記載したエクセル
- head.js 〜　エクセルファイルを解析し、htmlファイルに反映するスクリプト

#実装について
node.jsを使用し実装します。

## 使用するモジュール
- xlsx
- cheerio

以下コマンドでインストールしてください。

```
npm install xlsx cheerio --save-dev
```
## エクセルファイル
![2017-03-31.jpg](https://qiita-image-store.s3.amazonaws.com/0/126897/ddcd04e6-1313-eb7b-1d4c-a1186fdb408c.jpeg)


A列にファイルパス、B列にtitle、Cにdescriptionを記載しています。ファイルパスは"/"から始まるように記載します。

## ソースコード
```js:head.js
(function () {

  "use strict";

  /*
  * 設定
  */
  const userSetting = {
    // 対象のエクセルファイル
    xlsxFile: 'config.xlsx',

    // 対象のシート名   
    xlsxSheet: 'Sheet1',

    // 開始の行　※エクセル画像のNo.1
    startRowNum: 2,

    // 終わりの行 ※エクセル画像のNo.2
    endRowNum: 16,

    // ルートディレクトリ
    root: '/public',

    // canonicalをセットするかどうか
    isSetCanonical: true,

    // canonicalが存在している場合、上書きするかどうか
    isOverwrite: true,

    // サイトURL
    siteUrl: 'https://xxxxxxxxxx.com/'

  }

  const fs = require("fs");
  const xlsx = require("xlsx");
  const utils = xlsx.utils;
  const cheerio = require('cheerio');

  const getData = (filePath) => {
    return new Promise((resolve, reject) => {
      fs.readFile(filePath, 'utf8', (err, data) => {
        if (err) reject(err);
        resolve(data);
      });
    });
  }

  const replaceString = (str) => {
    str = str.replace(/"/g, "&quot;");
    str = str.replace(/'/g, "&apos;");
    str = str.replace(/</g, "&lt;");
    str = str.replace(/>/g, "&gt;");
    return str;
  }

  const file = xlsx.readFile(userSetting.xlsxFile);
  const sheet = file.Sheets[userSetting.xlsxSheet];

  for (let i = userSetting.startRowNum; i <= userSetting.endRowNum; i++) {

    let baseFilePath = (sheet["A" + i] !== undefined) ? sheet["A" + i].v : undefined;
    let title = (sheet["B" + i] !== undefined) ? sheet["B" + i].v : undefined;
    let description = (sheet["C" + i] !== undefined) ? sheet["C" + i].v : undefined;

    let relativePath = "." + userSetting.root + baseFilePath;

    if (relativePath !== undefined) {

      if (relativePath.slice(-1) === "/") {
        relativePath = relativePath + "index.html";
      }

      getData(relativePath).then((data) => {

        /*
        * 対象ファイルが存在しており、
        * htmlデータを取得できた時
        */

        let $ = cheerio.load(data, {
          decodeEntities: false
        });

        const setMetaContent = (elm, val) => {
          if ($(elm)[0] === undefined) {
          } else {
            if ($(elm)[0].attribs !== undefined) {
              $(elm)[0].attribs.content = val;
            }
          }
        }

        // title
        if (title !== undefined) {
          title = replaceString(title);
          $('title').text(title);
          setMetaContent('meta[property="og:title"]', title);
        }

        // description
        if (description !== undefined) {
          description = replaceString(description);
          setMetaContent('meta[name="description"]', description);
          setMetaContent('meta[property="og:description"]', description);
        }

        let ogUrl = userSetting.siteUrl + baseFilePath.substr(1);
        setMetaContent('meta[property="og:url"]', ogUrl);

        // canonicalの設定
        if (userSetting.isSetCanonical && relativePath.slice(-10) === "index.html") {

          // canonicalタグが存在しているかチェック
          let n;
          let isExist;
          let linkLength = $('link').length;
          for (let j = 0; j < linkLength; j++) {
            if ($('link')[j].attribs.rel === "canonical") {
              isExist = true;
              n = j;
            }
          }

          // 現状canonicalタグが存在する場合、上書き
          // 存在しない場合、新たにcanonicalタグを追加
          if (isExist && userSetting.isOverwrite) {
            $('link')[n].attribs.href = userSetting.siteUrl + baseFilePath.substr(1);
          } else {
            let canonical = '<link rel="canonical" href="' + userSetting.siteUrl + baseFilePath.substr(1) + '" >';
            $(canonical).appendTo("head");
          }
        }

        let html = $.html();
        fs.writeFile(relativePath, html, function (err) {
          if (err !== null) {
            console.log("error (" + relativePath + ")");
          }
        });

      }, (err) => {

        /*
        * 対象ファイルが存在していない場合
        */

        return true;

      });
    }
  }

})();

```

## 実行
コマンドプロンプトおよび、ターミナルで以下のコマンドを実行

```
node head.js
```

#補足事項
- head.jsのある場所がrootになる場合は、userSetting.rootを空にしてください

> root /
├ index.html
├ config.xlsx
├ package.json
└ head.js

```.js
const userSetting = {
  // ～ 中略 ～ 

  root: '',

  // ～ 中略 ～ 
}
```

#留意点
- 即席で作成したので細かいテストは行っておりません。不具合や誤作動でのクレームはご遠慮ください。
- Numbersで書き出したエクセルではうまく動作するか不明です。

#サンプル
githubにサンプルのソースコードをアップしてます。カスタマイズ含め、自由にご利用ください。
需要がありそうなら、何かしらでGUI化しようと思いますのでコメントいただけますと幸いです。

[https://github.com/TatsuyaNkmura/automatic-head-setting](https://github.com/TatsuyaNkmura/automatic-head-setting)