現代技術で作るWeb廃墟 feat.イラスト系個人サイト

この記事は [LIFULL Advent Calendar 2020](https://qiita.com/advent-calendar/2020/lifull) 4日目の記事です。

インターネット老人会会員です。最近の関心ごとはリヴリーアイランドのリニューアルです。

# Web廃墟とは
[Web廃墟(Enpedia)](https://enpedia.rxy.jp/wiki/Web%E5%BB%83%E5%A2%9F)より

> Web廃墟（ウェブはいきょ）とは、長期間にわたり更新が停止し、放置された状態になっているウェブサイトの俗称である。

> 多くの場合はスタイルシートが使われておらず、画像が少なくテキスト中心の個人サイトである。また、アクセスカウンタが設置されていることが多いことも特徴である。

## イラスト系個人サイトとは
私はイラストを描くタイプのオタクだったので、所謂 **個人サイト** という文化にたいへん馴染みがあります。
https://togetter.com/li/1618804

今でこそpixivやtwitterがオタクの社交場として主流ですが、数ヶ月に一度は個人サイトが話題になっています（体感）
私も例に漏れずタグを手打ち・コピペしまくっていたので、その度に非常にノスタルジーな気持ちに溢れています。
ダミーエンター……キリリク……うっ……頭が……😇

今は個人サイトを作ろうとするとWixなど素晴らしいサービスがありますし、そもそも古の文化は残っていません。
せいぜいマシュマロや質問箱が文化の片鱗を残している程度でしょうか。
そのせいか多くの個人サイトは **実質Web廃墟化** しているものがほとんどです。諸行無常。
※ちゃんと更新されているサイトももちろんあります！

ただ私はインターネット老人会のオタクなので、古代技術で作られたWeb廃墟に愛着があります。
しかしもうあの頃の技術（HTML4.1, CSSベタがき, jsコピペ）を使って開発は絶対に嫌です💢
そこで、今回は現代技術を使って**Web廃墟を偽装**していこうと思います。

# 成果物
![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171748/421469ea-a464-ea9b-5284-5d8c277fa420.png)
https://web-ruins.web.app/
今回はパンのイラストサイトという体で作ってみました。
イラストはすべて[いらすとや](https://www.irasutoya.com/)さんからお借りしています。いつもお世話になっています。

リポジトリも公開しています。
https://github.com/abcmPrivate/web-ruins/

# 開発環境
環境構築は省略します
## エディタ
- VSCode

Web廃墟といえば **メモ帳・ホームページビルダー・ファイルマネージャー** の三種の神器が主流ですが、
流石に現代において一つ一つタグ打ちコピペはしんどいです。
今回は現代エディタの代表格でもあるVSCodeを使います。Emmetのおかげで補完も楽々よ。

## フレームワーク
- Nuxt.js

フレーム（物理）ではない。

## ホスティング
- Firebase

オタクは黙って **FC2 || 忍者ツールズ || ロリポップ || さくらサーバー || 魔法のiらんど** と言われそうですが
こちらのほうが今時っぽいのでこうします。~~ジオシティーズ？あっ……（サ終）~~

最終的には以下を叩くだけで更新完了です。わ〜いらくち〜ん！

```bash
$ npm run generate
$ firebase deploy
```

# Web廃墟であるための要素
今回は00年代後半の廃墟をイメージして作成していこうと思います。
デザインは原色バリバリからシンプルに移行しつつ、古の技術が幅を利かせていた時代です。
## ページデザイン
### PC? SP?
まずここから問われそうですが、今時の技術なのでPC/SPのレスポンシブ対応はしています。
ただ、00年代にスマホなんてものは存在していないのでPCのフレームデザインをベースに構築しています。
~~魔法のiらんど風はちょっと時間がなかったです~~

### アンチエイリアスのかかっていないフォント
今のフォントはデフォルトできれいですよね。
ただ、Web廃墟にきれいなフォントは御法度です。アンチエイリアスはどんどん切っていきたい。
bodyに **-webkit-font-smoothing: none;** を突っ込んでギザギザフォントを錬成します。
ただしWindowsは未対応のため、font-familyでMSゴシックを突っ込んでせめてもの抵抗をします。

```scss
body {
  -webkit-font-smoothing: none;
  font-family: "ＭＳ ゴシック","メイリオ",sans-serif;
}
```

### 色の指定はnamed
カラーコードの指定は御法度です。ネームドでつけましょう。

```scss
.hoge {
  color: #f00; // NG
  color: red; // OK!!
}
```
ネームドで行うということはニュアンスカラーは使えません。頑張りましょう。

### フレーム
フレーム（物理）。
[阿部寛のホームページ](http://abehiroshi.la.coocan.jp/)では現役で使われているアレです。

ミソとしては以下でしょうか

* フレームの幅は可変できる
  * ただし上限・下限は設定したい（再現なくいくと辛い）
* 画面が変わった場合もフレーム幅は維持しておく
  * 画面遷移のたびに幅が変わるとフレーム感薄れますからね
* フレームの画面で使い回しができるようにする

今回はこれらをComponent, Store, Layoutsで再現していきます。

#### フレームの幅は可変できる
Vueのリアクティブな性質を生かして、ボーダーの要素をドラッグしてフレームの幅を操作できるようにしてみます。
本来ドラッグできないDOMの場合は `draggable` をつけると操作が可能になります。
ただそのままイベントを貼ってもうまく検知できないため、 `@dragover` のイベントをキャンセリングするとうまくいきます。

```vue:components/layouts/frame.vue
<template>
    <div class="frame-border" draggable @drag="onDragBorder" @dragover="onDragOverBorder"></div>
</template>
<script>
const DEFAULT_WIDTH = 280;
export default {
  data() {
    this.navWidth = DEFAULT_WIDTH;
  },
  methods: {
    onDragBorder(e) {
      const width = e.clientX;
      // default未満にしない
      if (width <= DEFAULT_WIDTH) {
        this.navWidth = DEFAULT_WIDTH;
        return;
      }
      // 画面幅の50%は超えさせない
      const half = window.innerWidth / 2;
      if (width >= half) {
        this.navWidth = half;
        return;
      }
      this.navWidth = width;
    },

    // ドラッグオーバー時に元の幅に戻ろうとするので阻止用
    onDragOverBorder(e) {
      e.preventDefault();
    },
  },
};
</script>

```

こんな感じになります

![フレーム幅可変.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171748/e3bdbe3a-e97a-da85-4e1a-34c1753f2628.gif)

#### 画面が変わった場合もフレーム幅は維持しておく

```vue:components/layouts/frame.vue`
<script>
const DEFAULT_WIDTH = 280;
export default {
  computed: {
    subStyle() {
      const navWidth = this.$store.state.navWidth;
      return {
        width: `${navWidth}px`,
      };
    },
  },
  methods: {
    onDragBorder(e) {
      const width = e.clientX;
      // default未満にしない
      if (width <= DEFAULT_WIDTH) {
        this.$store.dispatch('resetNavWidth');
        return;
      }
      // 画面幅の50%は超えさせない
      const half = window.innerWidth / 2;
      if (width >= half) {
        this.$store.dispatch('updateNavWidth', half);
        return;
      }
      this.$store.dispatch('updateNavWidth', width);
    },
    onDragOverBorder(e) {
      e.preventDefault();
    },
  },
};
</script>
```

storeはこんな感じです。値を受け取って更新するだけなのでシンプル。

```js:store/index.js
const DEFAULT_NAV_WIDTH = 280;
export const state = () => ({
  navWidth: DEFAULT_NAV_WIDTH,
});

export const mutations = {
  setNavWidth(state, { navWidth }) {
    state.navWidth = navWidth;
  },
};

export const actions = {
  updateNavWidth({ commit }, navWidth) {
    commit('setNavWidth', { navWidth });
  },
  resetNavWidth({ commit }) {
    commit('setNavWidth', { navWidth: DEFAULT_NAV_WIDTH });
  },
};
```

あとはlayoutsにフレームと固定しておきたいもの（今回だとナビゲーション）を設定すればOKです。

```vue:layouts/contents.vue
<template>
  <Frame>
    <template v-slot:main>
      <Nuxt />
    </template>
    <template v-slot:sub>
      <Navigation />
    </template>
  </Frame>
</template>

<script>
import Frame from '@/components/layouts/frame.vue';
import Navigation from '@/components/blocks/navigation.vue';
export default {
  components: {
    Frame,
    Navigation,
  },
};
</script>
```


## コンテンツ
### エンターページ
indexはもちろんエンターページです。
個人サイト特有の注意文やダミーエンターもしっかり装備していきましょう。
同盟バナーや毒吐きネットマナーもあると最高ですね。

ダミーをクリックすると **alertでキレ散らかす＆Yahoo!(not google)へ飛ばす** ようにすると廃墟感が上がってGOODです。

```vue:pages/index.vue
  methods: {
    onClickDummy() {
      alert('注意文を読んでください！！！！！！！！！！！');
      location.href = 'https://yahoo.co.jp/';
    },
  },
```

### logs（イラストコーナー）
メインコンテンツ。何個かイラストがたまったら新しいページを作る運用を想定しています。
更新のたびに `*.vue` ファイルが増えていくのは煩雑ですし、イラストを淡々と載せるだけなので同じレイアウトを使いまわしたいです。
そのため、Nuxtの動的ルーティングを用いて実装します。

まずは設定用マスタを作成します。

```js:settings/logs.js
export const logs = {
  2018: [
    {
      path: 'pan_bread_set.png',
      caption: 'トップ絵です',
    },
    ...
  ],
  2019: [
    {
      path: 'cafe_morning_coffee_set_ogura.png',
      caption: '名古屋行きたい',
    },
    ...
  ],
  2020: [
    {
      path: 'cooking_hotsand_sandwich_pan.png',
      caption: 'ホットサンドメーカー便利',
    },
    ...
  ],
};
```

動的ページは `params` から `pageId` を受け取り、それに応じたデータをマスタから呼び出します。

```vue:pages/logs/_pageId/index.vue
<template>
  <article class="logs">
    <ul class="logs-items">
      <li v-for="(log, index) in pageLogs" :key="index" class="logs-item">
        <!-- 画像タグが入る -->
      </li>
    </ul>
  </article>
</template>

<script>
import { logs } from '@/settings/logs.js';

export default {
  asyncData({ params }) {
    const pageId = params.pageId;

    return {
      pageId,
    };
  },
  data() {
    return {
      logs,
      pageId: 0,
    };
  },
  computed: {
    pageLogs() {
      return this.logs[this.pageId];
    },
  },
};
</script>
```

導線もマスタに合わせて自動で増やしていきます。
これでイラストを更新する際はファイルアップ＆設定ファイルに追記するだけでよくなりました。

```vue:components/blocks/navigation.vue
<template>
  <ul class="navigation-logs">
    <li v-for="(key, index) in logsArray" :key="index" class="navigation-logItem">
      <nuxt-link :to="{ name: 'contents-logs-pageId', params: { pageId: key } }">■</nuxt-link>
    </li>
  </ul>
</template>

<script>
import { logs } from '@/settings/logs.js';

export default {
  computed: {
    logsArray() {
      return Object.keys(logs);
    },
  },
};
</script>
```

### bkm
**ブクマ** と読みます。

こちらもマスタデータを更新したらリンクリストが展開されるようにしています。
造りはイラストとほぼ変わらないので解説は省略。

### Web拍手
ボタンをクリックするとランダムでお礼イラスト・メッセージがでるWebサービスです。
利用者からメッセージを送信することもできるので、サイト管理者への連絡手段としても重宝されています。

今回はイラストのランダム表示とメッセージ送信機能を手作りします。

#### イラストのランダム表示
![randomPan.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171748/e11b368e-9a27-6836-4c28-7772b60be702.gif)

マスタで画像・メッセージを定義してimport。
拍手ボタンを押した時にランダムにキーを算出して、表示する内容を決定します。

特別な工夫はしていませんが、画像の動的な呼び出しは `:src="require(path)` としないと正常に行えないのでそこだけ注意が必要です。

```js:settings/index.js
export const clapImage = [
  {
    path: 'bread_syokupan_medamayaki.png',
    caption: '目玉焼きを乗せた食パンです🍞',
  },
  ...
];
```

```vue:pages/clap/index.vue
<template>
  <div class="clap">
    <ImageBlock class="clap-image" :image-object="randomImage" />
    <div class="clap-wrapper">
      <p>拍手ありがとうございます🍞</p>
      <p>お礼絵は現在5種類です。</p>
      <nuxt-link v-slot="{ href }" :to="{ name: 'clap' }">
        <form :action="href" method="POST" @submit.prevent class="clap-form">
          <div class="clap-buttonWrapper">
            <button type="submit" @click="onClap">
              {{ buttonText }}
            </button>
          </div>
        </form>
      </nuxt-link>
    </div>
  </div>
</template>

<script>
import { ImageBlock } from '@/components/blocks/image.vue';
import { clapImage } from '@/settings/index.js';

const createKey = () => {
  return Math.floor(Math.random() * clapImage.length);
};

export default {
  components: { ImageBlock },
  asyncData() {
    return {
      randomKey: createKey(),
    };
  },
  data() {
    return {
      randomKey: 0,
    };
  },
  computed: {
    randomImage() {
      return clapImage[this.randomKey];
    },
  },
  methods: {
    onClap() {
      this.updateImage();
    },
    updateImage() {
      this.randomKey = createKey();
    },
  },
};
</script>
```

```vue:components/blocks/image.vue
<template>
  <div class="imageBlock">
    <figure class="imageBlock-imageWrapper">
      <img :src="require(`~/assets/images/${path}/${imageObject.path}`)" />
      <figcaption v-if="imageObject.caption" class="imageBlock-caption">{{ imageObject.caption }}</figcaption>
    </figure>
  </div>
</template>
```

#### メッセージ送信機能
Firebase Functionを使ってGmailへ送られる仕組みにしました。
こんな感じできます。
![スクリーンショット 2020-12-03 21.48.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171748/674d901c-2a49-2599-2abf-b89ffb26de77.png)


実装は以下の記事を参考にさせていただきました。ほぼこのままのコードですので解説は省略。
[Nuxt + nodemailerでメールフォーマットを作る](https://qiita.com/sychocola1/items/c63ab41473e983f95c52)

### † 裏 †
表側には置いておけないもののスペース、通称**† 裏 †**。
普通にアクセスさせないために、ベーシック認証をかけます。

今回は [nuxt-basic-auth-module](https://github.com/potato4d/nuxt-basic-auth-module) というプラグインを利用させていただきました。
限られたルーティングのみ認証をかけることもでき、大変便利です。

今回は `/ura/contents` 系のページに認証をかけました。初回に聞かれます。

```js:nuxt.config.js
export default {
  // ...
  // basic auth (https://github.com/potato4d/nuxt-basic-auth-module#readme)
  basic: {
    name: 'yes',
    pass: 'sushi',
    message: 'password hint: 管理人の好きな日本食(ローマ字で五文字)',
    enabled: true,
    match({ url }) {
      return (url || '').startsWith('/ura/contents');
    },
  },
  // ...
}
```

残りのページの造りは表側とほぼ変わらないので省略します☺️

# 今回未実装のネタ
- アクセスカウンター&キリばん
- アクセス解析
- 夢小説
  - input textとv-modelで一発で作れそう。ただ文才が無い
- 日記(memo) 

気が向いたら実装予定です。

# 完走した感想
## Web廃墟（偽装）は楽しい
インターネット老害なのでこういった懐かしコンテンツのことになるとハッスルしますね…😇
昔にせっせとタグをコピペしていた時代を思い出したと同時に、
作りたいものをどう実現するかを考えるのが楽しかったです。

## 浦島 firebase 太郎だった
料金体系がSpark(無料)からBlaze(従量課金)になった影響か、
cloud functionのNodeのVersionが上がっていたりセキュリティルールが通らなくなっていてかなり焦りました😇
その結果アクセスカウンターまで作り込めなかったのが反省点です。
Firestoreとcookieでなんとかしたかった……

# 蛇足：ネタかぶりしました
[Vue.js + Firebaseで懐かしい気持ちになるポートフォリオ作った - Never catch a cold.](https://alliance7.blogspot.com/2019/01/vuejs-firebase.html)
記事を書いている最中にネットサーフィンしていたら2年前の先駆者様を発見しました。
こちらの方は**THE2000年代初頭**な廃墟で非常にノスタルジーな気持ちになりました。
こういうのも作ったなあ……

ただ、イラスト系個人サイトの方向性は被ってなかったのでそのまま公開します。
少しでも懐かしい気持ちになったり床を転げ回ってもらえたら嬉しいです。
ご覧いただきありがとうございました🙇‍♀️

# bkm
[nuxt-basic-auth-module](https://github.com/potato4d/nuxt-basic-auth-module)
[Nuxt + nodemailerでメールフォーマットを作る](https://qiita.com/sychocola1/items/c63ab41473e983f95c52)