HTML+JavaScriptでブログのフロントエンドを作る
[質問]
このバックエンドを利用し、HTML+JavaScriptのフロントエンドを作ってください。用途は自分専用のマイクロブログとします。画面上部に記事投稿フォームがあり、画面下部に記事がタイムラインで並びます。記事に含まれるURLはリンク化してください。URLにUTF-8文字列のパーセントエンコードが含まれていればデコードしてからリンクテキストにしてください。各記事に、[テキストをコピーする]ボタン、[修正する]ボタン、[削除する]ボタンを付けてください。[修正する]ボタンを押すと、画面上部の入力フォームに既存の記事が読み込まれ、それを投稿すると新規登録ではなく、記事の書き換えになります。
[回答] Gemini
自分専用のマイクロブログとして、シンプルかつ機能的なフロントエンドを作成しました。 このHTMLファイルをバックエンド(FastAPI)と同じディレクトリに保存し、ブラウザで開くだけで動作します。
実装のポイント
- 状態管理: 現在編集中の記事IDを保持する変数
editingIdを用意し、投稿ボタンが「新規作成」か「更新」かを自動で判断します。 - URLのリンク化: 正規表現でURLを抽出し、
decodeURIComponentを使ってパーセントエンコード(日本語など)を読みやすい形式にデコードして表示します。 - UI/UX: 記事投稿後や削除後にリストを自動更新し、スムーズな操作感を実現しています。
フロントエンド実装 (index.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Microblog</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f4f7f6; }
h1 { text-align: center; color: #333; }
/* 投稿フォーム */
.post-form { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 30px; }
input, textarea { width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
textarea { height: 100px; resize: vertical; }
button { cursor: pointer; border: none; border-radius: 4px; padding: 10px 15px; transition: 0.3s; }
.btn-primary { background-color: #007bff; color: white; width: 100%; font-weight: bold; }
.btn-primary:hover { background-color: #0056b3; }
.btn-cancel { background-color: #6c757d; color: white; margin-top: 5px; display: none; }
/* タイムライン */
.article { background: white; padding: 15px; border-radius: 8px; margin-bottom: 15px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.article-date { font-size: 0.8rem; color: #888; margin-bottom: 8px; }
.article-title { font-weight: bold; margin-bottom: 5px; display: block; }
.article-content { white-space: pre-wrap; word-break: break-all; line-height: 1.5; }
.article-content a { color: #007bff; text-decoration: none; }
.article-content a:hover { text-decoration: underline; }
/* 操作ボタン */
.actions { margin-top: 10px; display: flex; gap: 5px; }
.btn-sm { font-size: 0.8rem; padding: 5px 10px; }
.btn-copy { background-color: #e2e3e5; color: #383d41; }
.btn-edit { background-color: #d1ecf1; color: #0c5460; }
.btn-delete { background-color: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>My Microblog</h1>
<div class="post-form">
<h3 id="form-title">新規投稿</h3>
<input type="text" id="title" placeholder="タイトル(任意)">
<textarea id="content" placeholder="今なにしてる?"></textarea>
<button id="submit-btn" class="btn-primary" onclick="handleSubmit()">投稿する</button>
<button id="cancel-btn" class="btn-cancel" onclick="resetForm()">キャンセル</button>
</div>
<div id="timeline"></div>
<script>
const API_BASE = "http://127.0.0.1:8000";
let editingId = null;
// 起動時に記事取得
window.onload = fetchArticles;
// URLをリンク化する関数 (デコード対応)
function linkify(text) {
const urlPattern = /(https?:\/\/[\w\.\/\?\&\=\%\-\#\~]+)/gi;
return text.replace(urlPattern, (url) => {
let decodedUrl = url;
try {
decodedUrl = decodeURIComponent(url);
} catch (e) {
console.error("URLのデコードに失敗しました", e);
}
return `<a href="${url}" target="_blank">${decodedUrl}</a>`;
});
}
// 記事リスト取得
async function fetchArticles() {
const res = await fetch(`${API_BASE}/articles`);
const articles = await res.json();
const timeline = document.getElementById('timeline');
timeline.innerHTML = '';
for (const art of articles) {
// リスト取得APIはサマリーのみなので、個別詳細を並列で取得(自分専用なので簡易化)
const detailRes = await fetch(`${API_BASE}/articles/${art.id}`);
const detail = await detailRes.json();
const div = document.createElement('div');
div.className = 'article';
div.innerHTML = `
<div class="article-date">${new Date(detail.created_at).toLocaleString()}</div>
<span class="article-title">${detail.title}</span>
<div class="article-content">${linkify(detail.content)}</div>
<div class="actions">
<button class="btn-sm btn-copy" onclick="copyText('${detail.content.replace(/'/g, "\\'")}')">コピー</button>
<button class="btn-sm btn-edit" onclick="startEdit('${detail.id}', '${detail.title}', '${detail.content.replace(/'/g, "\\'")}')">修正</button>
<button class="btn-sm btn-delete" onclick="deleteArticle('${detail.id}')">削除</button>
</div>
`;
timeline.appendChild(div);
}
}
// 投稿・更新処理
async function handleSubmit() {
const title = document.getElementById('title').value;
const content = document.getElementById('content').value;
if (!content) return alert("内容を入力してください");
const url = editingId ? `${API_BASE}/articles/${editingId}` : `${API_BASE}/articles`;
const method = "POST"; // バックエンドの仕様に合わせる
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content })
});
if (res.ok) {
resetForm();
fetchArticles();
}
}
// 修正モード開始
function startEdit(id, title, content) {
editingId = id;
document.getElementById('title').value = title;
document.getElementById('content').value = content;
document.getElementById('form-title').innerText = "記事を修正";
document.getElementById('submit-btn').innerText = "更新する";
document.getElementById('cancel-btn').style.display = "block";
window.scrollTo(0, 0);
}
// フォームリセット
function resetForm() {
editingId = null;
document.getElementById('title').value = '';
document.getElementById('content').value = '';
document.getElementById('form-title').innerText = "新規投稿";
document.getElementById('submit-btn').innerText = "投稿する";
document.getElementById('cancel-btn').style.display = "none";
}
// 削除処理
async function deleteArticle(id) {
if (!confirm("本当に削除しますか?")) return;
await fetch(`${API_BASE}/articles/${id}`, { method: 'DELETE' });
fetchArticles();
}
// コピー機能
function copyText(text) {
navigator.clipboard.writeText(text).then(() => alert("コピーしました"));
}
</script>
</body>
</html>
使い方
- バックエンドを起動: 前述の
main.pyをuvicornで実行しておきます。 - フロントエンドを起動: この
index.htmlをブラウザで開きます。 - 動作確認:
- URL(例:
https://ja.wikipedia.org/wiki/%E3%83%91%E3%82%A4%E3%82%BD%E3%83%B3)を投稿してみてください。リンクテキストがデコードされて読みやすくなるはずです。 - [修正する]を押すとフォームが切り替わり、保存するとその記事が更新されます。
次は、画像をアップロードして記事と一緒に表示する機能や、ダークモードへの対応などを行ってみますか?