この記事は、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にアクセス数を保存するデータベースにアクセス数を保存する
- そのアクセス数を返して、カウンターとして表示する
ということをやっています。リロードする度に数字が増えるレトロ仕様です。
「占い」部分はちゃんとAIを使った!
昔のホームページには本日の運勢を表示していたりしました。毎日違う占いが表示されるCGIとか使っていました。せっかくなので、Markdown AIの機能を使って、AIで占いを生成してもらっています。
↑ここの「占う」のボタンを押すと占いが表示されます。表示には30秒くらい待ちましょう。
こんな感じで、古のホームページを作ってみました。楽しいです!
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からの接続を許可しています(イタズラしないでね!)
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で遊んでみたという記事でした。みなさんのクオリティオブライフ(爆)が増進しますように!