ブロックチェーンSymbolに静的ページを分散型ホスティングする

# はじめに
アドカレ何か書くことあるかなー、今年は何事もなく終えるかなー、と考えていたところにXでのある投稿が目に入りました

それは

> Symbolで静的ファイルのWebページをホスティングしたりできるのかな

というものでしたが、これはいいネタだと思いアドカレ執筆を始めます。

SymbolなんですがMetalというプロトコルを使います。Metalに関しては以下を読んでみてください。面白いです。
手数料さえ払えばどんな大きいサイズのファイルでも理論上は可能です。

https://github.com/OPENSPHERE-Inc/metal-on-symbol

静的ページはなんでもいいんですが、ちょっとそれっぽくVite + Reactでシンプルなページ遷移ができるモノにします。なお僕はReactの知識はあんまりありません。ここは本題じゃないのでなんでもいいです。

ポイントは1枚のファイルにすることです。（デコードを自分でするなら別に1枚じゃなくてもいいけど）今回はViteで生成したWebサイトのbundleファイルをhtmlに全部直接書き込むという方法を取ります。

# 静的ページを作成
ここは本題じゃないのでささっと

```
npm create vite@latest static-page --template react
// 今回は tsx にした
cd static-page
static-page install
```

作成したら

``` vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'bundle.js',
        chunkFileNames: 'bundle.js',
        assetFileNames: 'assets/[name].[ext]',
      },
    },
  },
})
```

``` /src/App.tsx
import React, { useState } from 'react';

type Page = 'page1' | 'page2';

const App: React.FC = () => {
  const [currentPage, setCurrentPage] = useState<Page>('page1');

  const handleTabClick = (page: Page) => {
    setCurrentPage(page);
  };

  return (
    <div>
      <div style={{ display: 'flex', marginBottom: '20px' }}>
        <button onClick={() => handleTabClick('page1')}>Page 1</button>
        <button onClick={() => handleTabClick('page2')}>Page 2</button>
      </div>

      {currentPage === 'page1' && (
        <div>
          <h1>Page 1</h1>
          <p>This is Page 1 content.</p>
        </div>
      )}
      {currentPage === 'page2' && (
        <div>
          <h1>Page 2</h1>
          <p>This is Page 2 content.</p>
        </div>
      )}
    </div>
  );
}

export default App;
```

最下部にスタイル追加
``` /src/index.css
.app-container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
  text-align: center;
}

#root {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  width: 100%;
}

.tabs {
  display: flex;
  margin-bottom: 20px;
}

.tabs button {
  margin: 0 10px;
  padding: 10px 20px;
  font-size: 16px;
  cursor: pointer;
}

.content {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  width: 100%;
}
```

これだけです。
ビルドします

```
npm run build
```

indexファイルのjsとcss読み込み箇所に直接書き込む

``` /dist/index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>    
    <style>
      /* ここにassets/index.cssをコピペする */
    </style>
  </head>
  <body>
    <div id="root"></div>
    <script>
      // ここにbundle.jsをコピペする
    </script>
  </body>
</html>
```

これだけです。もし画像などを使用したい場合には

1. base64にしてファイルに直接書き込む
1. 画像をmetalでオンチェーンにする

という選択肢があるかなーと思いますが今回は使ってません

# Metalを使ってチェーンに刻む

metal cliのインストール
```
npm install -g metal-on-symbol
```

NODE情報をセットする

https://github.com/OPENSPHERE-Inc/metal-on-symbol?tab=readme-ov-file#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%A7%E3%83%8E%E3%83%BC%E3%83%89%E3%82%92%E8%A8%AD%E5%AE%9A

僕はMACなので
```
export NODE_URL=https://example.jp:3001
```

ちなみにNODEは
メインネットなら
https://symbolnodes.org/nodes/
テストネットなら
https://symbolnodes.org/nodes_testnet/
ここからAPIノードを選ぶ

さっそくForgeする

<b>viteで作成したアプリケーションのrootで作業する場合</b>
```
metal forge ./dist/index.html --seal 2
```
`--seal 2`というオプションを必ずつけます
これによりmimeTypeとファイルサイズが付加情報としてオンチェーンになります

秘密鍵を聞かれるので貼り付けてEnter

アナウンスするか聞かれるのでYESを選択し承認を待ちます。
承認されるとこんな感じで結果が表示されます。

![スクリーンショット 2024-12-17 17.18.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80081/4e4b19df-3d6e-d06d-9a8e-e32c63c7158c.png)

METAL IDを控えてノードに以下のようなエンドポイントでアクセスすると

https://2.dusanjp.com:3001/metadata/metal/FeERXDZeHMxjXYU5FmjjJdmK2oMRXtDg5yNP7Ms4KE924y

そのまま作成したWEBサイトが表示されます

# 注意点
Forgeするときはどのノードにアナウンスしてもいいんですが、REST経由で表示できるのは特定のノードのみです。
ノード側にちょっとした設定が必要で以下のノードの運営者さんが協力してくれました。
※なお、このリストは2024/12/17日時点で表示が確認できていますが、ノードのアップデートなどがあれば表示されない可能性があります。

- MainNet
https://pasomi.net:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://vmi1560137.contaboserver.net:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://symbol-mikun.net:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://finnel.harvestasya.com:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://symbol.harvesting-sweet-potatoes.club:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://00.symbol-node.com:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://01.symbol-node.com:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://02.symbol-node.com:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://03.symbol-node.com:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://villhell-symbol-mainnet.net:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN
https://node.sixis.xyz:3001/metadata/metal/Fe6p1K4dmVekyBqbqrNqUhNc4X37sFsA4NMGAPd1cbjtdN

- TestNet
https://2.dusanjp.com:3001/metadata/metal/Fe3kBsPpMgDstJq3uALpYtUXm6pfuyx1y8tNs1MYohYX2d
https://sakia.harvestasya.com:3001/metadata/metal/FeERXDZeHMxjXYU5FmjjJdmK2oMRXtDg5yNP7Ms4KE924y

REST経由で直接表示できなくともデコードはSDKを使用すれば可能です
https://github.com/OPENSPHERE-Inc/metal-on-symbol?tab=readme-ov-file#6-sdk-typescript--ecmascript

# さいごに
ざくっと書いたので言葉足らずな点もあるかもしれませんが、ひとまず以上で記事を終えます。
ちなみにbundle.jsやcssファイル単体をmetalでforgeしてそれをhtmlファイルで読み込むという方法でも良いと思います

分散型ホスティング面白いですね

追記。ちなみに静的じゃなくて動的でもMetalを使ってわりと簡単に実現できると思います