九尾空間

この世はとてもキュービックな空間ですね

Svelte + TailwindCSS + daisyUI で雑多な倉庫ページを整備する

以前、「実験的に作った細々としたものを雑に公開できるように」と思って倉庫ページを作っていたのですが、今日はそれをSvelte + TailwindCSS + daisyUIで書き直しました。

以前はNext.js + MUI で実装していたのですが、以下の理由からSvelteで全面的に書き直してみました:

  • 一覧ページ生成用のコードをあまりスケールする作りに出来なかった
  • 一覧ページ生成に使うメタデータの管理方法に不満が出てきた
  • そこまで複雑なSPAでもないのにNext.jsは少しオーバーキルすぎる感触だった
  • 単純にSvelteが使ってみたかった

まだまだ見た目的に綺麗に出来そうな余地は多分にありそうですが、そもそも倉庫にものがないと始まらないので、この後は中身を作るほうに注力しようかなと。

この記事では、こういう倉庫みたいなページを作る上で検討した事項を軽く備忘録的にご紹介できればと。

はじめに: 実験成果物の倉庫どうするよ問題

この記事をご覧になっているWeb開発者の中には、日々実験的にHTMLやJavaScriptを書き散らしていらっしゃる方も居るかと思います。そういった実験の結果、大きなサービスとして公開するまではいかないものの、みんなにちょっと見せて反応を知りたいものが出来たとき、どのようにして公開するのが良いのでしょうか。

CodePenなどのサービスにアップして公開するのも選択肢のひとつですが、サービスの終了などがあった時に消えてしまうリスクなどを考えると、なかなかそれにどっぷり依存というのもちょっと怖いものです。実際私も、以前 jsdo.it というサイトに上げていた思い思いのJavaScriptは既に電子の泡として消えてしまい、とても悲しい思いをしたことがあります。

また、自分が作ったものがなんか大きなサービスの賑やかしみたいになって何かいやだなーって思ったりもします。

結局私は、実験的に作った細々としたものを雑に公開できるWebサイトを作ってしまうことにしました。とはいってもあまりかっちりしたものを作ってもコンテンツ追加のハードルが上がってしまいます。

ものを置きやすい倉庫の要件とは?

今回は、ものの置きやすさを念頭に以下を意識して倉庫用のサイトを作成しました:

  • 昔(中学生ぐらいの頃)、個人サイトを更新していた時ぐらいの感覚で雑にHTMLがアップできる
    • コンテンツの追加は極力、所定のサブディレクトリにHTML, CSS, JS, 画像ファイルを置くだけでできる
    • 個別のコンテンツ追加時にVite等のビルド設定をいじる必要が無い
  • 一覧ページの仕組みはのちのち書き直す前提
    • 今回Svelteに書き直すことにしたのと同じようなことが今後起こりうる
    • アップする個別のコンテンツは一覧ページの生成用に用意したビルドツールで中身を変換しない
    • Vanilla JS、もしくは必要に応じて個別コンテンツ内でビルドを設定する
  • だからといって一覧ページはあまり手を抜かない
    • やっぱり自分が作ったものが一覧になるとモチベーションに繋がる
    • いつどんなものを自分が作ったか後で振り返るための資料にもなる
    • ポートフォリオにもなる

フォルダ構成

今回、上記の要件を踏まえて以下のようなフォルダ構成を採用しました (Svelteで作った一覧ページ生成用コードなどは除く):

/
├ src/
│ └ catalog.yaml ... 倉庫に置いたファイルのメタデータを記述するファイル
└ static/
  ├ _catalog/ (自動生成) ... src/catalog.yaml を自動変換してページ分けしておく
  │ ├ 1.json
  │ └ 2.json
  ├ content-1/
  │ ├ index.html
  │ ├ style.css
  │ ├ main.js
  │ └ ...
  ├ content-2/
  │ ├ index.html
  │ ├ style.css
  │ ├ main.js
  │ ├ thumbnail.png ... 一覧ページにサムネイル画像が必要だったらここに置く
  │ └ ...
  └ ...

この構成で新しいコンテンツを追加するときは、大体こんな手順を踏む想定です:

  1. staticフォルダ内に新しいフォルダを作って、そこにHTMLファイル等を入れる
  2. もし良い感じのサムネイルが用意できそうならstaticフォルダに入れる
  3. catalog.yamlを開いて、古いコンテンツのメタデータをコピペして新しいコンテンツ用のメタデータを作る
  4. 手元で軽く動作確認 (npm run dev)
  5. 正しく動いてそうならGitでコミット
  6. (あとはGitHub + CloudFlare Pagesにお任せ)

沢山コンテンツが増えてきたときにサイトが重くならないよう、catalogをページネーション化する仕組みを作りました。とはいっても難しいことはしておらず、src/catalog.yamlを読み込んで新しい順に10件ずつ分割して、連番のJSONファイルをstatic/_catalog/フォルダ以下に生成するだけです。

scripts/build-catalog.ts

import fs from 'node:fs';
import fsExtra from 'fs-extra';
import path from 'node:path';
import yaml from 'js-yaml';

// 1ページあたりに表示する項目数
const elementsPerPage = 10;

// 入力のカタログファイルのパス
const inputFilePath = path.join(__dirname, '../src/catalog.yaml');

// 出力先のディレクトリパス
const outputDirPath = path.join(__dirname, '../static/_catalog');

interface ContentMetaData {
    path: String;
    title: String;
    created_at: String;
    description: String;
    thumbnail?: String;
}

console.log('Building catalog files...');

const catalog = yaml.load(fs.readFileSync(inputFilePath, 'utf-8')) as ContentMetaData[];
const pageCount = Math.ceil(catalog.length / elementsPerPage);

fs.mkdirSync(outputDirPath, { recursive: true });
fsExtra.emptyDirSync(outputDirPath);

let currentPageNum = 1;
let currentPageMetaData: ContentMetaData[] = [];
const renderCurrentPage = () => {
    if (currentPageMetaData.length == 0) return;

    fs.writeFileSync(
        path.join(outputDirPath, `${currentPageNum}.json`),
        JSON.stringify({
            pageCount,
            metadata: currentPageMetaData
        }),
        'utf-8'
    );

    currentPageNum++;
    currentPageMetaData.length = 0;
};

let metadata;
while ((metadata = catalog.pop())) {
    currentPageMetaData.push(metadata);
    if (elementsPerPage <= currentPageMetaData.length) renderCurrentPage();
}

renderCurrentPage();

今回 vite-plugin-watch というプラグインを使って、npm run dev 中に src/catalog.yamlが更新されたことを検知したら自動で再ビルドがかかるようにしてあります。 だいたいこんな感じです:

package.json

{
    ...,
    "scripts": {
        "dev": "npm run build-catalog && vite dev",
        "build": "npm run build-catalog && vite build",
        "build-catalog": "vite-node scripts/build-catalog.ts",
    ...
    },
    ...
}

vite.config.ts

import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { watch } from 'vite-plugin-watch';

export default defineConfig({
    plugins: [
        watch({
            pattern: 'src/catalog.yaml',
            command: 'npm run build-catalog',
            onInit: false // onInitがオンだと、SvelteがJSONファイルの更新を検知 → Viteの再読込 → vite-plugin-watchがJSONを更新の無限ループが起こるのでオフにしておく。
        }),
        sveltekit()
    ]
});

これでSvelteのViteが起動する前に static/_catalog 以下が毎回更新されるようになるので、あとはそれを普通のSvelteのやり方でfetchして表示する実装をすれば、良い感じにページネーションが実装できます。

src/routes/[[page=integer]]/+page.ts

import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';

export const prerender = 'auto';

export const load: PageLoad = async ({ fetch, params }) => {
    const page = Number(params.page ?? 1);
    if (Number.isNaN(page) || page < 1) {
        error(404, 'Not found');
    }

    const res = await fetch(`_catalog/${page}.json`);
    const data = await res.json();

    return {
        ...data,
        currentPage: page
    };
};

今後の展望

今後の展望ですが、やはり目下としては折角作った倉庫ですのでコンテンツを増やしていきたいです。 個人的にはかなりハードルの低い倉庫が出来たと自負していますので、今後に乞う御期待です。

ただ、CloudFlare Pagesは1ファイル25MB制限があるらしいので、どこかのタイミングでレンタルサーバーに載せ替えが必要になるかもなと思っています。 最近またさくらのレンタルサーバーを契約したので、こちらも必要に応じて対応していきます。