Markdown AIのサーバーAI機能を使ってWebサイトを作ってみよう by MarkdownAI Advent Calendar 2024の12月20日分の企画に滑り込みます。
Markdown AI
で架空の映画をテーマにしたWebサイトを作ってみました。
内容はAIGeminiやImageFxをもりもりに使っています。
- Google Apps Scriptでホストしたタイピングゲーム
- Looker Studioによるダッシュボード
上記も使ってみました。
Markdown AIとは
その名の通りMarkdown
を書いて、手軽にWebサイトを構築できるツールです。
HTML
やCSS
などの知識がなくても、Markdown
で書いたテキストをWebサイトとして公開できます。
サンプルアプリが充実していますので、ざっとどんなものが作れるのかは調べられます。
アレンジの詳細は下記を参考にしてみてください。
項目 | 説明 |
---|---|
作成できるWebページ数 | 最大500ページ |
1ページあたりの入力可能文字数 | 半角英語で約100万文字 |
挿入可能な画像枚数 | 500枚 |
画像の最大サイズ | 7.5MB |
拡張Markdown | テーブル、チェックボックス ※GitHubで使える拡張機能やiframe にも対応しているようです |
作成できるWebページを最大500ページまで、各ページには、半角英語で約100万文字まで入力可能です。
Q:HTML形式で記述すれば反映されますか? A:反映されます。
上記がありだとなんでもできそうです。ということでLet's challenge!!
アレンジマシマシでWebサイトを作ってみましょう!
Markdown AIで作ってみたWebサイト
Markdown AIを使って、映画 「OTTER FORCE: The Last Water Guardian」 の架空の広告サイトを作成してみました。
画像はImageFxをもりもりに使っています。
下記の通り架空の情報です。
登場するキャラクター、出来事、団体は、すべてOTTER FORCEの世界観内での創作です。 実在の人物や出来事、団体との類似は完全に偶然であり、意図したものではありません。
Markdown
だけではなくHTML
をふんだんに使ったサイトです。
今回のテーマはServer AIの活用
のため、チャットボットを入れてみました。
数回のクリックでServer AI(チャットボット)を挿入できます。
ページの情報に基づいた回答が返ってきます。
"もちろん!OTTER FORCE: The Last Water Guardianの特長は、冒険と友情、そして環境保護のメッセージが詰まった素晴らしいストーリーです。映画は2157年の未来を舞台に、主人公のリップルが古代のライトセーバーを発見し、邪悪なハイドラテックと戦いながら地球の水のバランスを取り戻す旅を描いています。また、映画のビジュアルは美しい水中シーンや未来的なテクノロジーで彩られ、観客を魅了します。リップルの成長や仲間たちとの絆も感動的で、観る人に勇気を与えてくれます。もっとリップルの冒険や仲間たちについて知りたいですか?"
返答例
内容については、ぜひサイトで試してみてください。
Server AIの使い方
-
Markdown AI
のロボット🤖のマークをクリックします
-
Create Model
の画面が出るため、モデルの名前
、AIモデル
の設定、Prompt
を指定、必要に応じてKnowledgeを指定
- 選択可能なAIモデル
- gpt-4o-mini
- gpt-3.5-turbo
- gpt-4
- claude
- gemini
- dallL-e-3
- Knowledge
- 作成した
Markdown
のページ、もといfile
、link
を指定
- 作成した
- 選択可能なAIモデル
私はgpt-4o-mini
を指定しています。
Knowledgeは表示されるWebページのMarkdownです。
Server AI
の詳細は下記のようにまとめられます。
項目 | 機能 |
---|---|
最大応答 | 1回の返信で出力可能な最大文字数。2048トークン(英語で約4000〜5000文字)まで対応 |
プロンプト入力制限 | 約100万文字 |
複数モデルの生成 | リスト未指定時は「モデル名」入力で作成可 |
モデル作成上限 | 最大500個 |
Webサイトへの埋め込み方法 | サイト作成画面の「挿入」→「スクリプト」から AI モデルを選択し、テキストフォームと回答ボタンを配置 |
Knowledge機能 | file、link |
Prompt機能 | AIへの指示設定機能、システムプロンプトの立ち位置 |
Playground機能 | AIの動作確認・応答テスト用の検証 |
前置きが長くなりましたが作成したページの内容に移ります。
Google Apps Script Typing Shooting Game
Typing Shooting Game
を作成して参照しています。
当初Markdown AI
でホストするつもりでしたが、Google Apps Script
でいいのでは🧐となり、書き換えました。
Google Apps Script
で作成しています。
一回でも外したら終了の残念な仕様です。
Google Apps Script
の機能を利用して、Google スプレッドシート
にゲーム🎮の結果を記録しています。
その内容がiframe
のLooker Studio
に表示されます。
コスパが非常にいいプランですね。
Google Apps Scriptは、スプレッドシートにバインドされたスクリプトです。
がばがば条件ですが、サクッと設定できます。
#Google Apps Script
doget
でHTMLを表示しています。Google Apps Scriptの利点はsaveScore
で発揮されます。
function doGet() {
return HtmlService.createHtmlOutputFromFile('index.html')
.setTitle('Shooting Game')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function saveScore(username, score) {
try {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Scores');
const timestamp = new Date();
const rowData = [timestamp, username, score];
sheet.appendRow(rowData);
return { success: true, message: 'Score submitted successfully!' };
} catch (error) {
Logger.log('Error saving score: ' + error.toString());
return { success: false, message: 'Failed to submit score. Please try again.' };
}
}
Claude
様様の内容です。
<!DOCTYPE html>
<html>
<head>
<style>
body {
background: #1a1a1a;
color: #fff;
font-family: Arial, sans-serif;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 20px;
}
#game-container {
position: relative;
width: 800px;
margin: 20px auto;
}
canvas {
border: 2px solid #33ccff;
background: #000033;
}
#word-input {
width: 300px;
padding: 10px;
font-size: 18px;
margin: 10px 0;
background: #000033;
color: #33ccff;
border: 2px solid #33ccff;
border-radius: 5px;
text-align: center;
}
#current-word {
font-size: 24px;
color: #33ccff;
margin: 10px 0;
min-height: 36px;
}
#score {
font-size: 20px;
color: #33ccff;
margin: 10px 0;
}
#game-over {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
padding: 20px;
border-radius: 10px;
text-align: center;
}
button {
padding: 10px 20px;
font-size: 18px;
background: #33ccff;
color: #000033;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 10px;
}
button:hover {
background: #66ddff;
}
/* 新規追加のスタイル */
#username-input {
width: 200px;
padding: 10px;
font-size: 16px;
margin: 10px 0;
background: #000033;
color: #33ccff;
border: 2px solid #33ccff;
border-radius: 5px;
}
#submit-score {
background: #33ff33;
}
#submit-score:hover {
background: #66ff66;
}
.score-message {
color: #33ff33;
margin: 10px 0;
font-size: 16px;
}
</style>
</head>
<body>
<div id="game-container">
<div id="score">Score: 0</div>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div id="current-word"></div>
<input type="text" id="word-input" placeholder="Type the word..." autocomplete="off">
<div id="game-over">
<h2>Game Over!</h2>
<p id="final-score"></p>
<input type="text" id="username-input" placeholder="Enter your name" maxlength="20">
<button id="submit-score">Submit Score</button>
<p id="submit-message" class="score-message"></p>
<button onclick="startGame()">Play Again</button>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const wordInput = document.getElementById('word-input');
const currentWordDisplay = document.getElementById('current-word');
const scoreDisplay = document.getElementById('score');
const gameOverScreen = document.getElementById('game-over');
const usernameInput = document.getElementById('username-input');
const submitScoreButton = document.getElementById('submit-score');
const submitMessage = document.getElementById('submit-message');
const words = [
'splash', 'ripple', 'water', 'crystal', 'force', 'otter',
'aqua', 'wave', 'flow', 'swim', 'dive', 'fish',
'river', 'ocean', 'stream', 'pool', 'lake', 'sea'
];
const OTTER_SIZE = 50;
const ENEMY_SIZE = 40;
const LASER_SPEED = 7;
const BASE_ENEMY_SPEED = 0.5;
const ENEMY_SPAWN_RATE = 0.01;
const DIFFICULTY_INCREASE_RATE = 0.0001;
const MAX_ENEMIES = 4;
let currentEnemySpeed = BASE_ENEMY_SPEED;
let gameLoop;
let score = 0;
let currentWord = '';
let currentEnemy = null;
let enemies = [];
let lasers = [];
let otterX = canvas.width / 2;
let gameActive = false;
class Enemy {
constructor(word) {
this.x = Math.random() * (canvas.width - ENEMY_SIZE);
this.y = 0;
this.word = word;
this.targeted = false;
this.active = false;
}
draw() {
ctx.fillStyle = this.targeted ? '#ff3333' :
this.active ? '#33ff33' : '#33ccff';
ctx.fillRect(this.x, this.y, ENEMY_SIZE, ENEMY_SIZE);
ctx.fillStyle = this.active ? '#ffff00' : '#ffffff';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText(this.word, this.x + ENEMY_SIZE/2, this.y + ENEMY_SIZE + 20);
}
update() {
this.y += currentEnemySpeed;
return this.y > canvas.height;
}
}
class Laser {
constructor(startX, startY, targetX, targetY) {
this.x = startX;
this.y = startY;
const angle = Math.atan2(targetY - startY, targetX - startX);
this.dx = Math.cos(angle) * LASER_SPEED;
this.dy = Math.sin(angle) * LASER_SPEED;
this.target = null;
}
draw() {
ctx.strokeStyle = '#33ff33';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(this.x, this.y);
ctx.lineTo(this.x - this.dx * 2, this.y - this.dy * 2);
ctx.stroke();
}
update() {
this.x += this.dx;
this.y += this.dy;
return this.x < 0 || this.x > canvas.width ||
this.y < 0 || this.y > canvas.height;
}
}
function drawOtter(x) {
ctx.fillStyle = '#996633';
ctx.fillRect(x - OTTER_SIZE/2, canvas.height - OTTER_SIZE, OTTER_SIZE, OTTER_SIZE);
}
function spawnEnemy() {
if (Math.random() < ENEMY_SPAWN_RATE && enemies.length < MAX_ENEMIES) {
const word = words[Math.floor(Math.random() * words.length)];
const newEnemy = new Enemy(word);
while (enemies.some(enemy =>
Math.abs(enemy.x - newEnemy.x) < ENEMY_SIZE * 1.5)) {
newEnemy.x = Math.random() * (canvas.width - ENEMY_SIZE);
}
enemies.push(newEnemy);
}
}
function checkCollisions() {
for (let laser of lasers) {
currentEnemySpeed += DIFFICULTY_INCREASE_RATE;
if (laser.target) {
const enemy = enemies.find(e => e === laser.target);
if (enemy) {
const dx = laser.x - (enemy.x + ENEMY_SIZE/2);
const dy = laser.y - (enemy.y + ENEMY_SIZE/2);
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < ENEMY_SIZE/2) {
enemies = enemies.filter(e => e !== enemy);
lasers = lasers.filter(l => l !== laser);
score += 100;
scoreDisplay.textContent = `Score: ${score}`;
currentWord = '';
currentWordDisplay.textContent = '';
wordInput.value = '';
return;
}
}
}
}
}
async function submitScore(username, score) {
try {
const result = await google.script.run
.withSuccessHandler(function(response) {
if (response.success) {
submitMessage.textContent = response.message;
submitMessage.style.color = '#33ff33';
submitScoreButton.disabled = true;
} else {
throw new Error(response.message);
}
})
.withFailureHandler(function(error) {
submitMessage.textContent = 'Failed to submit score. Please try again.';
submitMessage.style.color = '#ff3333';
})
.saveScore(username, score);
} catch (error) {
submitMessage.textContent = 'Failed to submit score. Please try again.';
submitMessage.style.color = '#ff3333';
}
}
function gameOver() {
gameActive = false;
clearInterval(gameLoop);
document.getElementById('final-score').textContent = `Final Score: ${score}`;
gameOverScreen.style.display = 'block';
usernameInput.value = '';
submitMessage.textContent = '';
submitScoreButton.disabled = false;
}
function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
spawnEnemy();
drawOtter(otterX);
enemies = enemies.filter(enemy => {
enemy.draw();
if (enemy.update()) {
gameOver();
return false;
}
return true;
});
lasers = lasers.filter(laser => {
laser.draw();
return !laser.update();
});
checkCollisions();
}
function startGame() {
gameActive = true;
score = 0;
enemies = [];
lasers = [];
currentWord = '';
currentWordDisplay.textContent = '';
wordInput.value = '';
scoreDisplay.textContent = `Score: ${score}`;
gameOverScreen.style.display = 'none';
wordInput.focus();
if (gameLoop) clearInterval(gameLoop);
gameLoop = setInterval(update, 1000/60);
}
wordInput.addEventListener('input', (e) => {
if (!gameActive) return;
const typed = e.target.value.toLowerCase();
if (!currentWord) {
const availableEnemies = enemies.filter(e => !e.targeted);
if (availableEnemies.length > 0) {
for (let enemy of availableEnemies) {
if (enemy.word.toLowerCase().startsWith(typed)) {
currentWord = enemy.word.toLowerCase();
enemy.targeted = true;
currentWordDisplay.textContent = currentWord;
currentWordDisplay.style.color = '#33ff33';
break;
}
}
}
}
if (currentWord && typed === currentWord) {
const targetEnemy = enemies.find(e =>
e.word.toLowerCase() === currentWord && e.targeted);
if (targetEnemy) {
const laser = new Laser(
otterX,
canvas.height - OTTER_SIZE,
targetEnemy.x + ENEMY_SIZE/2,
targetEnemy.y + ENEMY_SIZE/2
);
laser.target = targetEnemy;
lasers.push(laser);
}
currentWord = '';
currentWordDisplay.textContent = '';
e.target.value = '';
} else if (currentWord && !currentWord.startsWith(typed)) {
const targetEnemy = enemies.find(e =>
e.word.toLowerCase() === currentWord && e.targeted);
if (targetEnemy) {
targetEnemy.targeted = false;
}
currentWord = '';
currentWordDisplay.textContent = '';
currentWordDisplay.style.color = '#33ccff';
}
});
// スコア送信イベントリスナーを追加
submitScoreButton.addEventListener('click', () => {
const username = usernameInput.value.trim();
if (username) {
submitScore(username, score);
} else {
submitMessage.textContent = 'Please enter your name';
submitMessage.style.color = '#ff3333';
}
});
startGame();
</script>
</body>
</html>
おわりに
知識不足の自分でもシームレスな連携ができる時代に感激しています。
一方で2025年はどうなるんだろうと焦る今です。
上手く技術と付き合わないとですね!