2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

作品が増え続ける創作サイトを、HTML・CSS・JavaScriptだけで更新しやすく設計した話

2
Posted at

創作作品をエージェンシーに登録することになり、作品をまとめて見てもらうための公式サイトを制作しました。

今回作ったのは、オリジナル作品 「シュガーテイルへようこそ」 の作品サイトです。

この作品は、

  • 小説
  • ショートアニメ
  • ミュージックビデオ
  • ショート動画
  • キャラクターイラスト

と、ひとつの作品を複数のメディアで展開しています。

そのため普通のポートフォリオとは少し事情が違い、

「作品をたくさん掲載したい。でも新作が出るたびにサイト更新で苦労したくない」

というのが今回の一番大きな課題でした。

そこで、フレームワークなどは使わず、HTML・CSS・JavaScriptだけで、作品が増えても比較的簡単に更新できる1ページサイトとして設計しました。


サイト構成

ファイル構成はかなりシンプルです。

├── index.html              サイト本体(1ページ構成)
│
├── assets/
│   ├── css/
│   │   └── style.css       スタイル
│   │
│   ├── js/
│   │   └── main.js         メニュー・動画の遅延読み込み・拡大表示
│   │
│   └── img/
│       ├── asagi.png       浅葱優 キャラクタープロフィール
│       ├── yang.png        楊弦月 キャラクタープロフィール
│       ├── aika.png        御堂愛佳 キャラクタープロフィール
│       ├── characters.png  三人のキャラクター設定シート
│       ├── mail.png        お問い合わせアイコン
│       └── favicon.svg     ファビコン
│
└── README.md

基本的には、

  • index.html = コンテンツ
  • style.css = 見た目
  • main.js = 動き

という分け方です。

あえて複雑な構成にはしていません。

今回はCMSのような高度な管理機能よりも、

「新しい作品ができたら既存のHTMLをコピーして内容だけ変更する」

ことを優先しました。


1ページ構成にした理由

サイトは次のセクションに分けました。

TOP
├── NEW RELEASE
├── ABOUT
├── CHARACTER
├── MOVIE
├── STORY
├── READ
├── CAST
└── CONTACT

作品サイトなのでページを細かく分割する方法もあります。

ただ今回は、エージェンシーや仕事関係の方にURLを渡したとき、

ひとつのページをスクロールするだけで作品の規模が分かる

ことを優先しました。

特にMOVIEにはMVが多数、STORYにはショートアニメが多数あります。

ページを分けすぎると「何を作っている人なのか」が伝わるまで何度もクリックしなければなりません。

そこで、

詳しく読むためのサイト

というより、

まず大量の制作実績を一気に見てもらうサイト

として設計しています。


一番重要だったのは「更新を簡単にすること」

作品は今後も増えていきます。

サイトを作った時点で完成ではなく、

  • 新しいMVを公開
  • 新しいショートアニメを公開
  • 新章を公開
  • 新しい配信先を追加

という更新が頻繁に発生します。

そこで、更新作業を可能な限りコピー&ペーストで完結できる構造にしました。


MVを追加するとき

MOVIE部分は次のようなカード形式です。

<li class="mv-card">
  <div
    class="yt"
    data-yt="YouTubeの動画ID"
    data-title="動画タイトル"
  >
    <button
      class="yt-play"
      type="button"
      aria-label="動画を再生"
    ></button>
  </div>

  <h3 class="mv-title">
    動画タイトル
  </h3>

  <p class="mv-meta">
    出演キャラクター
  </p>
</li>

新しいMVを追加するときは、このブロックをコピーします。

変更するのは主に、

data-yt
動画タイトル
出演キャラクター

の3か所です。

YouTube URLが、

https://youtu.be/XXXXXXXXXXX

なら、

data-yt="XXXXXXXXXXX"

と動画IDだけを入れます。

最新作品には、

<span class="tag-new">NEW</span>

を追加します。

CMSではありませんが、更新する場所をパターン化することで運用コストを下げています。


YouTubeを最初からiframeで読み込まない

このサイトで特に気をつけたのがYouTubeです。

作品数を見せるため、MOVIEにはかなり多くの動画があります。

これを普通に、

<iframe src="https://www.youtube.com/embed/..."></iframe>

と全部埋め込むと、ページを開いただけで大量のYouTube iframeを読み込むことになります。

作品が増えれば増えるほどページが重くなります。

そこで最初はiframeを作っていません。

HTMLには、

<div class="yt" data-yt="D0cqbRSQ230">
  <button class="yt-play"></button>
</div>

だけ置いています。

JavaScript側でYouTubeのサムネイルを背景画像として表示します。

document.querySelectorAll('.yt').forEach(function (box) {
  var id = box.dataset.yt;

  box.style.backgroundImage =
    'url(https://i.ytimg.com/vi/' + id + '/hqdefault.jpg)';
});

そして、ユーザーが再生ボタンを押したときだけiframeを生成します。

box.querySelector('.yt-play').addEventListener('click', function () {
  var iframe = document.createElement('iframe');

  iframe.src =
    'https://www.youtube-nocookie.com/embed/'
    + id
    + '?autoplay=1&rel=0';

  iframe.allowFullscreen = true;

  box.innerHTML = '';
  box.appendChild(iframe);
});

つまり、

サイトを開く
↓
YouTubeサムネイルだけ表示
↓
ユーザーが見たい作品をクリック
↓
その動画のiframeだけ生成
↓
再生

という仕組みです。

作品数が増えることが最初から分かっていたので、この部分はかなり重要でした。


「たくさん見せる」と「全部読み込む」は別

作品サイトを作ると、

せっかくだから全部見せたい

となりがちです。

私もそのタイプです。

しかし、

全部掲載することと、全部を初回ロードすることは別問題

だと考えました。

そのため画像にも、

loading="lazy"
decoding="async"

を設定しています。

<img
  src="assets/img/asagi.png"
  alt="浅葱優のキャラクタープロフィール"
  loading="lazy"
  decoding="async"
>

ページを開いた瞬間に下の方にある画像まで読み込む必要はありません。

作品数が増えるサイトほど、

必要になったものから読み込む設計

が重要になります。


STORYも「1話追加するだけ」にした

ショートアニメも今後話数が増えていきます。

こちらも更新方法を統一しました。

<li>
  <a href="作品URL" target="_blank" rel="noopener">
    <span class="ep-no">
      第二十四話
    </span>

    <span class="ep-text">
      ここにあらすじ
    </span>
  </a>
</li>

基本的には既存の<li>をコピーして、

  • URL
  • 話数
  • あらすじ

だけ変更します。

作品ごとに特殊なHTMLを書く必要はありません。

更新頻度の高いサイトの場合、

「更新するたびに実装方法を考える」状態を作らない

ことも大切だと思っています。


最新情報はNEW RELEASEだけ見れば分かる

作品数が増えると、もうひとつ問題になります。

「結局、今どれが新しいの?」

という問題です。

そこでページ上部にNEW RELEASEを配置しました。

<ul class="news-list">
  <li>
    <span class="badge">NEW MV</span>

    <a href="#movie">
      量子の海で君に恋をした
    </a>

    <span class="news-note">
      ミュージックビデオ公開中
    </span>
  </li>
</ul>

新作を公開したときは、

  1. 本来のセクションへ作品を追加
  2. NEW RELEASEを差し替える

という運用です。

サイト全体を見直さなくても、

「最新作だけは常に上に出る」

ようにしています。


キャラクター画像はライトボックスで拡大

キャラクタープロフィールや設定資料には細かい情報が入っています。

ただ、最初から大きく表示するとページが非常に長くなります。

そこで通常時はレイアウトに合わせたサイズで表示し、クリックすると拡大するライトボックスを実装しました。

HTML側では、

<button
  class="zoom-btn"
  type="button"
  data-zoom="assets/img/asagi.png"
  data-zoom-caption="浅葱優 キャラクタープロフィール"
>
  <img
    src="assets/img/asagi.png"
    alt="浅葱優のキャラクタープロフィール"
    loading="lazy"
  >
</button>

JavaScript側ではdata-zoomから画像URLを取得します。

document.querySelectorAll('.zoom-btn').forEach(function (btn) {
  btn.addEventListener('click', function () {
    var img = btn.querySelector('img');

    openLightbox(
      btn.dataset.zoom,
      btn.dataset.zoomCaption,
      img ? img.alt : ''
    );
  });
});

これで、

一覧性を保ちつつ、必要な人には設定資料を大きく見てもらう

ことができます。

作品ポートフォリオと資料集の中間のようなUIになりました。


スクロールアニメーションもIntersectionObserverで

各コンテンツには軽いフェードインを付けています。

var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (entry) {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      io.unobserve(entry.target);
    }
  });
}, {
  rootMargin: '0px 0px -8% 0px',
  threshold: 0.08
});

CSS側では、

.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition:
    opacity .9s ease,
    transform .9s cubic-bezier(.16,1,.3,1);
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

としています。

すべての要素を常に監視する必要はないので、一度表示したら、

io.unobserve(entry.target);

で監視から外しています。


スマートフォンでは1カラムに変更

作品サイトなので、スマートフォンから閲覧される可能性も高いです。

PCではMVを3列表示しています。

.mv-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

1024px以下では2列、

@media (max-width: 1024px) {
  .mv-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

780px以下では1列です。

@media (max-width: 780px) {
  .mv-grid {
    grid-template-columns: 1fr;
  }
}

キャラクター紹介や小説配信先についても同様に、画面幅によってレイアウトを変更しています。

コンテンツそのものは同じなので、HTMLをPC用・スマホ用に分けてはいません。


アニメーションを苦手な人への対応

CSSには、

@media (prefers-reduced-motion: reduce)

も入れています。

@media (prefers-reduced-motion: reduce) {

  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    transition-duration: .001ms !important;
  }

  .reveal {
    opacity: 1;
    transform: none;
  }
}

OS側で「視差効果を減らす」などを設定している場合は、スクロールアニメーションを実質無効化します。

作品サイトなので演出は入れたい。

でも、演出を見ることを強制する必要はないと考えました。


地味だけどアクセシビリティも意識した

ほかにも、

<a class="skip-link" href="#about">
  本文へスキップ
</a>

を用意しています。

キーボード操作時には、

:focus-visible {
  outline: 2px solid var(--gold-lt);
  outline-offset: 3px;
}

でフォーカス位置が分かるようにしました。

ハンバーガーメニューにも、

aria-expanded="false"
aria-controls="globalNav"
aria-label="メニューを開く"

を設定しています。

開閉するとJavaScriptで、

toggle.setAttribute('aria-expanded', String(open));

toggle.setAttribute(
  'aria-label',
  open ? 'メニューを閉じる' : 'メニューを開く'
);

と状態を変更します。

また画像には、単なる、

alt="キャラクター画像"

ではなく、可能な範囲で内容が分かるaltを設定しました。

作品サイトでは画像そのものが重要な情報なので、このあたりも意識しています。


CSSはカスタムプロパティで世界観を統一

作品はサスペンス・ロマンスなので、全体を黒・赤・金で統一しました。

色はCSSカスタムプロパティにまとめています。

:root {
  --ink:        #0b0709;
  --ink-2:      #130d11;
  --ink-3:      #1c1418;

  --paper:      #f6f1ea;
  --text:       #e9e0d6;
  --text-dim:   #b3a79c;

  --crimson:    #c8102e;
  --crimson-lt: #e2394f;

  --gold:       #c9a227;
  --gold-lt:    #e5cd8a;

  --line:       rgba(201, 162, 39, .28);
}

たとえば今後、

金色をもう少し明るくしたい

となっても、各CSSを探して変更する必要がありません。

--gold
--gold-lt

を変更すればサイト全体に反映できます。

更新頻度が高いサイトでは、コンテンツだけではなく、デザイン側も変更箇所を集約することが重要だと思います。


更新方法をREADMEにも残す

自分で作ったサイトでも、数か月後には、

これ、どこを変更するんだっけ?

となります。

そこで更新方法もREADMEにまとめました。

基本的な更新方法は次の3つだけです。

MVを追加する

MOVIEセクションの、

<li class="mv-card">

をコピー。

data-ytにYouTubeの動画IDを設定します。

最新作なら、

<span class="tag-new">NEW</span>

を付けます。

話数を追加する

STORYセクションの<li>をコピー。

  • リンク先URL
  • 話数
  • あらすじ

を書き換えます。

最新情報を更新する

NEW RELEASE内の、

.news-list

を編集します。


あえてCMSにしなかった理由

今回のようなサイトなら、WordPressなどのCMSを使う方法もあります。

ReactやVueなどでコンポーネント化する方法もあります。

ただ今回は、

新作を作る
↓
YouTubeなどへ公開
↓
既存カードをコピー
↓
タイトルとURLを変更
↓
公開

までを簡単にすることを優先しました。

更新する本人がHTMLを触れるのであれば、作品数がある程度の規模までは、この方法でも十分運用できます。

技術的に高度なものを使うことよりも、

「自分が今後どう更新するのか」から逆算して技術を選ぶ

ことを意識しました。


実際に作って感じたこと

今回改めて感じたのは、

作品数の多いポートフォリオでは、制作時より更新時のことを考える方が重要

ということでした。

最初の10作品だけなら、多少特殊なHTMLを書いても困りません。

でも、

10作品
↓
20作品
↓
30作品
↓
さらに毎月追加

となったとき、それぞれ違う構造になっていると更新がつらくなります。

そのため今回は、

  • 同じ種類のコンテンツは同じHTML構造にする
  • data-*属性を利用する
  • コピーして内容を書き換えれば追加できるようにする
  • 重いコンテンツは必要になるまで読み込まない
  • CSSの共通値は変数へまとめる
  • 更新方法をREADMEに残す

という方針にしました。


今後やりたいこと

現状はindex.htmlを直接編集しています。

作品がさらに増えた場合は、

作品データ
↓
JSON
↓
JavaScriptでカードを生成

という方式への変更も考えています。

たとえば、

{
  "title": "量子の海で君に恋をした",
  "youtubeId": "D0cqbRSQ230",
  "cast": "優 & 御堂",
  "new": true
}

のように作品情報だけを管理できれば、HTMLを直接コピーする必要もなくなります。

さらに作品数が増えた段階では、

  • JSONによるコンテンツ管理
  • ビルド時のHTML生成
  • カテゴリ別フィルタリング
  • MVの自動並び替え
  • NEW判定の自動化

なども面白そうです。

ただし、今の規模ではそこまで作り込まず、

「今、一番簡単に更新できる構成」

を選びました。


まとめ

今回の作品サイトでは、豪華な機能を増やすことよりも、

作品をたくさん見せられて、しかも次の作品を簡単に追加できること

を優先しました。

特に効果が大きかったのは、

  • 1ページで作品全体を見せる
  • 同種類のコンテンツはカード構造を統一する
  • YouTubeはクリックされるまでiframeを生成しない
  • 画像は遅延読み込みする
  • 更新箇所を限定する
  • レスポンシブ対応をCSS側で完結させる
  • アクセシビリティやprefers-reduced-motionにも対応する

といった部分です。

ポートフォリオは「完成した作品を置く場所」ですが、創作を続けている限り中身は増え続けます。

だからこそ、

完成時にきれいなサイトではなく、半年後にも自分で更新できるサイトを作る。

今回はそこを一番大切にしました。


2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?