コンテンツにスキップ

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>

使い方

  1. バックエンドを起動: 前述の main.pyuvicorn で実行しておきます。
  2. フロントエンドを起動: この index.html をブラウザで開きます。
  3. 動作確認:
  4. URL(例: https://ja.wikipedia.org/wiki/%E3%83%91%E3%82%A4%E3%82%BD%E3%83%B3)を投稿してみてください。リンクテキストがデコードされて読みやすくなるはずです。
  5. [修正する]を押すとフォームが切り替わり、保存するとその記事が更新されます。

次は、画像をアップロードして記事と一緒に表示する機能や、ダークモードへの対応などを行ってみますか?