【URLから】HeadlessCMSでつくったブログにリンクカードを表示する話【タイトルを取得する】

みなさんこんにちわ。**Headless**というスペルを**Headress**とたまに間違えるいとうです。最近HeadlessCMSはやってますね。[Contentful](https://www.contentful.com/)や[CraftCMS](https://craftcms.com/)、[WP REST API](https://ja.wp-api.org/)、国産の[microCMS](https://microcms.io/)など様々なHeadlessCMSが生まれています。とても便利で案件でも増えてきました。

その背景にはやはり低コスト、アジャイル、自由度の増したデザインetc.....メリットしかありません。世界の3割のサイトがWordpressでできていると言われていたのがすごく前のように感じます。

いえーい、Wordpressくんみてるー?

## 本題、リンクカードとは

本題です。案件でNuxtでつくったブログにリンクカードを表示したいと言われたのでどうしたら実装できるのかを備忘録程度に書いておきます。

リンクカードというのはその名前の通り、リンクのカードです。

![_2020-09-29_2.12.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204436/fddc5f1b-347a-333b-ae30-2beb88e8f877.png)

Notionでいう↑こういうやつです。

要件としてはURLの文字列から、**タイトル**、**ディスクリプション**、**favicon**、**og:image**を取得するというものです。

今回の記事では前提として、フレームワークにNuxt、CMSはmicroCMSを使用します。

## とりあえずURLを表示する。

とりあえず好きなサイトのURLを取得しましょう。

URLの文字列からfetchしてHTMLを取得し、ヘッドの情報を抜き取りましょう。簡単なスクレイピングです。

```jsx
<script>
export default {
  data() {
    return {
      link: 'https://www.awwwards.com/',
    }
  },
  mounted() {
    console.log(this.link)
    const url = this.link

    fetch(url)
      .then((res) => res.text())
      .then((text) => {
        const el = new DOMParser().parseFromString(text, 'text/html')
        const headEls = el.head.children
        Array.from(headEls).map((v) => {
          console.log(v)
          const prop = v.getAttribute('property')
          if (!prop) return
          console.log(prop, v.getAttribute('content'))
        })
      })
  },
}
</script>
```

こんな感じのコードになります。

簡単に説明すると、URLの文字列をもとにfetchしheadの情報を抜き取ってproperty とcontentをタグに関係なく表示します。

しかしこのままだとCORSエラーになります

URLをfetchする場合、セキュリティの都合上からCORSに怒られてしまうのです。

なのでCORSエラーを回避するためにURLの頭に `https://cors-anywhere.herokuapp.com/` をつけてあげます。

```jsx
mounted() {
    console.log(this.link)
    const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/'
    const url = CORS_PROXY + this.link

    fetch(url)
      .then((res) => res.text())
      .then((text) => {
        const el = new DOMParser().parseFromString(text, 'text/html')
        const headEls = el.head.children
        Array.from(headEls).map((v) => {
          console.log(v)
          const prop = v.getAttribute('property')
          if (!prop) return
          console.log(prop, v.getAttribute('content'))
        })
      })
  },
```

これでエラーは出ません。やったね。

## HeadlessCMSと組み合わせる。

HeadlessCMSと組み合わせます。

```jsx
<template lang="pug">
  .container
    div(v-html="singleData")
</template>

<script>
export default {
  async fetch() {
    const [single] = await Promise.all([
      this.$axios.get('CMSのAPI', this.headers),
    ])
    this.singleData = single
  },
  data() {
    return {
      link: 'https://www.awwwards.com/',
      singleData: '',
    }
  },
  mounted() {
    console.log(this.link)
    const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/'
    const url = CORS_PROXY + this.link

    fetch(url)
      .then((res) => res.text())
      .then((text) => {
        const el = new DOMParser().parseFromString(text, 'text/html')
        const headEls = el.head.children
        Array.from(headEls).map((v) => {
          console.log(v)
          const prop = v.getAttribute('property')
          if (!prop) return
          console.log(prop, v.getAttribute('content'))
        })
      })
  },
}
</script>
```

ざっくりですが、こんな感じになるかと思います。`this.header`  はヘッダーの情報とかそんな感じです。

`div(v-html="singleData")` の中にCMSから取得したhtmlが吐き出されます。NuxtでSSRを使用している場合、レンダリングされたhtmlは`mounted`  などで取得することはできません。

これはライフサイクルの仕様です。なので、`moutend`  関数ではなく、`updated`関数を使います。`updated`関数は内容がDOMにレンダリングされたあとに実行されます。

```jsx
<template lang="pug">
  .container
    div.container__body(v-html="singleData")
</template>

<script>
export default {
  async fetch() {
    const [single] = await Promise.all([
      this.$axios.get('CMSのAPI', this.headers),
    ])
    this.singleData = single
  },
  data() {
    return {
      link: 'https://www.awwwards.com/',
      singleData: '',
    }
  },
  updated() {
    const bodyArray = document.querySelector('.container__body')
    bodyArray.forEach((bodyEl) => {
      const linkElArray = bodyEl.querySelectorAll('a')
      linkElArray.forEach((linkEl) => {
        this.getHead(linkEl.href, linkEl)
      })
    })
  },
  methods: {
    getHead(linkString, linkEl) {
      console.log(this.link)
      const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/'
      const url = CORS_PROXY + linkString

      fetch(url)
        .then((res) => res.text())
        .then((text) => {
          const el = new DOMParser().parseFromString(text, 'text/html')
          const headEls = el.head.children
          Array.from(headEls).map((v) => {
            const title = el.head.querySelector.textContent
            console.log(title)
            linkEl.innerHTML = `<p>${title}</p>`
          })
        })
    },
  },
}
</script>
```

`update`関数をつかってレンダリング後にレンダリングしたエレメントオブジェクトを配列で取得します。その中からaタグだけを抜き出し`getHead`メソッドにURLと要素を渡しましょう。

あとは上に書かれたコードの通りです。最終的には要素の内容を書き換えることで pタグの中にURLから取得されたサイトタイトルを表示しています。

## やっていることは簡単...だけど

`https://cors-anywhere.herokuapp.com/`　は魔法の文字列ですが、もちろんデメリットもあります。もともとCORSというのはサイトのセキュリティを保守するためにあるのでこれを無条件で回避しようとすると悪意あるサイトからの攻撃も許してしまう可能性があります。

ですので、絶対信頼できるURLしか貼らないようにしましょう。

みなさまも良いHeadlessCMSライフを！