Laravel でSpeakerDeck のスライドをアップロードして表示する機能実装

## はじめに
スライドを表示する機能の実装方法を調べていたところ、SpeakerDeckというサービスを見つけました。このサービスはUIやUXが優れているものの、公式のAPIは提供されていません。それでも、スライドを表示したいと考え、Laravelを使用して実装に挑戦しました。
##  SpeakerDeck とは
[SpeakerDeck](https://speakerdeck.com/
)とは、PDFをアップロードするだけで、簡単にWeb上にスライドを共有できるサービスです。

https://speakerdeck.com/productmarketing/whats-in-a-name-adding-method-to-the-madness
## 実装手段の経緯
#### peakerDeckにアップロードし、そのスライドのIDをカラムに保存し,iframeに埋め込む

<img width="500" alt="スクリーンショット 2024-06-08 7.24.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/50b0898e-7f67-7c40-6c07-060f2d9d43eb.png">

ブログで埋め込むときに使用する`Copy iframe embed code`を選択します。取得される`iframe`のコードは以下のようになっています。
```php
<iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/67945f4875504c52a924be0311bcc089" title="itheme-webinar-podcast.pdf" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe>
```
コードの`playyer`の直後の箇所がIDです。このIDを置き換えることで任意のスライドを表示できることを見つけました。
```php
src="https://speakerdeck.com/player/67945f4875504c52a924be0311bcc089"
```
しかし、ユーザーに`iframe`からIDを取得させるのは適切ではありません。URLを入力してもらうだけでスライドを表示できる方が、ユーザーにとっても手間がかからず良いと考えました。そのためには、SpeakerDeckの`oEmbed`を利用して、スライドのURLからIDを抽出する必要がありました。

#### oEmbedとは
oEmbedは、ウェブページに他のサイトのコンテンツ（ビデオ、画像、テキストなど）を埋め込むためのフォーマットです。URLを使って埋め込みたいコンテンツの情報を取得し、自動的にそのコンテンツを表示する方法を提供します。

https://oembed.com/

## スライドURLからIDを取得
SpeakerDeckはoEmbed の[エンドポイント](https://help.speakerdeck.com/help/how-do-i-use-oembed-to-display-a-deck-on-my-site)を提供しています。
```
https://speakerdeck.com/oembed.json
```
したがって、oEmbed をリクエストすると次のようになります。
```
https://speakerdeck.com/oembed.json?url=URL_OF_THE_DECK
```
実際のURLに合わせると次のようになります。該当のスライドの埋め込みコードがレスポンスされます。
```
https://speakerdeck.com/oembed.json?url=https://speakerdeck.com/kazu1212star/itheme-webinar-podcast
```
レスポンスのJSON構造は以下の通りです。この中で特に重要なのは、htmlキーに含まれるiframe情報です。返却されるiframeは、例えばplayer/67945f4875504c52a924be0311bcc089のようなIDをベースにしています
```
{
  "type": "rich",
  "version": 1.0,
  "provider_name": "Speaker Deck",
  "provider_url": "https://speakerdeck.com/",
  "title": "itheme-webinar-podcast.pdf",
  "author_name": "kazu1212-star",
  "author_url": "https://speakerdeck.com/kazu1212star",
  "html": "<iframe id=\"talk_frame_1196406\" class=\"speakerdeck-iframe\" src=\"//speakerdeck.com/player/67945f4875504c52a924be0311bcc089\" width=\"710\" height=\"399\" style=\"aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;\" frameborder=\"0\" allowtransparency=\"true\" allowfullscreen=\"allowfullscreen\"></iframe>",
  "width": 710,
  "height": 399,
  "ratio": 1.7777777777778
}
```

## スライド表示機能を実装
### コントローラー作成
#### スライドのIDを取得方法
1.　JSONからhtmlキーを読み取り、含まれているiframeを取得します。
2.　iframeのURLからplayer直後に続くIDを抽出し、変数に格納します。
```php
 $htmlContent = $data['html'];
 preg_match('/player\/([a-z0-9]+)"/i', $htmlContent, $matches);
 $slideId = $matches[1] ?? null;
```
#### コントローラーのコード説明
UserSlideControllerコントローラーを作成し、`storeSlideId`メソッドを作成します。
Usersテーブルに`slide_id`カラムをstring型で作成し、そこに保存します。
#### エラー対応
- URLが正しくなく、取得したいレスポンスではなかったらエラーメッセージを表示
- スライドのIDが正しく取得できなかったらエラーメッセージを表示
```contoroller.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class UserSlideController extends Controller
{
    public function storeSlideId(Request $request)
    {
        $validatedData = $request->validate([
            'url' => 'required|url'
        ]);

        $url = $validatedData['url'];
        $response = Http::get('https://speakerdeck.com/oembed.json', [
            'url' => $url,
            'format' => 'json'
        ]);

        $data = $response->json();

        // 'html' キーが存在するか確認
        if (!isset($data['html'])) {
            return back()->with('flashError', 'スライド情報を取得できませんでした。URLを確認してください。');
        }

        $htmlContent = $data['html'];
        preg_match('/player\/([a-z0-9]+)"/i', $htmlContent, $matches);
        $slideId = $matches[1] ?? null;

        if (is_null($slideId)) {
            return back()->with('flashError', 'スライド情報を取得できませんでした。URLを確認してください。');
        }

        $company = Auth::user()->company;
        $company->slide_id = $slideId;
        $company->save();

        return redirect()->route('company.base_profile')->with('flashSuccess', 'スライドを保存しました。');
    }
}
```
#### ルーティング
```route.php
Route::controller(UserSlideController::class)->group(function () {
    Route::post('/store_slide_id', 'storeSlideId')->name('user.store_slide_id');
});
```
#### レイアウト
```blade.php
        <div class="mt-4 ml-10 w-[734px]">
            <div class="inline-block text-h5">
                説明スライド
            </div>
            <form action="{{ route('user.store_slide_id') }}" method="POST" class="mb-1">
                @csrf
                <label for="url" class="text-l text-gray-900 mb-4 mt-2">Speakerdeck SlideのURLを入力してください。</label>
                <div class="flex items-center justify-between space-x-2 mb-3 mt-2">
                    <input type="url" id="url" name="url" class="form-input h-11 w-[550px] border border-gray-300 rounded-lg text-l-w text-gray-900">
                    <button type="submit" class="px-4 py-2.5 bg-white rounded-lg border border-gray-300 flex justify-center items-center w-[112px] text-slate-700 text-button-medium font-bold hover:bg-gray-100">
                            <img src="{{ asset('svg/edit.svg') }}" class="w-[17px] h-[17px] mr-1">
                            更新する
                    </button>
                </div>
            </form>
            <x-input-error :messages="$errors->get('url')" class="mt-2" />
            @if($user->slide_id)
            //ここで表示させます
                <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/{{ $user->slide_id }}" title="itheme-webinar-podcast.pdf" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe>
            @endif
        </div>
```
#### IDを埋め込んでいる箇所
`src="https://speakerdeck.com/player/{{ $user->slide_id }}" `
```php
<iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/{{ $user->slide_id }}" title="itheme-webinar-podcast.pdf" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe>
```
以下のようなレイアウトになります。
![スクリーンショット 2024-06-07 22.47.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/bdf2904e-52b5-4b2b-7731-136539adc4f1.png)

### スライド表示するまでのステップのまとめ
1. SpeakerDeckのURLを取得
<img width="500" alt="スクリーンショット 2024-06-08 22.00.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3012370/ff8c779c-7a5f-dd18-b163-9d77e8e868e5.png">

2. 入力フォームに1で取得したURLを入力します。
3. SpeakerDeckのoEmbed から埋め込みコードがレスポンスとして返されます。
4. JSONからhtmlキーを読み取りIDをカラムに保存します。
5. `iframe`に取得したIDを埋め込んで表示します。


## 参考サイト
https://d.potato4d.me/entry/20210211-embed-slide/

https://kenchon.github.io/Speakerdeck