📃

Svelte-Kit / GitHub Pages / Googleスプレッドシートで簡単に動的に更新が可能なWebページを作る

2024/12/29に公開

n13uです. 今回 力強くブログを108記事アウトプットする日開催にあたり、当日参加した方が書いたブログをまとめるサイトを作ることになり、そこで使った技術のお話です.

どんなサイトを作ったか

forceoutput.github.io
を作りました.

参加者が書いたブログを自分でGoogle スプレッドシートに書き加えることでコンテンツが更新されブラウザ側で表示がされます.

Google スプレッドシート+GASで簡易的なCMSもどきを作る

今回、投稿したブログの情報をまとめるにあたってGoogleフォームやなんらかのサービスを使うことも考えました. ですが今回以下の点にあたりGoogleスプレッドシートを選びました

Googleスプレッドシートで1行目以外のデータを配列で返すGAS

Googleスプレッドシートでは関数名をdoGet()にすることでGASをデプロイしたときに公開されたWeb URLをエンドポイントとして簡易的なHTTPサーバーを建てることができます.

https://developers.google.com/apps-script/guides/web?hl=ja

今回はシンプルにアクセス時に、特定のGoogleスプレッドシートの中身を1行目を除いて(ヘッダーとして参加者向けにメタタグを書いています)返すような仕組みを作りました.

実際のGoogleスプレッドシートの様子

スプレッドシートから1行目を除いて全て取得する

以下のコードで取得できます. 特に変わったこともしておらず、スプレッドシートをopenByIdで取得し、その中から該当するSheetをシート名で取り出しています.

その後に、sheet.getRange()で2行目の1列目〜最後の行数-1行の最後の列までを範囲選択して取得します.
こうすると文字列や数字の入った2次元配列が取得できます.

  // 対象のスプレッドシートIDを指定
  var spreadsheetId = "xxxxxxxxxxxxxxx"; // スプレッドシートのID
  var sheetName = "main"; // シート名

  // スプレッドシートを取得
  var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);

  // 2行目以降のデータを取得
  var dataRange = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
  var values = dataRange.getValues();

2次元配列の中身は各列を1次元配列にしたものを行で2次元配列にした以下のようになっていてヘッダーなどのメタ情報は特に含まれていません. 次にこれを扱いやすく変換します.

[
 [0,test,test,test],
 [1,test,test,test]
]

変換自体もかなりシンプルで、配列から各行のデータを取り出し対応する列のkeyにvalueを設定するといったものです.
```javascript
  var response = {
    status: "success",
    data: values.map((v) => {
      return {
        id: v[0], 
        name: v[1],
        profileUrl: v[2],
        blogTitle: v[3],
        blogUrl: v[4]
      }
    })
  };

あとはこの値をJSON文字列として返したコードが以下になります.

main.gs
function doGet(e) {
  // 対象のスプレッドシートIDを指定
  var spreadsheetId = "xxxxxxxxxxxxxxx"; // スプレッドシートのID
  var sheetName = "main"; // シート名

  // スプレッドシートを取得
  var sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName(sheetName);

  // 2行目以降のデータを取得
  var dataRange = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
  var values = dataRange.getValues();

  // JSON形式でレスポンスを返す
  var response = {
    data: values.map((v) => {
      return {
        id: v[0], 
        name: v[1],
        profileUrl: v[2],
        blogTitle: v[3],
        blogUrl: v[4]
      }
    })
  };

  return ContentService.createTextOutput(JSON.stringify(response))
    .setMimeType(ContentService.MimeType.JSON);
}

これで、簡易的なCMSの完成です. Googleスプレッドシートを操作すればGASのデプロイされたURLにアクセスするだけでJSONが返ってきます.

実際のエンドポイント

Svelte-Kitは動的に外部データをとるときにかなり便利

今回LPを作るにあたって余計なことをしたくなかったので、フロントエンドメタフレームワークを使うことにしました. 色々選択肢はあるものの、今回の要件としては単に外部のURLをfetchして結果のJSONをいい感じに表示したいだけ(複雑な状態管理をしなくて良い)という観点からSvelte-Kitを選んでいます.

Svelte-Kitのloadを使い, promiseでloading処理を挟む

Svelte-Kitには(同様にRemixなどにも近い機構がありますが)データローディングのための仕組みとしてload関数があります.

これは、以下のようにload関数を利用して外部のデータを取得したり、計算を事前に行いその結果をView側に伝える仕組みを持ちます.

利点として非同期処理を分離できること、データ取得と表示の責務を分離し単方向のデータフローを作れることが挙げられます.

src/routes/blog/[slug]/+page.js
/** @type {import('./$types').PageLoad} */
export function load({ params }) {

	return {
		post: {
			title: `Title for ${params.slug} goes here`,
			content: `Content for ${params.slug} goes here`
		}
	};
}
src/routes/blog/[slug]/+page.svelte
<script>
	/** @type {import('./$types').PageData} */
	export let data;
</script>

<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>

https://kit.svelte.jp/docs/load

そしてこのload関数はPromiseをViewのコンポーネント側に渡すことができます.
そしてこのPromiseawaitブロックを利用することでデータ取得に時間がかかる場合のローディング表現が可能になります.

{#await promise}
	<!-- promise is pending -->
	<p>waiting for the promise to resolve...</p>
{:then value}
	<!-- promise was fulfilled or not a Promise -->
	<p>The value is {value}</p>
{:catch error}
	<!-- promise was rejected -->
	<p>Something went wrong: {error.message}</p>
{/await}

https://svelte.dev/docs/svelte/await

これらを組み合わせて今回のサイトでは、GoogleスプレッドシートのデータをGAS経由で取得し描画するという処理をSvelte-Kitを使いシンプルに表現できるようにしたわけです.

実際のコードの一部を抜粋します.

+page.svelte
...
{#await data.blogData}
	<p>loading...</p>
{:then blogRes}
<ul>
    {#each blogRes.data as blog}
    <li>
        <span>No.{blog.id} </span>
        <font color="blue">
            <a class="underline" href={blog.blogUrl} target="_blank" rel="noopener noreferrer">
                {blog.blogTitle}
            </a>
        </font>
        <span>by</span>
        <font color="blue">
            <a class="underline" href={blog.profileUrl} target="_blank" rel="noopener noreferrer">
            {blog.name}
            </a>
        </font>
    </li>
    {/each}
    </ul>
{/await}
...

GitHub PagesにSvelte-Kitをデプロイする

ビルドした結果をGitHub Actionsのpeaceiris/actions-gh-pages@v3経由でデプロイします.
GitHubリポジトリ側の設定でブランチをgh-pagesに設定したり、GitHubのAPI_TOKENを環境変数に渡したりすれば完成です.

prod.yml
name: deploy-to-github-pages

on:
  push:
    branches: [ "main" ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      
      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: setup node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      # package.jsonの内容でパッケージインストール
      - name: setup app env
        run: pnpm install
      
      # ビルド実行、静的リソースを作る
      - name: build static resources
        run: npm run build
          
      # GitHub Pagesにデプロイする
      - name: deploy to github pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GH_API_TOKEN }}
          # アプリ直下のbuildディレクトリに生成されるため、指定
          publish_dir: build

サイトURL

というわけでぜひ一度お目通しいただけると嬉しいです.

https://forceoutput.github.io/

Discussion