【Vuetify】埋め込みツイート風コンポーネント

## はじめに

埋め込みツイートの表示をするにあたり、公式の埋め込みツイートを使おうと思ったのですが、なぜかVueで`<script>`が動きませんでした。原因を調べてもいまいちわからなかったので、自作したほうが早いのではないかと思いました。
一応、Vuetifyにも「Twitter card」というものはあったのですが……**なんか違う。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/84a2991b-8dae-a845-f2a2-e6bedcb68086.png)
というわけで、v-cardを基本にそれっぽいレイアウトにしてみました。

### つくるもの

こんな感じのツイート風コンポーネントです。
<img width="510" alt="tweet" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/322882/e89d20ae-b167-fc2e-fec5-0f13f63a00be.png">

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="html,result" data-user="aiandrox" data-slug-hash="wvKqmwK" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Oembed Tweet">
  <span>See the Pen <a href="https://codepen.io/aiandrox/pen/wvKqmwK">
  Oembed Tweet</a> by END (<a href="https://codepen.io/aiandrox">@aiandrox</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 環境

- Vue.js  2.6.11
- Vuetify  2.2.21
- Material Design Icon  5.1.45

アイコンは別のものでも代用可能だと思います。Twitterアイコン、リプライ、リツイート、いいねで使用しています。


## 使用データ

サンプルコードでは以下のようにデータをネストしています。

- tweet
  - tweetId（ツイートのID）
  - html（本文部分のHTML）
  - tweetedAt（ツイート日時）
  - user
      - name（表示名）
      - screenName（`@screen_name`のユーザー名）
      - avatarUrl（アイコンの画像URL）

htmlは[GET statuses/oembed](https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed)で取得した埋め込みツイート用のHTMLから`<p>`タグの内側を抜き出して利用しました。
日時は[Day.js](https://day.js.org/)などを使ってフォーマットを整えることをおすすめします。

## ツイート表示の規定

Twitterの埋め込みツイートには表示の規定があるので、それに従ってカードを作成します。
こちらの[Display requirements – Twitter Developers](https://developer.twitter.com/en/developer-terms/display-requirements)をざっくりGoogle意訳しました。間違っていたらご指摘をお願いします。

- 実際のアカウントによる変更を加えられていないツイートを表示すること。
- 公式ツイッターロゴを表示すること。
  - 個々のツイートの右上隅に表示するか、タイムラインに直接添付すること。
  - ロゴは、画像と同じ高さにすること。
- 作成者のプロフィール画像、@ユーザーネーム、表示名を常に表示し、Twitterプロフィールページにリンクさせること。
- @ユーザーネームは`@`を用いて表示すること。
- プロフィール画像は、表示名と@ユーザーネームの左側に配置すること（ただし右から左に読む言語のツイートは例外）
- ツイートのテキストは作成者の表示名と@ユーザーネームの下の行に表示すること。
- ツイートのテキストと作成者の周りのスペースはツイートのパーマリンクにリンクさせること。
- テキスト内のツイートエンティティは、Twitter上の適切なホームに適切にリンクすること。
  - `@ユーザーネーム`によるメンションは言及されたユーザーのプロフィールページにリンクすること。
  - ハッシュタグは、ハッシュタグをクエリとしてTwitter検索にリンクすること。
  - テキスト内のリンクは、[URL entities API](https://developer.twitter.com/en/docs/tweets/enrichments/overview/expanded-and-enhanced-urls)におけるdisplay_urlを表示し、元のt.co urlにリンクすること（詳細は[t.co best practices article](https://developer.twitter.com/en/docs/basics/tco)を参照）
- ツイートのタイムスタンプを表示し、ツイートのパーマリンクにリンクすること。
- プラットフォームに存在しないツイートのモックアップを使用しないこと。


## コード

CodePenの方はそれだけで完結するようにしています。
こちらのコードはコンポーネント用のコードです。

```tweet.vue
<template>
  <div>
    <v-card flat outlined max-width="500" class="mt-3" :href="tweetUrl">
      <v-card-title>
        <v-list-item class="pl-0">
          <v-list-item :href="userUrl">
            <v-list-item-avatar color="grey" size="40">
              <v-img :src="tweet.user.avatarUrl" />
            </v-list-item-avatar>
            <v-list-item-content>
              <v-list-item-title>{{ tweet.user.name }}</v-list-item-title>
              <v-list-item-subtitle class="font-weight-light">@{{ tweet.user.screenName }}</v-list-item-subtitle>
            </v-list-item-content>
          </v-list-item>
          <v-spacer />
          <v-list-item-action>
            <v-icon color="blue">mdi-twitter</v-icon>
          </v-list-item-action>
        </v-list-item>
      </v-card-title>

      <v-card-text class="text--primary" v-html="tweet.html" />
        <v-card-actions>
          <v-btn
            v-for="button in buttons"
            :key="button.icon"
            :href="button.url"
            :color="button.color"
            icon
          >
            <v-icon>{{ button.icon }}</v-icon>
          </v-btn>
          <v-spacer />
          <span class="body-2 font-weight-light">{{ tweet.tweetedAt }}</span>
        </v-card-actions>
      </v-card>
    </v-card>
  </div>
</template>

<script>
export default {
  data() {
    return {
      tweet: {
        tweetId: "0000000000",
        html: "テキスト<br>改行するとこんな感じ",
        tweetedAt: "2020-12-31 10:51",
        user: {
          name: "ユーザー",
          screenName: "screen_name",
          avatarUrl: "https://avataaars.io/?avatarStyle=Transparent&topType=ShortHairShortCurly&accessoriesType=Prescription02&hairColor=Black&facialHairType=Blank&clotheType=Hoodie&clotheColor=White&eyeType=Default&eyebrowType=DefaultNatural&mouthType=Default&skinColor=Light"
        },
      }
    }
  },
  computed: {
    userUrl() {
      return `https://twitter.com/${this.tweet.user.screenName}`
    },
    tweetUrl() {
      return `https://twitter.com/${this.tweet.user.screenName}/status/${this.tweet.tweetId}`
    },
    replyUrl() {
      return `https://twitter.com/intent/tweet?in_reply_to=${this.tweet.tweetId}`
    },
    retweetUrl() {
      return `https://twitter.com/intent/retweet?tweet_id=${this.tweet.tweetId}`
    },
    likeUrl() {
      return `https://twitter.com/intent/like?tweet_id=${this.tweet.tweetId}`
    },
    buttons() {
      return [
        { url: this.replyUrl, color: "gray", icon: "mdi-chat-outline" },
        { url: this.retweetUrl, color: "green", icon: "mdi-twitter-retweet" },
        { url: this.likeUrl, color: "pink", icon: "mdi-heart-outline" }
      ]
    },
  },
}
</script>
```

## おわりに

プロフィールのリンクの範囲が気になりますが、これで妥協点とします。
もっといい感じにできそうなら踏み台にしていただきたいです。リンクをしていただければ見に行きますので……。

ご意見、ご指摘などございましたらコメントを頂けると幸いです。