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にサンプルのソースコードをアップしてます。カスタマイズ含め、自由にご利用ください。
需要がありそうなら、Electronでデスクトップアプリ化しようと思いますのでコメントいただけると幸いです。

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