Nuxt.jsで作成したAPIで、Markdownの書式を自動的に生成

# 要約

- MarkdownのリンクとかLaTeXの\\hrefを作成するときにwebページのタイトルを調べるのが面倒
- タイトルを取得するプログラムをクライアント環境で実行するとCORS違反
- サーバ環境でタイトルを取得するためにNuxt.js環境にAPIを作成

# はじめに

MarkdownとかLaTeXで文章を書いていて、webページへのリンクを作成するために、こんなことをしていました。もっと便利な方法があるのかもしれませんけど。

1. リンクを作成したいページをブラウザで開く。
1. ctrl-Uでソースコードを表示する。
1. ctrl-Fでtitleタグを検索して、`<title>`タグを見つける。
1. `<title>`と`</title>`の間をマウスで選択してから、ctrl-Cでコピーする。
1. Markdownだったら、[と]の間にコピーしたタイトルを貼り付けて、その後ろの(と)の間にwebページのURLを貼り付ける。

これでは、プログラマの三大美徳の「怠慢」を違反しますので、ツールを作りました。node.jsの開発サーバー環境で動作するツールです。http://localhost:3333にアクセスします。他のnode.jsの開発サーバー環境とポートを競合しないように、デフォルトのポート番号から変更しています。

[GitHub \- kubotama/must: MarkUp Support Tool](https://github.com/kubotama/must)

テキストエリアにURLを入力して、ボタンをクリックするとリンクの形式に変換します。上のGitHubへのリンクも、このツールで作成しました。他にもLaTexの\\hrefの形式に変換したり、MarkdownやLaTeXの特殊文字のエスケープ処理をします。

# APIの作成

クライアント環境で実行するJavaScriptのプログラムからwebページにアクセスしてタイトルを取得しようとするとCORSに違反します。そのため、クライアント環境から直接アクセスするのではなく、指定したwebページにアクセスしてタイトルを取得するAPIを作成します。

## nuxt.config.jsの設定

Nuxt.jsではnuxt.config.jsにserverMiddlewareを設定することで、APIを作成できます。

```javascript:nuxt.config.js
  },
  serverMiddleware: [{ path: '/api', handler: '~/api/index.js' }]
}
```

<http://localhost:3333/api>にアクセスすると、Nuxt.jsのプロジェクトディレクトリの直下のapiディレクトリに作成したindex.jsがが呼びされます。

## APIとして呼び出されるスクリプト

タイトルを取得したいwebページが<http://www.google.co.jp>だとすると、クライアント環境からは、<http://localhost:3333/api?url=http://www.google.co.jp>にアクセスします。

```javascript:/api/index.js
import url from 'url'
import { JSDOM } from 'jsdom'
import HttpStatus from 'http-status-codes'
import axios from 'axios'

export default (req, res, next) => {
// 1. クライアント環境からURLで引き渡されたwebページのアドレスを取得
  const targetUrl = new url.URL(req.url, 'http://localhost').searchParams.get(
    'url'
  )

// 2. 取得したアドレスのwebページのコンテンツをaxios.getで取得
  axios.get(targetUrl).then((response) => {
    const body = response.data

// 3. 取得したwebページのコンテンツからjsdomモジュールでタイトルを取得
    const dom = new JSDOM(body)
    const title = dom.window.document.getElementsByTagName('title')[0].textContent

// 4. 取得したタイトルをJSON形式のレスポンスとしてクライアント環境に返信
    res.writeHead(HttpStatus.OK, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ title }))
  })
}
```
このプログラムでは以下の処理をします。

1. クライアント環境からURLで引き渡されたwebページのアドレスを取得
1. 取得したアドレスのwebページのコンテンツをgetで取得
1. 取得したwebページのコンテンツからjsdomモジュールでタイトルを取得
1. 取得したタイトルをJSON形式のレスポンスとしてクライアント環境に返信

## jsdomモジュールを利用するための設定

APIの作成とは直接関係ありませんが、Nuxt.js環境でjsdomモジュールをimportしてyarn devを実行するとエラーが発生するため、nuxt.config.jsに以下の設定が必要です。

```javascript:nuxt.config.js
  build: {
    /*
     ** You can extend webpack config here
     */
    extend(config, { isDev, isClient }) {
      if (isClient) {
        config.node = {
          fs: 'empty',
          child_process: 'empty',
          tls: 'empty',
          net: 'empty'
        }
      }
    }
  },
```

# クライアント環境

## テキストエリアとボタン

webページのアドレスを入力するテキストエリアと、ボタンが表示されています。ボタンをクリックすると、変換結果でテキストエリアを更新します。

```vuejs:MustUi.vue
<template>
  <div>
    <div>
      <el-input
        id="mustArea"
        v-model="mustArea"
        :rows="10"
        type="textarea"
        placeholder="変換する文字列を入力してください"
      >
      </el-input>
    </div>
    <div>
      <el-button id="btnLinkMd" @click="clickLinkMd">リンク</el-button>
    </div>
  </div>
</template>
```

## スクリプト

ボタンをクリックされると、テキストエリアに入力されているアドレスを引数にして、APIを呼び出して、webページのタイトルを取得します。取得したタイトルを使って作成したリンクをテキストエリアに設定します。

```vuejs:MustUi.vue
<script>
export default {
  name: 'MustUi',
  data() {
    return {
      mustArea: ''
    }
  },
  methods: {
    getTitle(url, callback) {
      this.$axios
        .get(`${location.protocol}//${location.host}/api?url=${url}`)
        .then((response) => {
          this.mustArea = callback(url, response.data.title)
        })
    },
    clickLinkMd() {
      this.getTitle(this.mustArea, (url, title) => {
        return `[${title}](${url})`
      })
    }
  }
}
</script>
```

# 終わりに

文章を書くときに定形作業が必要な場合、VS Codeとかの拡張機能もいいですけど、変換前後がよくわかるので、こういうツールも便利じゃないかと思いました。

記事のいいね！などもらえたら、とても励みになりますので、よろしくお願いします。