2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

古の個人"ほぉむぺぇじ"をMarkdown AIで作ってみる

Last updated at Posted at 2024-12-21

この記事は、Markdown AIのサーバーAI機能を使ってWebサイトを作ってみよう by MarkdownAI Advent Calendar 2024の参加記事です。

タイトルの通り、2000年前後に存在した、個人ホームページをMarkdown AIで作ってみたというものです。以下が実物です。

作りかけの中途半端な雰囲気がいかにもそれっぽくないですか! 投稿までに間に合わなかったいいわけではない(爆)

Markdown AIはHTMLタグも使える

文字が右から左に流れる懐かしのマーキーは、まんま、marqueeタグで動いています。Markdown AIでサポートされているとは思わなかったので驚きました。そして嬉しかったです。spanタグも使えるので、そこでスタイルを指定すれば、色を付けたりも自由自在です。マーキー部分は以下の様なソースになっています。

<marquee><span style="color:red;">ここは電脳天使が運営する、まったり&のんびりなホームページです。ゆっくりしていってね!</span></marquee>

Markdown AIはheadタグ内にstyleも書ける

動いたので嬉しいです。ここで、壁紙をbodyに対して設定したり、文字色を変えたりしています。背景画像ではなく壁紙と言うのがレトロスタイルです!

Markdown AIはscriptタグも使える

Markdown AIはその名の通り、gpt-4o-miniなどのAIを呼び出すことができるのがウリです。呼び出すにはscriptタグを使うので、つまりは、Javascriptでできることは大抵出来てしまいます。

そこで、マウスカーソルを★に変える遊びを入れてみました。これでますますレトロな雰囲気に!

scriptタグが使えるということは、外部との通信も出来る

今回、アクセスカウンターを設置しています。これは実装としてはちょっと大げさで、

  • Markdown AIで作ったページから、Cloudflare workersで作ったエンドポイントを叩く
  • Cloudflare wordersでKVにアクセス数を保存するデータベースにアクセス数を保存する
  • そのアクセス数を返して、カウンターとして表示する

ということをやっています。リロードする度に数字が増えるレトロ仕様です。

image.png

「占い」部分はちゃんとAIを使った!

昔のホームページには本日の運勢を表示していたりしました。毎日違う占いが表示されるCGIとか使っていました。せっかくなので、Markdown AIの機能を使って、AIで占いを生成してもらっています。

image.png

↑ここの「占う」のボタンを押すと占いが表示されます。表示には30秒くらい待ちましょう。

image.png

こんな感じで、古のホームページを作ってみました。楽しいです!

Markdown AIの真面目な使い方

今回は企画ものということでネタに走ってしまいましたが、本来的な使い方としては

  • 学園祭の出し物の案内のように、必要とされる期間が短く対象も限定的だがあると便利なページを作る
  • 家庭教師など、個人で請け負う小規模な仕事の告知ページを作って、チラシのQRコードから飛べるように設定する

といった使い方だとすごく便利だと思います。同様のサービスとしては、ペライチやWixなどがありますが、それらに比べると、Markdownでちょろっと書くだけ、というのはすごく便利です。

最近はWordファイルをMarkdownに変換することもできますし、Wordで作って変換してペタッと貼るだけで、上記のようなページを作れるのは便利なはず。そこにAIでちょっとした遊び心を入れられるのは楽しいですよね。

ソース

一応、ソースを貼っておきます。

Markdown部分

壁紙画像はCloudflare R2に置き、アクセスカウンターのエンドポイントはCloudflare Workersで立てています。


<head>
    <meta charset="UTF-8">
    <style>
        html, body {
            height: auto;
            min-height: 100%;
        }
        body {
            margin: 0;
            padding: 0;
            background-image: url('https://■■■■■■■■■■.r2.dev/b071.jpg'); //画像はCloudflare R2に置いています
            background-attachment: fixed;
            background-repeat: repeat;
            background-color: #000040;
            color: #000000;
            font-family: "MS PGothic", sans-serif;
            cursor: none;
            min-height: 100%;
        }

        #retro-container {
            padding: 20px;
            text-align: center;
            background-color: rgba(0, 0, 0, 0.5);
            min-height: 100vh;
        }

        #content-wrapper {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: rgba(255, 255, 255, 0.9);
            border-radius: 10px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
        }

        #visitorCount {
            font-family: monospace;
            font-size: 24px;
            padding: 10px;
            border: 2px solid #FFD700;
            display: inline-block;
            background-color: rgba(0, 0, 0, 0.7);
            color: #FFD700;
        }

        #debug {
            display: none;
            font-family: monospace;
            white-space: pre-line;
            margin: 20px;
            padding: 10px;
            border: 2px solid #FFD700;
            background: rgba(0, 0, 0, 0.8);
            color: #FFFFFF;
            position: fixed;
            bottom: 10px;
            right: 10px;
            max-width: 400px;
            max-height: 300px;
            overflow: auto;
            z-index: 9999;
            font-size: 12px;
            border-radius: 5px;
            box-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
        }

        #custom-cursor {
            position: fixed;
            pointer-events: none;
            z-index: 10000;
            font-size: 24px;
            color: #FFD700;
            text-shadow: 0 0 5px #FFD700;
        }

        /* Markdown content styles */
        h1, h2, h3 {
            text-shadow: 2px 2px 4px #000;
        }

        ul {
            list-style-type: none;
            padding-left: 20px;
        }

        img {
            margin: 10px;
            border: 2px solid #FFD700;
            border-radius: 5px;
        }

        marquee {
            background-color: rgba(0, 0, 0, 0.5);
            border-top: 2px solid #FFD700;
            border-bottom: 2px solid #FFD700;
            padding: 5px 0;
            margin: 20px 0;
        }
    </style>
</head>
<body>
    <div id="retro-container">
        <!-- デバッグパネル -->
        <div id="debug">
            Debug Log:
            <button onclick="toggleDebug()" style="position: absolute; top: 5px; right: 5px; cursor: pointer;">×</button>
        </div>

        <!-- カスタムカーソル -->
        <div id="custom-cursor">★</div>

        <!-- メインコンテンツ -->
        <div id="content-wrapper">
# <span style="color:blue;">ようこそ!★★★電脳天使の秘密基地へ★★★</span>

<marquee><span style="color:red;">ここは電脳天使が運営する、まったり&のんびりなホームページです。ゆっくりしていってね!</span></marquee>

## 占い


<div style="display: inline-block;">
  <input type="text" id="text-■■■■■■" style="width: 200px;" value="今日の運勢を教えちゃいます!">
  <button type="button" id="button-■■■■■■">占う</button>
</div>
<div id="answer-■■■■■■"></div>

<script>
(() => {
  const button = document.getElementById('button-■■■■■■');
  button.addEventListener('click', async event => {
    button.disabled = true;
    
    const serverAi = new ServerAI();
    const message = document.getElementById('text-■■■■■■').value;
    const answer = await serverAi.getAnswerText('■■■■■■2', '', message);
    
    document.getElementById('answer-■■■■■■').innerText = answer;
    button.disabled = false;
  });
})();
</script>

## <span style="color:green;">更新情報</span>

-   **2000/05/05** 日記を更新しました。「ゴールデンウィークはゲーム三昧!」
-   **2000/04/28** キリ番報告を更新しました。1000HIT達成!
-   **2000/04/20** 趣味のページに「ガンプラコレクション」を追加しました。
-   **2000/04/15** 自己紹介を更新しました。
-   **2000/04/01** 電脳天使の秘密基地、開設しました!

## <span style="color:purple;">コンテンツ</span>

*   自己紹介- 電脳天使ってどんな人?
*   日記 - 毎日更新(するつもり)!
*   趣味のページ - ガンプラ、アニメ、ゲームなどなど
*   キリ番報告 - キリ番ゲットしたら報告してね!
*   リンク集 - お友達のサイトを紹介!
*   BBS - みんなで仲良くおしゃべりしよう!

<br>

## あくせすかうんたぁ

あなたは
<div id="visitorCount">Loading...</div>
人目の訪問者です☆彡

## <span style="color:orange;">キリ番報告</span>

祝!**10HIT**達成!<br>
1000HITを踏んだのは、**ハンドルネーム:勇者**さんです!おめでとうございます!<br>
BBSに記念カキコしてくれた方に、ささやかなプレゼントを差し上げます!

次は**100HIT**を目指します!

<br>

## <span style="color:pink;">日記</span>

### 2000/12/23 クリスマスはゲーム三昧!

もう今年も終わりかぁ。早いなぁ。<br>
ずっと家でゲームしてた!<br>
寝る間も惜しんでプレイしちゃったよ。<br>
そんな漏れもクリスマスは空いているらしいよ<br>
あー、お誘いとかないかなぁ(チラッ)。

<br>

---

<br>

<span style="color:gray;">Copyright (C) 2000 電脳天使. All Rights Reserved.</span>
        </div>
    </div>

    <script>
        // デバッグ関連の機能
        function debugLog(message) {
            console.log(message);
            const debugDiv = document.getElementById('debug');
            const timestamp = new Date().toLocaleTimeString();
            debugDiv.textContent += `\n[${timestamp}] ${message}`;
            debugDiv.scrollTop = debugDiv.scrollHeight;
        }

        function debugError(message, error) {
            console.error(message, error);
            const debugDiv = document.getElementById('debug');
            const timestamp = new Date().toLocaleTimeString();
            debugDiv.textContent += `\n[${timestamp}] [ERROR] ${message}`;
            if (error) {
                debugDiv.textContent += '\n' + error.toString();
            }
            debugDiv.style.display = 'block';
            debugDiv.scrollTop = debugDiv.scrollHeight;
        }

        function toggleDebug() {
            const debugDiv = document.getElementById('debug');
            debugDiv.style.display = debugDiv.style.display === 'none' ? 'block' : 'none';
        }

        // カスタムカーソルの制御
        document.addEventListener('mousemove', (e) => {
            const cursor = document.getElementById('custom-cursor');
            cursor.style.left = `${e.clientX}px`;
            cursor.style.top = `${e.clientY}px`;
            cursor.style.transform = 'translate(-50%, -50%)';
        });

        // デバッグパネルのショートカットキー
        document.addEventListener('keydown', function(e) {
            if (e.ctrlKey && e.shiftKey && e.key === 'D') {
                e.preventDefault();
                toggleDebug();
            }
        });

        // カウンター更新機能
        async function updateVisitorCount() {
            debugLog('Updating visitor count...');
            
            try {
                const response = await fetch('https://■■■■■■■■■■.workers.dev/increment', {
                    method: 'POST'
                });
                
                debugLog('Response status: ' + response.status);
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                const data = await response.json();
                debugLog('Received count: ' + data.count);

                const countDisplay = data.count.toString().padStart(6, '0');
                document.getElementById('visitorCount').textContent = countDisplay;
                debugLog('Counter updated successfully');

            } catch (error) {
                debugError('Error updating visitor count', error);
                document.getElementById('visitorCount').textContent = 'Error';
                
                if (error instanceof TypeError) {
                    debugError('Network error - Could not connect to Worker. Please check:' +
                        '\n1. Worker URL is correct' +
                        '\n2. Worker is deployed and running' +
                        '\n3. CORS is properly configured');
                }
            }
        }

        // 初期化
        document.addEventListener('DOMContentLoaded', () => {
            debugLog('Page loaded');
            debugLog('Starting counter update...');
            updateVisitorCount();
        });
    </script>
</body>

Cloudflare Workers部分

データベースであるKVはVISITORSという名前空間で作成し、それをバインドしています。今回はすぐに消すネタなので、'Access-Control-Allow-Origin': '*', のガバガバセキュリティでMarkdown AIからの接続を許可しています(イタズラしないでね!)

image.png

image.png

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // CORSヘッダーを設定
  const headers = {
    'Access-Control-Allow-Origin': '*',
    'Content-Type': 'application/json',
    'Access-Control-Allow-Methods': 'POST',
  }

  // OPTIONSリクエストへの対応
  if (request.method === 'OPTIONS') {
    return new Response(null, { headers })
  }

  // URLからパスを取得
  const url = new URL(request.url)
  const path = url.pathname

  // /increment パスのみを処理
  if (path === '/increment' && request.method === 'POST') {
    try {
      // KVから現在のカウント値を取得
      let count = await VISITORS.get('count')
      count = count ? parseInt(count) + 1 : 1

      // 新しい値をKVに保存
      await VISITORS.put('count', count.toString())

      // JSONレスポンスを返す
      return new Response(
        JSON.stringify({ count }),
        { headers }
      )
    } catch (err) {
      return new Response(
        JSON.stringify({ error: 'Failed to update counter' }), 
        { 
          status: 500,
          headers 
        }
      )
    }
  }

  // 不正なパスまたはメソッドの場合
  return new Response('Not found', { 
    status: 404,
    headers 
  })
}

というわけで、Markdown AIで遊んでみたという記事でした。みなさんのクオリティオブライフ(爆)が増進しますように!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?