みんなで見られるのは、ポストがサーバーに集まっているからです。
だから画面とサーバーを分けて作ります。
| 言語 | 役割 | 今日 |
|---|---|---|
| HTML | 画面の部品と並び | 書く |
| CSS | 色や形などの見た目 | 用意済み。最後に少しいじる |
| JavaScript | 動き。サーバーとのやりとり | 書く |
| 道具 | 役割 |
|---|---|
| VS Code | コードを書くエディタ |
| Bun | 書いたページを手元で動かす(開発サーバー) |
| Chrome | 動いた画面を確かめる。エラーもここで見る |
この 3 つは、プロのエンジニアも毎日使っている道具です。
家に帰ってからも、同じ手順で続きが作れます。
| コマンド | この教材での使いみち |
|---|---|
bun run index.html |
書いたページを手元で配る(開発サーバー) |
bun run server.js |
サーバーを動かす(応用 B5) |
bun add react |
外のパッケージを入れる(応用 B10) |
もっと知りたい人は、公式ドキュメントへ:https://bun.com/docs
DOCS.md(手元資料)を開くmini-x.zip を解凍するMac はダブルクリック、Windows は右クリックして「すべて展開」で解凍できます。mini-x というフォルダができたら成功です。
# Mac
curl -fsSL https://bun.com/install | bash
# Windows(PowerShell)
powershell -c "irm bun.sh/install.ps1|iex"
bun --version で数字が出たら成功mini-x フォルダを開くbun run index.html
左右にメニューが出たら成功です。
保存と字下げは自動で行われます。
F12(Mac は Cmd + Option + I)で開くここから先、動かないときはまず Console を見ます。

③ の投稿カードはポスト 1 件分、④ のタイムラインはそれを縦に並べた入れ物です。
<タグ名> と </タグ名> で挟んで書く<h1>ホーム</h1>
<button id="post-btn">ポストする</button>
<p class="text">はじめまして!</p>
| 書き方 | 意味 |
|---|---|
id="..." |
部品の名札。ページの中で 1 つだけ 主に JavaScript が探すときに使う |
class="..." |
見た目の種類。何個でも 付けられる CSS は主にこの名前で色や形を決める |
資料どおりに class を付けると、
用意してある CSS が当たって X の見た目になります。
| タグ | 何のタグか |
|---|---|
<header> |
見出しのまとまり |
<h1> |
いちばん大きな見出し |
<div> |
部品をまとめる入れ物 |
<img> |
画像を出す |
<input> |
1 行の入力欄 |
<textarea> |
複数行の入力欄 |
| タグ | 何のタグか |
|---|---|
<button> |
ボタン |
<ul> |
リスト全体 |
<li> |
<ul> の中の 1 項目 |
<span> |
文の一部の入れ物 |
<p> |
文章のまとまり |
<template> |
画面に出ない見本 |
<img> と <input> は、閉じタグを書きません。src や placeholder などの設定(属性)は、手元資料の表を見てください。
書く場所:index.html の <main> の中
<h1>ホーム</h1>
成功:大きく太い「ホーム」の字が出る
書き換える:<h1> を <header class="header"> で包む
<header class="header">
<h1>ホーム</h1>
</header>
成功:字が小さくなり、下に区切りの線が出るstyles.css にある header の見た目が当たった
書く場所:</header> の下
<div class="compose">
<img class="avatar" src="icon.svg" alt="">
</div>
成功:見出しの下に、丸いアイコンが出る
書く場所:アイコンの下
<div class="compose">
<img class="avatar" src="icon.svg" alt="">
<div class="compose-body">
<textarea id="text-input" placeholder="いまどうしてる?"></textarea>
<button id="post-btn">ポストする</button>
</div>
</div>
成功:「いまどうしてる?」の欄と、薄い青のボタンが出る
書く場所:<textarea> の上
<div class="compose">
<img class="avatar" src="icon.svg" alt="">
<div class="compose-body">
<input id="name-input" type="text" placeholder="名前">
<textarea id="text-input" placeholder="いまどうしてる?"></textarea>
<button id="post-btn">ポストする</button>
</div>
</div>
成功:本文の欄の上に、「名前」の欄が出る
書く場所:投稿欄の最後の </div> の下
<ul id="timeline">
<li class="post">
<img class="avatar" src="icon.svg" alt="">
</li>
</ul>
成功:投稿欄の下に、アイコンだけのカードが出る
書く場所:カードのアイコンの下
<ul id="timeline">
<li class="post">
<img class="avatar" src="icon.svg" alt="">
<div class="post-body">
<span class="name">たろう</span>
<p class="text">はじめまして!</p>
</div>
</li>
</ul>
成功:アイコンの右に、名前と本文が出る
<template> の中身は、そのままでは画面に出ない<template> に入れた見本は画面に出ず、コピーされるのを待つ書き換える:<li> を <ul> の外に出して <template> で包む
名前と本文は空にする
<ul id="timeline"></ul>
<template id="post-template">
<li class="post">
<img class="avatar" src="icon.svg" alt="">
<div class="post-body">
<span class="name"></span>
<p class="text"></p>
</div>
</li>
</template>
成功:カードが画面から消える(<template> の中身は画面に出ない)
崩れて見えるときは、閉じタグとclass のつづりを確かめましょう。
{ } は 1 件分のまとまり、[ ] はそれを並べたものconst posts = [
{ name: 'たろう', text: 'はじめまして!' },
{ name: 'はなこ', text: 'こんにちは〜' },
];
| 書き方 | 取り出せるもの |
|---|---|
posts[0] |
1 件目のまとまり(0 から数える) |
posts[0].name |
1 件目の名前 'たろう' |
posts[1].text |
2 件目の本文 'こんにちは〜' |
書く場所:script.js のいちばん下
const posts = [
{ name: 'たろう', text: 'はじめまして!' },
{ name: 'はなこ', text: 'こんにちは〜' },
{ name: 'じろう', text: 'ミニX できた
' },
];
console.log(posts);
成功:Console に (3) [{…}, {…}, {…}] と出る。三角を押すと中身が開く
| やること | 書き方 |
|---|---|
| ① 名札で部品を探す | document.getElementById('timeline') |
| ② 見本をコピーする | template.content.cloneNode(true) |
| ③ カードの中の部品を探す | card.querySelector('.name') |
| ④ 文字を入れる | 部品.textContent = posts[0].name |
| ⑤ タイムラインに入れる | timeline.appendChild(card) |
querySelector は .name のように class で探せます。. を付け忘れると見つかりません。
書く場所:console.log(posts); の下
function showPosts() {
const timeline = document.getElementById('timeline');
const template = document.getElementById('post-template');
const card = template.content.cloneNode(true);
card.querySelector('.name').textContent = posts[0].name;
card.querySelector('.text').textContent = posts[0].text;
timeline.appendChild(card);
}
showPosts();
成功:タイムラインに「たろう」のカードが 1 枚出る
for で全部のデータに同じことをするfor (const post of posts) は、1 件ずつ取り出して中身をくり返すfor (const post of posts) {
console.log(post.name);
}
// たろう → はなこ → じろう の順に出る
post に 1 件目が入り、2 周目は 2 件目が入るposts[0] と書いていたところを post に変えれば、全件に同じことができる書き換える:カードを作る 4 行を for で包み、posts[0] を post にする
function showPosts() {
const timeline = document.getElementById('timeline');
const template = document.getElementById('post-template');
timeline.textContent = '';
for (const post of posts) {
const card = template.content.cloneNode(true);
card.querySelector('.name').textContent = post.name;
card.querySelector('.text').textContent = post.text;
timeline.appendChild(card);
}
}
成功:カードが 3 枚並ぶ
書き足す:posts の中に、自分の名前と好きな本文で 1 行
const posts = [
{ name: 'たろう', text: 'はじめまして!' },
{ name: 'はなこ', text: 'こんにちは〜' },
{ name: 'じろう', text: 'ミニX できた
' },
{ name: 'さぶろう', text: '自分で足した 4 件目' },
];
成功:保存すると、カードが 4 枚に増える
データを変えると、画面が変わります。
ここまでできたら 1 日目は完了です。
script.js の中にしかない配列を書く代わりに、サーバーに「ポストをください」と頼んで もらってきます。
(講師が伝える URL)/posts?room=(部屋の名前)
[
{
"id": "6b1f...",
"name": "じろう",
"text": "ミニX できた
",
"likes": 0,
"createdAt": "2026-11-04T..."
},
...
]
API と ROOM も、fetch と awaitconst res = await fetch(API + '/posts?room=' + ROOM);
const posts = await res.json();
fetch(URL) は、その URL のサーバーに頼む命令res.json() は、返事の中身を JavaScript の配列として取り出す命令await は「返事が来るまで待つ」印。サーバーの返事には少し時間がかかるawait を使う関数には、頭に async を付ける消す:const posts = [ ... ]; と console.log(posts);
書き足す:関数の頭に async、中の先頭に 2 行
async function showPosts() {
const res = await fetch(API + '/posts?room=' + ROOM);
const posts = await res.json();
const timeline = document.getElementById('timeline');
const template = document.getElementById('post-template');
timeline.textContent = '';
// ここから下はミッション 1 のまま
成功:サーバーにあるポストが新しい順に並ぶ
index.html:ヘッダーの <h1> の下に 1 行
<header class="header">
<h1>ホーム</h1>
<button id="reload-btn">更新</button>
</header>
script.js:いちばん下に 1 行
document.getElementById('reload-btn').addEventListener('click', showPosts);
addEventListener は、
「クリックされたら showPosts を動かして」と頼む命令です。
開発者ツールの Network タブで、posts の行が赤くなっていないか見ます。
赤い行は失敗です。クリックすると、サーバーの返事が読めます。
await fetch(API + '/posts?room=' + ROOM, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, text: text }),
});
| 書くもの | 意味 |
|---|---|
method: 'POST' |
データを送って、サーバーに何かしてもらう頼み方 |
headers |
送る中身が JSON だと伝える |
body |
送る中身。JSON.stringify で JSON の文字にする |
書く場所:script.js の showPosts(); の上
async function addPost() {
const name = document.getElementById('name-input').value;
const text = document.getElementById('text-input').value;
console.log(name, text);
}
いちばん下に 1 行
document.getElementById('post-btn').addEventListener('click', addPost);
成功:名前と本文を入れて「ポストする」を押すと、Console にその中身が出る
書き換える:console.log の行を、送る処理と後片付けに置き換える
async function addPost() {
const name = document.getElementById('name-input').value;
const text = document.getElementById('text-input').value;
await fetch(API + '/posts?room=' + ROOM, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, text: text }),
});
document.getElementById('text-input').value = '';
showPosts();
}
成功:自分のポストがタイムラインの先頭に出て、本文の欄が空になる
| 状況 | サーバーの返事 |
|---|---|
| 本文が空 | 本文を入れてください |
| 本文が 140 文字を超えた | 本文は140文字までです |
JSON.stringify を書き忘れた |
送るデータを JSON の文字にしてください |
ポストは textContent で画面に出しています。
ほかの人の文字を HTML として読み込まないので、いたずらで画面を壊されません。
自分のポストはサーバーに保存されたので、
ほかの人の画面にも出ます。
POST /posts/ポストのid/like?room=sample
id という番号が付いているid で伝えるindex.html:<template> の中、本文の下に 1 行
<template id="post-template">
<li class="post">
<img class="avatar" src="icon.svg" alt="">
<div class="post-body">
<span class="name"></span>
<p class="text"></p>
<button class="like-btn"><span class="like-count"></span></button>
</div>
</li>
</template>
成功:保存すると、カードの下にハートのボタンが出る
showPosts の for の中:本文を入れる行の下に足す
card.querySelector('.text').textContent = post.text;
card.querySelector('.like-count').textContent = post.likes;
card.querySelector('.like-btn').addEventListener('click', function () {
addLike(post.id);
});
timeline.appendChild(card);
addPost の下:いいねを頼む関数
async function addLike(id) {
await fetch(API + '/posts/' + id + '/like?room=' + ROOM, { method: 'POST' });
showPosts();
}
function () { ... } で包むのかcard.querySelector('.like-btn').addEventListener('click', function () {
addLike(post.id);
});
addEventListener('click', addLike) と書くと、どのポストかを渡せないid で addLike を呼ぶ数字がサーバーに保存されているので、
ほかの人の画面でも同じ数が見えます。
styles.css の先頭で、色をまとめて変えられる:root {
--accent: #1d9bf0;
--like: #f91880;
--bg: #ffffff;
--text: #0f1419;
}
--accent)icon.svg)value="...")| ミッション | |
|---|---|
| A1 | アプリの名前を自分で決める |
| A2 | 投稿欄の言葉を変える |
| A3 | デフォルトアイコンの色を変える(icon.svg) |
| A4 | 投稿カードを、角の丸いカードにする |
| A5 | 右のトレンド欄を、自分の話題にする |
| A6 | アイコンとボタンを、丸から四角にする |
どのミッションにも、手元資料の最後に答えがあります。
Ctrl + Enter でポストする| やりたいこと | 使ったもの |
|---|---|
| 画面の部品を作る | HTML のタグ、id、class、<template> |
| データをまとめて持つ | 配列 [ ]、オブジェクト { } |
| 部品を探して中身を入れる | getElementById、querySelector、textContent |
| 同じことをくり返す | for (const post of posts) |
| ボタンが押されたら動かす | addEventListener |
| サーバーから取る、送る | fetch、await、POST、JSON.stringify |
今日のミニX は、家でも bun run index.html で続きを作れます。
「こんな機能を足したい」と思ったら、それが次に学ぶことの入口です。