- Webデザイナー/フロントエンドエンジニアを志望する学生としてポートフォリオの一つも作れないのはマズいかも
- 今Qiitaではポートフォリオを作るのが流行ってるっぽい
という理由で、ポートフォリオサイトを作ってみることにしました。
つくったサイト
(本当はもっと画質のいいGIF上げたいんですがQiitaに怒られたので諦めました…)
リポジトリ
TL;DR
- Nuxt.jsとContentfulを使用したSPA
- シームレスなページ遷移
- URL付きモーダルウィンドウ
- Canvasでインタラクティブな飾り
- PHPで無理やりOGP対応
コンセプト
コンテンツ
- 過去に作った作品を置くだけのサイト (プロフィール等なし)
- 曲、動画、写真などあらゆる種類のコンテンツを置く
僕がDTMも電子工作もゲーム制作もWeb制作もする人間なので、いろんなことに手を出す人間だというのを一番に押し出したいと考えました。
wararyoってどんな人間なんだろう、と思って訪れた人がwararyoの全容をなんとなく把握できる場所です。
プロフィールやお問い合わせなどはブログにあるため、今回は純粋に作品を表示するだけのサイトにしました。
デザイン
- wararyoカラーのアクセントをどこかに設けたい
- Canvasを除くすべての要素に対してRetina対応
- 全画面ヘッダーを採用 なんかカッコいい文言を書く
- 作品はモーダルウィンドウで表示したい
- BootstrapやBuefyなど既存のCSSライブラリは用いない
デザインの詳細はwararyo's work内のwararyo's workページに書いてます
性能・機能
- とりあえず通常時60fpsは達成したい(モバイル・PC)
- ファーストビューを表示するのに必要な通信量500KB以内
- 作品に一意のURLを与えて気軽にシェアできるようにする
- OGP対応は絶対する
CMS選び
初めは無難にWordpressを考えていましたが、新しいことに挑戦してみたかったので今流行りのヘッドレスCMS"Contentful"を使ってみることにしました。
Jekyllなども考えたのですが、新しい作品を追加するたびにサイトを生成しなければいけないのは、ポートフォリオとしては煩雑かなと思い見送りました。
結果としてContentfulは今回の要件にぴったりでした。
例えばカテゴリーに固有の色を設定したい場合、WordpressではわざわざPHPを書くかAdvanced Custom Fieldなどのプラグインが必要でした。しかしContentfulでは全てを1から定義できるため、CategoryにColorフィールドを追加することも数秒でできます。
また、「作品一覧では画像をアイキャッチにしつつ、個別作品ページではYoutube動画をアイキャッチにしたい」といった複雑な要求にも対応できる柔軟さを持っています。
作品の詳細がMarkdownで記述できるのも個人的には推しポイントです。
互換性の面でContentfulに一抹の不安
多くのWordpress製サイトと違い、データベースは自分のサーバーではなくContentfulのサーバーが持っています。また、自分でコンテンツの枠組みから設定する形なので、ほかのCMSとの互換性も保証できません。Contentfulがサービスを終了した場合にはどうやってほかのサービスに移行すればいいのか、少々不安です。
NuxtでモーダルウィンドウにURLを付与する
今回作ったポートフォリオのURL書式はhttps://work.wararyo.com/{カテゴリのSlug}/{作品のSlug}
となっています。
Nuxtはpages
ディレクトリ内のファイルに応じてRouterの設定を自動生成してしまうので、URLをこの書式にするために少々強引な方法をとりました。
...
┣pages
┃┣index.vue
┃┗index
┃ ┣_category.vue
┃ ┗_category
┃ ┗work.vue
...
<!-- 空 -->
<!-- 空 -->
vueファイルと同名のフォルダを作ることで、そのページの中に入れる子ページを定義することができます。
子ページは<nuxt-child>
タグで挿入することができるのですが、今回は使用していません。
そう、子ページを定義しつつ、その子ページを使わないという方法をとりました。
例えばhttps://work.wararyo.com/game/eclair
にアクセスした場合、
$route.params.category
にgame
が、
$route.params.work
にeclair
が代入されます。
あとは$route
をwatchして$route.params.work
がundefined
でない時は該当するworkオブジェクトを取得したのちモーダルウィンドウに渡すだけです。
<template>
<section class="container">
<work-modal :work="workObject"/>
<!-- 省略 -->
</template>
<script>
export default {
computed: {
work () {
if(this.$route.params.work === void 0) return "";
return this.$route.params.work;
},
workObject () {
if(this.work == "") return void 0;
let work = this.work;
let w = this.works.filter(function (item) {
return item.fields.slug == work;
});
return w[0];
}
//...
</script>
カードをクリックする->該当するURLへ移動する->$route.params.work
に値が代入される->モーダルウィンドウが表示される、というスタイルです。
レスポンスの遅さが心配されるところですが、さすがはVue、全く遅延は感じません。
<!-- 作品一覧で使われる作品カード -->
<template>
<nuxt-link v-if="post !== void 0"
:to="makeLink($route.params.category,post.fields.slug)"
class="work-card">
<!-- 省略 -->
</nuxt-link>
</template>
<!-- ... -->
そんなわけで作品カードの実体はただのnuxt-link
(レンダリング時にaタグに変換される)です。@click
属性は使用していません。
Canvas製「魔法のじゅうたん」
wararyoカラーの飾りを作りました。一人で勝手に「魔法のじゅうたん」と呼んでいます。
マウスを置くと頂点が集まってきます。
既存のライブラリを使い楽をしたかったのですがいいものが見つからなかったので自前で書きました。
偶然見つけたakm2さんの"Abstriangulation"という作品がイイカンジだったのでForkして作りました。MIT Licenseなのできっと大丈夫
http://jsdo.it/akm2/wTcC
擬似3D
3Dっぽい見た目をしていますが、実際は完全2D処理です。
- 上の方の透明度を下げてフォグを表現
- 上から下にいくにつれて頂点の縦方向の間隔を大きくし、遠近感を表現
- 下の頂点の方がマウスに追従しやすい
これらの工夫により3Dっぽい見た目を再現しています。
これらを全て無効にしたらかなり安っぽいです。
iOS対応
iOS Safariはスクロールするとアドレスバーが伸縮しますが、その度にresize
イベントが発生します。
resize
が発生するたびに頂点を再配置する仕様にしていたら、スクロールするたびに魔法のじゅうたんが激しく荒ぶるようになってしまったので、幅が同じ時は頂点の再配置を行わないようにしました。
また、iOS Safariではmousemove
イベントは発生せず、代わりにtouchmove
イベントが発生します。
UAで判別して、iOSの場合はtouchmove
イベントを使用するようにしました。
var userAgent = window.navigator.userAgent.toLowerCase();
var isIOS = userAgent.indexOf('iphone') > -1 ||userAgent.indexOf('ipad') > -1 ||userAgent.indexOf('ipod') > -1;
var moveEventName = isIOS ? "touchmove" : "mousemove";
//...
function init() {
window.addEventListener('resize', resize, false);
resize(null);
document.addEventListener(moveEventName,mouseMoved);
//...
}
function resize(e) {
//Set Context
context = canvas.getContext('2d');
context.lineWidth = 0.6;
context.strokeStyle = LINE_COLOR;
if(canvas.width === window.innerWidth) return;//幅が一緒なら以降の処理はしない
canvas.width = window.innerWidth;
canvas.height = CANVAS_HEIGHT;
//以降、頂点の再配置処理
PHPで無理やりOGP
皆さんご存知の通り、JavascriptでOGPタグを設定しても反映されません。静的ページにしたり、SSRするなどの対策が一般的です。
しかし、今回はnuxt build --spa
で生成したindex.html
をindex.php
にして、PHPでOGPタグを埋め込むことにしました。
毎回置き換えるのは面倒なので自動化スプリプトを組みました。頭悪い置換っぷりですが所詮は自分のために自分で作っているサイトです。手を抜けるところはどんどん抜いていきましょう。
module.exports = {
head: {
title: 'wararyo\'s work',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'og:site_name', property: 'og:site_name', content: 'wararyo\'s work' },
{ hid: 'og:type', property: 'og:type', content: 'website' },
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:site', content: '@wararyo' },
{ content: 'replace' } //これをOGPに置き換える
],
//...
"scripts": {
"dev": "nuxt --spa",
"build": "nuxt build --spa && node ogp-replace.js dist/index.html '<meta data-n-head=\"true\" content=\"replace\">' ogp.php dist/index.php",
},
if(process.argv.length < 6) {
console.log("Usage: node "+process.argv[1]+" [file_in] [replace_from] [replace_file] [file_out]");
return 0;
}
var fs = require('fs');
var fileInPath = process.argv[2];//置換を行いたいファイル
var replaceFrom = process.argv[3];//置換を行いたいキーワード
var replaceTo = fs.readFileSync(process.argv[4], 'utf-8');//置換先
var fileOutPath = process.argv[5];//保存先
fs.readFile(fileInPath, 'utf8', function (err,data) {
if (err) {
return console.log(err);
}
var result = data.replace(replaceFrom, replaceTo);
fs.writeFile(fileOutPath, result, 'utf8', function (err) {
if (err) return console.log(err);
});
if(fileInPath != fileOutPath) fs.unlinkSync(fileInPath);//置換前のファイル消す
});
<?php
$DESCRIPTION_MAX_LENGTH = 96;
//正しいURLなら$matches[1]にカテゴリーSlugが、$matches[2]に作品Slugが入る
$matched = preg_match('/\/(.+)\/(.+)\/?$/',$_SERVER["REQUEST_URI"],$matches);
if($matched){
$work_slug = $matches[2];
$url = "https://cdn.contentful.com/spaces/_YOUR_SPACE_ID_/environments/master/entries/?access_token=_YOUR_ACCESS_TOKEN_&content_type=work&fields.slug=$work_slug";
$json = file_get_contents($url);
if($json) {
$obj = json_decode($json);
$ogp = new stdClass();
$ogp->title = "wararyo's work - ".$obj->items[0]->fields->title;
$ogp->image = 'https:'.$obj->includes->Asset[0]->fields->file->url;
$ogp->description =
str_replace(PHP_EOL, '',
htmlspecialchars(
mb_substr($obj->items[0]->fields->content,
0,DESCRIPTION_MAX_LENGTH)));
}
}
?>
<meta name="description" content="<?=isset($ogp->description)?$ogp->description:'wararyoのポートフォリオサイトです。'?>">
<meta property="og:title" content="<?=isset($ogp->title)?$ogp->title:'wararyo\'s work'?>">
<meta property="og:image" content="<?=isset($ogp->image)?$ogp->image:'https://work.wararyo.com/ogp.jpg'?>">
<meta property="og:description" content="<?=isset($ogp->description)?$ogp->description:'wararyoのポートフォリオサイトです。'?>">
その他気づいたこと
Contentfulでリサイズした画像はキャッシュされるっぽい
Contentfulは画像の後ろに?w=640&fm=jpg
などと書くことでサイズやフォーマットの変換を行ってくれるのですが、「もしかしたら画像をリサイズする時間より、リサイズされてない画像をダウンロードする方が早いのでは…?」という疑問がありました。
しかしそれは杞憂でした。
?w=640&fm=jpg
を追記した後に初回アクセス->ブラウザのキャッシュを消去して再読み込み
という実験をしてみたところ、両方ともブラウザ側のキャッシュが無いにも関わらず、後者の方が明らかに高速でした。
Contentfulサーバー内部でキャッシュされていると考えられます。
Canvasは存在するだけで重い
魔法のじゅうたんを描画しながらYoutubeを再生すると重すぎてYoutubeがカクカクになったりしました。
モーダル表示時は描画処理を停止しても相変わらず重く、やむなくvisibility:hidden;
で非表示にすることにしました。謎です。
結果
ContentfulをNuxt.jsを使って、自分が求めていた通りのポートフォリオサイトを作ることができました!
改善できるポイントとしては、
- PICK UP!!ページにて、作品のカテゴリが分からない(PICK UP!!だけは専用ページにすることも検討)
- 背景もなんかカッコよく動いて欲しい(マシン負荷と相談しながら)
- カテゴリー切り替え時にフェードインだけでなくフェードアウトもしてほしい(もうちょっとでできそう)
- ロリポップにはもうちょっとTTFBを早くしていただきたい(スタンダードプランに変更検討中)
- ファビコンが見辛い
などがあります。気ままに改善していきましょう。
ページ速度
Auditsの結果は以下の通りです。
まずまずですね。PWA対応してみるのも面白そうです。
ファーストビューを表示するのに必要なデータ量は453KBでした。ギリギリ目標達成です。