
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、個人的にはあまりTwitterでの情報発信はしていないのですが、最近Twitterに関連する開発をしていて「あっ、これあったらいいよね」と思う機能を発見しました。
それが・・・・・・
カード形式のリンク
です。
例えばこんな感じですね。
※Twitterの開発ページから引用
これってかっこいいですよね。
そこで
今回はURLを元にしてページ情報を取得しカードリンクとして表示する方法をご紹介します。
ぜひ皆さんの参考になれば嬉しいです
(最後にソースコード一式をダウンロードできますよ)
「最近、ちょっとずつ断捨離しています」
開発環境: Laravel 7.x, Vue 2.6.11
目次 [非表示]
モデル&データベースの準備をする
では最初はデータベースまわりを作っていきます。
以下のコマンドを実行してください。
php artisan make:model Card -m
すると、モデルとマイグレーションのファイルが一気に作成されます。
モデルの設定
では、モデルの設定からです。
app/Card.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Card extends Model
{
protected $guarded = ['id'];
// Accessor
public function getTitleAttribute($value) {
if(empty($value)) {
return '(タイトルなし)';
}
return mb_strimwidth($value, 0, 80, '...');
}
public function getDescriptionAttribute($value) {
$value = preg_replace('/^[\s ]+/u', '', $value); // 先頭の全角・半角スペース除去
$value = preg_replace('/[\s ]+$/u', '', $value); // 最後の全角・半角スペース除去
return mb_strimwidth($value, 0, 150, '...');
}
public function getThumbnailUrlAttribute($value) {
if(empty($value)) {
return url('images/no_image_available.png'); // サムネイル画像がないとき
}
return $value;
}
// Others
public function setAttributes($url) {
$this->url = $url;
try {
$html = file_get_contents($url);
} catch (\Exception $e) {
return false;
}
$default_libxml_error = libxml_use_internal_errors(true); // libxml エラー設定を変更
$doc = new \DOMDocument();
$doc->loadHTML($html);
libxml_use_internal_errors($default_libxml_error); // libxml エラー状態を元にもどす
$tags = $doc->getElementsByTagName('meta');
$attributes = [
'title' => '',
'thumbnail_url' => '',
'description' => ''
];
foreach($tags as $tag) {
if($tag->hasAttribute('property') && $tag->hasAttribute('content')) { // 必要なデータを集める
$property = $tag->getAttribute('property');
$content = $tag->getAttribute('content');
if($property === 'og:title') {
$attributes['title'] = $content;
} else if($property === 'og:image') {
$attributes['thumbnail_url'] = $content;
} else if($property === 'og:description') {
$attributes['description'] = $content;
}
} else if($tag->hasAttribute('name')) {
$property = $tag->getAttribute('name');
$content = $tag->getAttribute('content');
if(empty($attributes['description']) && $property === 'description') { // og:descriptionがないときの保険
$attributes['description'] = $content;
}
}
}
if(empty($attributes['title'])) { // og:titleがないときの保険
$titles = $doc->getElementsByTagName('title');
if(count($titles) > 0) {
$title = $titles[0];
$attributes['title'] = $title->textContent;
}
}
$this->fill($attributes);
return true;
}
}
この中でやっているのは、まずLaravel
が提供する便利機能、Accessor
を使って以下3つのデータを加工するようにしています。
- title: タイトルが存在していない場合に(タイトルなし)に入れ替える。テキストが長すぎる場合は切り詰める
- description: 前後のスペースを除去し、テキストが長すぎる場合は切り詰める
- thumbnail_url: サムネイル画像が存在しない場合、以下の画像に差し替える
※ ちなみに、この画像はパブリックドメイン(「自由に使っていいよ」ライセンス)です。リンク
そして、setAttributes()というメソッドを追加し、実行すると、該当ページから以下のデータを取得します。
- <meta>タグの中から「og:title」「og:image」「og:description」を取得
- 「og:title」「og:description」が存在しない場合、別のデータで代用する
なお、一点注目してほしいのが、libxml_use_internal_errors()
です。
今回ページデータを取得するためにDOMDocument()
と呼ばれるPHPの機能を使っていますが、libxml_use_internal_errors(true)
を設定していないと「閉じタグがない」などのエラーが発生した場合に処理が止まってしまうからです。
※ ただし、そのままずっと変更したままでは他のコードに影響する可能性がありますので、元に戻しています。
マイグレーションの設定
では、続いてマイグレーションを設定します。
database/migrations/****_**_**_******_create_cards_table.php
// 省略
public function up()
{
Schema::create('cards', function (Blueprint $table) {
$table->id();
$table->string('url')->comment('URL');
$table->string('title')->nullable()->comment('タイトル');
$table->string('thumbnail_url')->nullable()->comment('サムネイルURL');
$table->text('description')->nullable()->comment('ページ概要');
$table->timestamps();
$table->unique('url');
});
}
// 省略
なお、以下のフィールドは取得できない場合を想定してnullable()
をつけています。
- title
- thumbnail_url
- description
また、URLが重複しないようにunique()
をつけていることにも注目してください。
では、この状態で以下のコマンドを実行しcards
テーブルをつくってみましょう。
php artisan migrate
実行が完了するとテーブルはこうなります。
カードリンクを表示するVueコンポーネントをつくる
次に、取得したページ情報を簡単に表示できるようにVue
コンポーネント「v-card」をつくります。
以下のファイルを作成してください。
public/js/vue/v-card.js
Vue.component('v-card', {
props: {
url: String,
ajaxUrl: String
},
data() {
return {
card: {}
}
},
computed: {
hasCard() {
return (Object.keys(this.card).length > 0);
},
thumbnailStyle() {
const thumbnailUrl = this.card.thumbnail_url;
return {
height: '100%',
backgroundImage: `url(${thumbnailUrl})`,
backgroundRepeat: 'no-repeat',
backgroundSize: 'cover',
backgroundPosition: 'center center'
}
}
},
mounted() {
if(this.ajaxUrl !== '') {
const params = { url: this.url };
axios.post(this.ajaxUrl, params)
.then(response => {
if(response.data.result === true) {
this.card = response.data.card;
}
});
}
},
template: `
<div class="border rounded" style="text-decoration:none;" v-if="hasCard">
<a :href="card.url" style="text-decoration:none !important;color:#333;" target="_blank">
<div class="row">
<div class="col-4 pr-0">
<div :style="thumbnailStyle" class="rounded-left border-right"></div>
</div>
<div class="col-8 pl-0">
<div class="px-3 py-2">
<div class="font-weight-bold mb-1" v-text="card.title"></div>
<div class="mb-1" v-if="card.description">
<small v-text="card.description"></small>
</div>
<div class="mb-1" style="white-space:nowrap;overflow:hidden;text-overflow: ellipsis;" v-if="card.url">
<small class="text-secondary" v-text="card.url"></small>
</div>
</div>
</div>
</div>
</a>
</div>
`
});
この中で重要なのは、ajax
でデータを取得しているところです。
プロパティとして渡されるajaxUrl
にアクセスしてページ情報を取得することになります。
※なお、CSSはBootstrap 4
の利用を想定しています。
ページ情報を取得する部分をつくる
続いて、先ほど作成したv-card
内からのajax
送信を受ける部分をつくっていきます。目的は、OGP
などからページ情報を取得することです。
ルートをつくる
まずルートをつくります。
routes/web.php
Route::post('api/card/generate', 'Api\CardController@generate');
コントローラーをつくる
そして、専用コントローラーもつくりましょう。
php artisan make:controller Api\\CardController
中身は以下のように変更してください。
app/Http/Controllers/Api/CardController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class CardController extends Controller
{
public function generate(Request $request) {
$card = null;
$url = $request->url;
$card = \App\Card::FirstOrNew(['url' => $url]);
$exists = $card->exists();
$dt_ago = now()->subMonth(); // 1ヶ月前
if(!$exists|| ($exists && $card->updated_at < $dt_ago)) {
if($card->setAttributes($url)) {
$card->updated_at = now(); // 中身に変換がなくても必ず更新
$card->save();
} else { // HTTP接続に失敗した場合
return false;
}
}
return [
'result' => true,
'card' => $card
];
}
}
この中でやっていることは次のとおりです。
- すでに該当URLのデータを持っているかチェック
- もし存在しない場合、または、存在しているけどデータが古い場合はデータを取得して保存します
- ページ情報を返す
テストしてみる
では、これで準備は完了しました。
以下のようなルートとテンプレートをつくってカードリンクがうまく表示されるかを見てみましょう!
※なお、ページ情報を取得するURL
は、私が好きな「六甲ビール」さんに関するニュースです。個人的には苦味が強い「スサノオ」がおすすめ。( 企業案件ではありませんよ 笑)
routes/web.php
Route::get('test/card', function(){ return view('test_card'); });
resources/views/test_card.blade.php
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
</head>
<body>
<div id="app" class="p-3">
<div class="row">
<div class="col-6">
<v-card :url="url" :ajax-url="ajaxUrl"></v-card>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script src="/js/vue/v-card.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
url: 'https://www.value-press.com/pressrelease/245407', // 該当URL
ajaxUrl: '/api/card/generate'
}
});
</script>
</body>
</html>
実際にブラウザでアクセスしたものが以下になります。
うまく表示されました。
成功です
ダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードできます。
ツイッターみたいなカードリンクをつくる※ただし、マイグレーション等はご自身で行ってください。
おわりに
ということで、今回はTwitter
のようなカード形式のリンクをつくってみました。
ユーザーさんからすると、リンク先の情報を先に確認できるので安心してクリックしやすいんじゃないでしょうか。
ちなみに、当初はaxios
を使わず、純正JavaScript
のfetch()
で実装してみようと思いましたが、csrfトークンエラー(419)が出てしまい、めんどうなので結局axios
を使うことにしました。
どうやらaxios
は自動でcsrfトークン
を取得し、ヘッダーに含めてくれているようですね。さすがです
ということで、ぜひみなさんのサイトでカードリンクを使ってみてくださいね。
ではでは〜
「どんなに忙しくても最低15分は練習してます。
大した才能ないですが・・・」