⚗️

学校主催の対外イベントの公式サイトを作りました【Nuxt 3】

2023/02/22に公開

今回、私の通っている某中高一貫校が主催する科学イベント「○○○○地域フォーラム2023」の公式サイトの製作を担当しました。

スクショ
え、もはや本文で学校名を隠す必要がないじゃないかって?
それは言わないお約束だぞ

1. ウェブサイト概要

外部からの入場者も見込んだイベントの内容紹介のための特設サイトです。

弊校に興味のある小中学生や地域の皆さんに幅広くこのイベントを知ってもらえるように、SEOに配慮したウェブサイトとするため、以下のような構成にしました。

  • 経費削減のため、WordPressなどのSSRシステムは使用しない
  • ページ読み込み速度改善 + SEOのために、SSGを採用

また、イベントの情報の更新を簡単にするための案内資料ビルダーも同時に制作し、先生側でのデータ入力の効率化も行いました。

さらに、何を思ったのかイベント前日になって体験型ブースの残数や待機人数をリアルタイムで確認できるシステムを作成しました。

2. なぜ作ったのか

  • 新型コロナウイルス パンデミック開始後、初めて一般入場者の受け入れを行うこともあり、イベントの再周知が必要になったため
  • 弊校における国からの補助制度の成果を発表する一大イベントなので、本気を出す必要があるため
  • 実績を作って進路面で得をするため

3. 技術構成

今回は「経費削減」も大きな目標としています。実際、今回のウェブサイトの運用に際して費用は 1 円も発生していません[1]

3-1. ウェブサイト本体

フレームワークには、先日正式版がリリースされたばかりの Nuxt3 を採用。Nuxt3 は Beta 版のころからちょくちょく触っていたので、スムーズに開発を進められました。また、ようやく重い腰を上げて TypeScript を一部に導入しました。VSCode で自動補完が効いて気持ちいいですね~!

コンテンツ管理

頻繁に更新が必要なコンテンツ[2]の管理には、Nuxt 公式モジュールの Nuxt Content v2 を使用。Document Driven モードが注目されていますが、なんか自由度が低そうな感じがした[3]ので、今回そちらの採用は見送っています。

また、簡単なお知らせは特設サイト本体を通すことなく更新できるようにするため、学校公式サイトの WordPress 上で投稿した記事を、WP REST API のラッパー API[4] 経由で参照させています。

CSSフレームワーク

スタイルの調整には、Tailwind CSS をメインに据えたうえで、フォームコンポーネントにのみ Bootstrap を採用しています。

Bootstrap を採用した部分のスクショ
Bootstrap は、イベント情報ページの「絞り込み検索」で使用しています

フォームにだけ Bootstrap を採用したのは、

  • Tailwind CSS でフォームを作ると見た目が微妙になってしまう
  • classが他の部分の比にならないレベルで肥大化する
  • Bootstrap のフォームコンポーネントが凄く作りこまれていたので、自作するよりそちらを利用したほうがいい

と考えたためです。皆さんもぜひやってみてくださいね!

【おまけ】Bootstrap をフォームのみで使用するためのSCSS(コピペでそのまま使えます)

Bootstrap v.5.2.3 で動作確認済。フォームとボタンの class が使えるようになります

bootstrap-forms.scss
@import "bootstrap/scss/mixins/banner";
@include bsBanner("Forms & Buttons");

// scss-docs-start import-stack
// Configuration
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/utilities";

// Layout & components
@import "bootstrap/scss/root";

// Breaking down form components
@import "bootstrap/scss/forms/labels";
@import "bootstrap/scss/forms/form-control";
@import "bootstrap/scss/forms/form-select";
@import "bootstrap/scss/forms/form-range";
@import "bootstrap/scss/forms/form-check";
@import "bootstrap/scss/forms/input-group";

// button
@import "bootstrap/scss/buttons";
@import "bootstrap/scss/button-group";

ホスティング アーキテクチャ

今回も、私がいつもお世話になっている Cloudflare Pages を採用しました。最近環境の構築が短縮されたのと、Nuxt3 に内蔵の Vite の爆速ビルドのおかげで、1 分ちょっとでデプロイが完了します。

Cloudflare Pagesのデプロイログ
たった 1 分半でデプロイが完了

しかも Cloudflare Pages は世界中の CDN で参照できるようになるため、世界中どこでも最速で読み込まれます!お値段はなんと無料!すごい!

3-2. その他のツール

案内資料ビルダー

案内資料ビルダーのスクショ
なんか私いっつもビルダー作ってますね

案内資料ビルダーは、急ごしらえで構築する必要があったため、以前文化祭で使用したものをほぼそのまま流用しました。

https://zenn.dev/kakkokari_gtyih/articles/d592fd1d1e45dd#問題を作るやつ(問題ビルダー)

HTMLベタ打ち + Bootstrap + Vue 3 という、いかにもやる気のない構成となっておりますが、一般公開するものじゃありませんのでこんなので大丈夫です。

操作方法は文化祭でのものとほぼ変わらずですが、今回はプログラミングに明るくない先生方も使用するということで、使用方法の案内動画を作成しました。ここでお見せできるかどうかは確認中です…

リアルタイム残数カウンター

リアルタイム残数カウンターのスクショ

体験型ブース では待機列や残数が発生するため、それらを把握できるようにすることで、利用者に効率よくイベントを回ってもらおうと考え、システムを作成しました。

カウンター自体のフロントエンドは Vue 2 を使用しました。なぜわざわざ Vue 2 を採用したのかというと、このシステムを古い端末(Android 4.2 搭載のポンコツタブレット、チャレンジパッド2 など)で動作させることを検討していたためです。

しくみは非常に単純で、API に1秒毎にリクエストを打ち続けるというものです。


時間がなかったため、Server-Sent Eventsとかは使わなかった

そして、バックエンドと管理画面は PHP で作成。平文テキストファイルに残部数などを書き込んだり読み出したりする方式を採用しました。

このシステムは

  • 操作する人が限られていた(信頼できる人しか操作しないシステムだった)
  • フロントエンド・管理画面ともに URL は非公開で運用した
  • 時間がなかった

ため、管理画面のログイン認証を除き、セキュリティに関してはかなり甘く見ています。とは言っても、数字以外を投げたり、負の値を投げたりするとエラーで弾くようにしてあるので、全く対策をしていないわけではありません。

4. デザイン

デザインに凝ったわけではないのですが、一応工夫したところはあるので紹介をばと…

4-1. イベントのタイムライン表示

イベントは複数の教室で時間を区切って行われます。それらを効率的に網羅するために、タイムライン形式の一覧表示を CSS Grid で作成しました。

grid-auto-flowdense を指定することで、空いた位置が自動で埋まるようになっています。もうちょっと応用すれば、番組表とかにも使えそうですね!CSS Grid の可能性が広がります。

CSS Gridを使用したタイムライン表示
情報の入力待ちで、真価を発揮できていないときの図

4-2. ページ遷移

楽しいイベント感を演出するために、ページ遷移にアニメーションをつけています。

また、イベントの詳細ページでは、いちいちこのアニメーションが出てくるとクドいので、無効にしてあります。このアニメーションはかなりトリッキーな方法で実装しています。気になる方は下を御覧ください。

ページトランジション実装

トランジション出し分け制御

/nuxt.config.ts
export default defineNuxtConfig({
    app: {
        pageTransition: {
            name: 'page',
            mode: 'out-in'
        }
    }
});
/middleware/disable-transition.global.ts
export default defineNuxtRouteMiddleware((to, from) => {
    if (from.path.includes("/events") && to.path.includes("/events")) {
        // イベントページではアニメーション無効
        from.meta.pageTransition = false;
        to.meta.pageTransition = false;
    } else {
        // 他のページではアニメーションを実施
	// (設定が消えていたとき用に念のため上書きしておく)
        from.meta.pageTransition = {
            name: 'page',
            mode: 'out-in'
        };
        to.meta.pageTransition = {
            name: 'page',
            mode: 'out-in'
        };
    }
});

トランジション アニメーション実装

何をやっているか: 大きな円形の要素(縦横比 1:1)を設置して、そこのborderを広げたり狭めたりしています。

/assets/css/main.css
.page-enter-active,
.page-leave-active {
    position: relative;
    overflow: hidden;
    transition: all;
}

/* ナビゲーションが空いているときはアニメーションを素早く終了する */
.nav-open .page-leave-active,
.nav-open .page-leave-active::before {
    transition-timing-function: step-start;
    transition-duration: .5s;
    transition-delay: 0s;
}

.page-leave-active,
.page-leave-active::before {
    transition-duration: .5s;
    transition-delay: 0s;
}

.nav-open .page-enter-active,
.nav-open .page-enter-active::before {
    transition-delay: .6s;
    transition-duration: .5s;
}
.page-enter-active,
.page-enter-active::before {
    transition-delay: .5s;
    transition-duration: .5s;
}

.page-enter-active::before,
.page-leave-active::before {
    content: "";
    @apply fixed border-0 border-secondary-500 z-[9997];
    top: calc(calc(100vw - 50vh) * -1);
    left: -50vw;
    width: 200vw;
    height: 200vw;
    border-radius: 50%;
    transition-property: border-width;
    transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

@media (orientation: portrait) {
    .page-enter-active::before,
    .page-leave-active::before {
        left: calc(calc(100vh - 50vw) * -1);
        top: -50vh;
        width: 200vh;
        height: 200vh;    
    }
}

.page-enter-from::before,
.page-leave-to::before {
    border-width: max(100vw, 100vh);
}

5. 発覚した問題

5-1. サブドメインの取得に高額請求が発生!?

計画当初は、SEO的観点および見た目の信頼性から、弊校ホームページのサブドメインを取得して公開する予定でした。しかし、弊校ホームページは業者が全面的に管理しており、確認の結果、サブドメインの登録サブドメインの設定代行には別料金がかかることが判明。

そこで、断腸の思いでサブドメインを断念。Cloudflare Pages のデフォルトドメインでの公開と相成りました。

【2023年10月02日追記】いろいろありまして、サブドメインの取得が完了しました!

https://forum.tonko.ed.jp

5-2. Google Search Consoleが動かない

サイトがある程度完成したタイミングで Google Search Console の手続きを開始したのですが、なぜか Google が動いてくれません。イベント一覧ページが全く読み込まれないのですが、なんか悪いことしちゃいましたかね…?

色々こねくり回して、トップページが検索結果に表示されるようになりました。ちょっと外したようなキーワードでも上位に入っているので、SEO対策はわりと成功したのかも?

富田林市内
富田林市内では、「地域フォーラム」だけで検索結果3位に浮上

6. 結果報告

詳細な情報が出るまでしばらくお待ち下さいませ🙏

あと、各方面から承認を得られれば、サイトのソースコードを公開することも検討中です。

7. ふりかえり

2023年3月3日・4日に、予定通りイベントは開催されました。

「微妙な時期だし、手を打たなければ人は来てくれない気がする」と考えて宣伝を打ちまくった結果、当方の予想を遥かに上回るとんでもない人数にご来場いただきました。

その結果、体験型ブースの椅子や机が足りず急遽用意したり、終いには薬剤が底をつきブースの営業を打ち切らせていただくなど、参加者の皆様には多々ご迷惑をおかけしました。


大量に準備したつもりだったのですが…
(写真の外にもこれがあと4机分くらいありました)

また、延長コードが断線して拡声器・プロジェクター・PCが全てお釈迦になるという致命傷なハプニングなど、問題も多々発生。かくいう私も、ホバークラフトの運転補助で足腰を酷使し、筋肉痛で寝たきりになりながらこの記事の執筆を行っています。

体験型イベントを行うのは今回が初めてで、上記のように至らない点も本当にたくさんありましたが、来年度は今回の反省点を踏まえてイベントが開催されると思いますので、是非お楽しみに!

8. 謝辞

  • 公立学校で
  • 生徒が作成したホームページが
  • 公式として公開される

ということは、めったにないケースだと思います。このような事ができる本校の柔軟な体制にある種の誇りと責任感を感じるとともに、今回のページ作成に特に協力してくださった以下の皆さんに、心から感謝申し上げます(順不同)。

  • I先生
  • Y先生
  • K先生
  • S先生
  • 科学部のみなさん
  • 「ジョウショウ」のみなさん
  • 探究Ⅱ 受講者のみなさん

また、本記事公開後には多数の反応をいただき、非常に励みになりました!重ねて御礼申し上げます。

この記事を最後までご覧になった JST 職員の方へ

お時間がない中、このような記事をご覧いただき、ありがとうございました!

本校には、私なんかよりも素晴らしい活動をしている人が何人もいます。実際に私もその人達を知っていますし、その人達の凄さを広く一般に紹介できるよう尽力しているところです。

それこそ、今回の特設サイトで紹介した100を超える研究班の発表は、文理問わず「導入→仮説→実験(聞き取り調査)→考察(それをもとにした提案)」といった研究のサイクルがしっかりと確立されたものばかりです。

また、地域に根ざした研究や、現代的な視点から切り込んだ研究など、独創的なものも多く、これからの研究の展開が楽しみなものも多くあります。

ぜひ、これらの研究活動についてより興味を持っていただき、賛同いただけるようでしたら、引き続き JST や文科省からの強力なバックアップをお願い致します!


追伸:第三者から見た本イベントの感想なども公開されています。是非併せてご覧ください。

https://creators.yahoo.co.jp/okukawachiinfo/0100410624

脚注
  1. 知りうる限りは ↩︎

  2. イベント情報や、イベントの注意事項で使用 ↩︎

  3. ドキュメンテーションを作るときとかならこれでもいいと思うんだけど、普通のウェブサイトには…ちょっと…ねぇ… ↩︎

  4. ラッパー API を作成したのは CORS エラーを回避するため。ラッパー API は PHP で作成しました。 ↩︎

Discussion