2019-10-21に投稿

Twitterシェア用のOGPを作成する

質問、回答と作成しましたので、次にその回答をツイッターに画像つきで投稿する必要があります。ツイートされた時に画像を表示するため、そのOGP用の画像を生成する処理を作成します。

こんな画像を作成します。

ちなみに実は以前別の記事でもLaravelでOGPを作る方法を解説しました。

LaravelでOGPを作る

上記ではGDを使って生成しています。同じような解説になってしまってもあまり意味がないので、今回はImagickを使って生成してみます。サーバーにはImageMagick自体とPHPのImagickがインストールされている必要があります。準備が難しい場合はGDのパターンで試してみてください。

背景画像を用意する

簡単にOGPをカッコよくしたいので、背景には枠画像を使うことにしました。下記でフリー素材をダウンロードしてきました。

街中の住宅シルエット フレーム素材 枠-囲み 247 | 素材Good

これをresources/images/ogp.pngとして保存しておきます。

フォントを用意する

文字を描画するためにはフォントが必要です。Googleフォントから日本語フォントをダウンロードしてきました。これをresources/fontsフォルダに保存しておきます。

画像の生成処理を作る

素材が準備できましたので、Questionモデルに画像の生成処理を作ります。

まずはuseを追加します。

use Imagick;
use ImagickDraw;

そしてメソッドを追加します。各行の文字数が長いと画像からはみ出してしまうため、適当に折り返しを行っています。

    public function generateOgp(): Imagick
    {
        $body = $this->getWordwrapedBody();

        $image = new Imagick(resource_path('images/ogp.png'));

        $draw = new ImagickDraw();
        $draw->setFont(resource_path('fonts/MPLUS1p-Regular.ttf'));
        $draw->setFontSize(24);
        $draw->setTextAlignment(Imagick::ALIGN_CENTER);
        $lines = explode("\n", $body);
        $draw->annotation(300, 157 - (count($lines) - 1) * 12, $body);

        $image->drawImage($draw);

        return $image;
    }

    private function getWordwrapedBody(): string
    {
        $lines = explode("\n", $this->body);
        $result = [];
        foreach ($lines as $line) {
            $length = mb_strlen($line);
            for ($start = 0; $start < $length; $start += 20) {
                $result[] = mb_substr($line, $start, 20);
            }
        }
        return join("\n", $result);
    }

画像を表示する

実際にコントローラに画像を表示するためのアクションを追加します。AnswerControllerに追加します。

    public function ogp($id)
    {
        $answer = Answer::findOrFail($id);
        $image = $answer->question->generateOgp();
        return response($image, 200)
            ->header('Content-Type', 'image/png');
    }

順番が前後してしまいましたが、上記でanswerから関連するquestionを取得するようにしているため、Models/Answer.phpにリレーションを定義しておきます。

    public function question()
    {
        return $this->belongsTo(Question::class);
    }

アクセスできるようにルーティングを設定します。誰でもアクセスできるようにする必要があるので認証ミドルウェアのグループ外に記述します。

Route::get('answers/{id}/ogp.png', '[email protected]');

これでhttp://localhost:8000/answers/1/ogp.pngのようなURLにアクセスすると、画像が表示されます。(ホスト名とIDは適宜置き換えてください)

回答の詳細ページを作成

画像ができましたので、次に実際にTwitterにURLをシェアしてもらうための回答詳細ページを作成します。URLはanswers/IDにします。questionsのreceived機能とほとんど同じではあるのですが、あちらは質問を受け取った登録済みユーザー専用のページになります。表示もほとんど同じで良いと思いますが、同じページで共通化して条件分岐などでの対処にしてしまうと、不具合が発生した時に誰でも質問を見れてしまう場合が発生してしまう可能性があります。そういった問題を避けるためにも多少冗長化し、answersデータがあるものだけは一般公開で表示する、という安全な仕様にしています。

とりあえずAnswerControllerにメソッドを追加します。

    public function show($id)
    {
        $answer = Answer::findOrFail($id);
        return view('answer.show', compact('answer'));
    }

ここにアクセスできるようにルーティングを追加しておきます。ogpの下辺りにshowメソッドの追加を並べておきます。

Route::get('answers/{id}/ogp.png', '[email protected]');
Route::resource('answers', 'AnswerController')->only(['show']);

表示する時に回答者の名前も表示するため、回答データから回答ユーザーを取得できるように、Models/Answer.phpにリレーションの定義を追加しておきます。

    public function user()
    {
        return $this->belongsTo(User::class);
    }

これで$answer->userのような形でアクセスできます。

そしてテンプレートをresources/views/answer/show.blade.phpとして作成します。基本的にreceivedからコピーして不要部分を削っただけです。せっかくなので質問の箇所はOGPと同じ画像を表示するようにしています。軽くはないと思うのでリリース後はCloudflareでキャッシュしたいですね。

@extends('layouts.app')

@section('title', "{$answer->user->name}さんへの質問")
@section('description', $answer->body)
@section('ogp', url("answers/{$answer->id}/ogp.png"))

@section('content')
<section class="text-center">
  <h1 style="font-size: 1.5rem">届いた質問</h1>
  <div class="mb-4">
    <img src="{{ url("answers/{$answer->id}/ogp.png") }}">
  </div>

  <h2 style="font-size: 1.2rem">{{ $answer->user->name }}さんからの回答</h2>

  <div class="card">
    <div class="card-body">
      {{ $answer->body }}
    </div>
  </div>
</section>
@endsection

sectionでtitle, description, ogpを指定しています。この様にして、各ページごとに共通レイアウトに渡したい値を指定することが出来ます。共通レイアウト側にこれを受けて表示をするための処理を追加しておきます。headタグ内に下記を追加します。(タイトルの部分は条件分岐を追加しています)

    @hasSection('title')
    <title>@yield('title') - {{ config('app.name') }}</title>
    <meta property="og:title" content="@yield('title') - {{ config('app.name') }}">
    @else
    <title>{{ config('app.name') }}</title>
    <meta property="og:title" content="{{ config('app.name') }}">
    @endif
    @hasSection('description')
    <meta name="description" content="@yield('description')">
    <meta property="og:description" content="@yield('description')">
    @else
    <meta name="description" content="質問と回答ができるサービスです">
    <meta property="og:description" content="質問と回答ができるサービスです">
    @endif
    <meta property="og:type" content="website">
    <meta property="og:site_name" content="Crieit">
    @hasSection('ogp')
    <meta property="og:image" content="@yield('ogp')">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:image" content="@yield('ogp')">
    @endif

こんな感じで表示できるようになりました。

今はローカル環境で試しているのでダメですが、サーバーにアップしている状態であればこのページのURLをツイートすればTwitterでも画像が表示されるようになっているはずです。

ちなみに上記のスクリーンショットのようにちょっと左右のズレがあるようです。画像のサイズと画面の横幅があっていないため、共通レイアウトに直接書かれている600pxという表記を適当に全て700pxに変更しておきます。

まとめ

とりあえずメインの機能はおおよそできてきました。このあと面白くなくなってくる可能性があるのでこの記事はあとにすればよかったと後悔しています。次回は細々としたところを調整していくか、ハロウィン仕様への変更を予定しています。


view_list [連載] Laravelで質問箱みたいなサービスを作る
第3回 質問できるようにする
第4回 Bootstrapでベースデザインを整える
第5回 質問に回答する機能を作る
第6回 Twitterシェア用のOGPを作成する
第7回 受け取った質問一覧ページを作る

だら@Crieit開発者

Crieitの開発者です。 主にLAMPで開発しているWebエンジニアです(在宅)。大体10年程。 記事でわかりにくいところがあればDMで質問していただくか、案件発注してください。 業務依頼、同業種の方からのコンタクトなどお気軽にご連絡ください。 業務経験有:PHP, MySQL, Laravel5, CakePHP3, JavaScript, RoR 趣味:Elixir, Phoenix, Node, Nuxt, Express, Vue等色々

Crieitは個人で開発中です。 興味がある方は是非記事の投稿をお願いします! どんな軽い内容でも嬉しいです。
なぜCrieitを作ろうと思ったか

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください!

ボードとは?

関連記事

コメント