質問、回答と作成しましたので、次にその回答をツイッターに画像つきで投稿する必要があります。ツイートされた時に画像を表示するため、その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', 'AnswerController@ogp');
これで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', 'AnswerController@ogp');
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
に変更しておきます。
とりあえずメインの機能はおおよそできてきました。このあと面白くなくなってくる可能性があるのでこの記事はあとにすればよかったと後悔しています。次回は細々としたところを調整していくか、ハロウィン仕様への変更を予定しています。
第4回 | Bootstrapでベースデザインを整える |
第5回 | 質問に回答する機能を作る |
第6回 | Twitterシェア用のOGPを作成する |
第7回 | 受け取った質問一覧ページを作る |
第8回 | LaravelのDebugbarを導入する |
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント