API一覧を取得→HTMLに自動表示するミニアプリ
See the Pen Public APIs Explorer by MONO365 -Color your days- (@monoqlo365) on CodePen.
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Public APIs Explorer — ミニアプリ</title>
<style>
:root{--bg:#0f1724;--card:#0b1220;--muted:#9aa4b2;--accent:#4f46e5}
html,body{height:100%;margin:0;font-family:Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;color:#e6eef8;background:linear-gradient(180deg,#071029 0%,#07172a 100%)}
.app{max-width:1100px;margin:28px auto;padding:20px}
header{display:flex;align-items:center;gap:16px;margin-bottom:18px}
h1{font-size:20px;margin:0}
.controls{display:flex;gap:8px;flex-wrap:wrap}
input[type=text],select{padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.06);background:rgba(255,255,255,0.02);color:inherit;min-width:180px}
button{background:var(--accent);border:none;color:white;padding:10px 12px;border-radius:8px;cursor:pointer}
.grid{display:grid;grid-template-columns:1fr;gap:12px}
@media(min-width:760px){.grid{grid-template-columns:repeat(2,1fr)}}
.card{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,0.03);box-shadow:0 6px 18px rgba(2,6,23,0.6)}
.card h3{margin:0 0 6px 0;font-size:16px}
.meta{display:flex;gap:8px;flex-wrap:wrap;align-items:center;color:var(--muted);font-size:13px}
.tag{background:rgba(255,255,255,0.03);padding:6px 8px;border-radius:999px;font-size:12px}
footer{margin-top:18px;color:var(--muted);font-size:13px}
.toolbar{display:flex;gap:8px;align-items:center}
.pagination{display:flex;gap:6px;align-items:center}
.pagebtn{background:transparent;border:1px solid rgba(255,255,255,0.04);padding:6px 8px;border-radius:6px;color:var(--muted);cursor:pointer}
.small{font-size:13px;color:var(--muted)}
.links{margin-top:8px;display:flex;gap:8px}
a.btn-link{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:6px 8px;border-radius:8px;color:inherit;text-decoration:none}
.empty{padding:18px;border-radius:10px;border:1px dashed rgba(255,255,255,0.04);color:var(--muted)}
</style>
</head>
<body>
<div class="app" id="app">
<header>
<div>
<h1>Public APIs Explorer</h1>
<div class="small">https://api.publicapis.dev/entries を使って API 一覧を自動表示します</div>
</div>
<div style="flex:1"></div>
<div class="toolbar">
<button id="refresh">再読み込み</button>
</div>
</header>
<section class="controls card">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
<input id="q" type="text" placeholder="検索(API名 / 説明)..." />
<select id="category"><option value="">カテゴリで絞り込み(すべて)</option></select>
<select id="auth"><option value="">認証(すべて)</option><option value="null">なし</option><option value="apiKey">API Key</option><option value="OAuth">OAuth</option></select>
<select id="https"><option value="">HTTPS(すべて)</option><option value="true">あり</option><option value="false">なし</option></select>
<div style="margin-left:auto;display:flex;gap:8px;align-items:center">
<label class="small">表示件数</label>
<select id="perPage"><option>10</option><option>20</option><option>50</option></select>
</div>
</div>
</section>
<main id="list" class="grid" style="margin-top:14px"></main>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px">
<div id="summary" class="small"></div>
<div class="pagination" id="pagination"></div>
</div>
<footer>
<div class="small">Powered by <a href="https://api.publicapis.dev/entries" target="_blank">api.publicapis.dev</a>. このミニアプリはクライアント側のみで動作します。</div>
</footer>
</div>
<script>
// --- 設定 ---
const API_URL = 'https://api.publicapis.dev/entries';
// --- 状態 ---
let entries = []; // 全件保存
let filtered = []; // フィルタ後
let page = 1;
let perPage = 10;
// --- 要素 ---
const el = {
list: document.getElementById('list'),
q: document.getElementById('q'),
category: document.getElementById('category'),
auth: document.getElementById('auth'),
https: document.getElementById('https'),
perPage: document.getElementById('perPage'),
refresh: document.getElementById('refresh'),
summary: document.getElementById('summary'),
pagination: document.getElementById('pagination')
};
// --- 初期化 ---
async function init(){
addEventListeners();
perPage = Number(el.perPage.value);
await loadData();
}
function addEventListeners(){
el.q.addEventListener('input', onFilter);
el.category.addEventListener('change', onFilter);
el.auth.addEventListener('change', onFilter);
el.https.addEventListener('change', onFilter);
el.perPage.addEventListener('change', () => { perPage = Number(el.perPage.value); page = 1; render(); });
el.refresh.addEventListener('click', () => loadData(true));
}
// --- データ取得 ---
async function loadData(force=false){
try{
el.refresh.disabled = true;
el.refresh.textContent = '読み込み中...';
const res = await fetch(API_URL);
if(!res.ok) throw new Error(res.status + ' ' + res.statusText);
const json = await res.json();
// API の返却形式: { count: number, entries: [...] } のはず
entries = json.entries || json || [];
// カテゴリをセット
populateCategories(entries);
page = 1;
onFilter();
}catch(err){
el.list.innerHTML = `<div class="empty">データ取得に失敗しました: ${err.message}</div>`;
console.error(err);
}finally{
el.refresh.disabled = false;
el.refresh.textContent = '再読み込み';
}
}
function populateCategories(list){
const cats = Array.from(new Set(list.map(e => e.Category || e.category).filter(Boolean))).sort();
el.category.innerHTML = '<option value="">カテゴリで絞り込み(すべて)</option>' + cats.map(c=>`<option value="${escapeHtml(c)}">${escapeHtml(c)}</option>`).join('');
}
// --- フィルタ ---
function onFilter(){
const q = el.q.value.trim().toLowerCase();
const cat = el.category.value;
const auth = el.auth.value;
const https = el.https.value;
filtered = entries.filter(e => {
const name = (e.API || e.api || '').toLowerCase();
const desc = (e.Description || e.description || '').toLowerCase();
if(q && !(name.includes(q) || desc.includes(q))) return false;
if(cat && (e.Category || e.category) !== cat) return false;
if(auth){
if(auth === 'null'){
if((e.Auth || e.auth)) return false;
} else {
if((e.Auth || e.auth) !== auth) return false;
}
}
if(https){
const hasHttps = !!(e.HTTPS === true || String(e.HTTPS).toLowerCase()==='true');
if(https === 'true' && !hasHttps) return false;
if(https === 'false' && hasHttps) return false;
}
return true;
});
page = 1;
render();
}
// --- レンダリング ---
function render(){
el.list.innerHTML = '';
const total = filtered.length;
const totalPages = Math.max(1, Math.ceil(total / perPage));
if(page > totalPages) page = totalPages;
const start = (page - 1) * perPage;
const slice = filtered.slice(start, start + perPage);
if(slice.length === 0){
el.list.innerHTML = '<div class="empty">条件に一致するAPIがありません。</div>';
} else {
const frag = document.createDocumentFragment();
slice.forEach(entry => frag.appendChild(renderCard(entry)));
el.list.appendChild(frag);
}
// summary
el.summary.textContent = `表示 ${start+1}〜${Math.min(start+slice.length,total)} / 全 ${total} 件`;
// pagination
renderPagination(totalPages);
}
function renderCard(e){
const card = document.createElement('article');
card.className = 'card';
const title = document.createElement('h3');
title.textContent = e.API || e.api || 'NO NAME';
const desc = document.createElement('div');
desc.textContent = e.Description || e.description || '';
desc.style.margin = '8px 0';
const meta = document.createElement('div');
meta.className = 'meta';
const auth = document.createElement('div'); auth.className='tag'; auth.textContent = `Auth: ${e.Auth || e.auth || 'None'}`;
const https = document.createElement('div'); https.className='tag'; https.textContent = `HTTPS: ${e.HTTPS ? 'Yes' : 'No'}`;
const cors = document.createElement('div'); cors.className='tag'; cors.textContent = `CORS: ${e.Cors || e.cors || 'unknown'}`;
const category = document.createElement('div'); category.className='tag'; category.textContent = e.Category || e.category || '';
const links = document.createElement('div'); links.className='links';
if(e.Link || e.link) links.innerHTML += `<a class="btn-link" href="${escapeHtml(e.Link||e.link)}" target="_blank">公式</a>`;
links.innerHTML += `<button class="pagebtn" data-url="${escapeHtml(e.Link||e.link||'')}">curl をコピー</button>`;
meta.appendChild(auth); meta.appendChild(https); meta.appendChild(cors); meta.appendChild(category);
card.appendChild(title); card.appendChild(desc); card.appendChild(meta); card.appendChild(links);
// copy curl handler
card.querySelector('.pagebtn').addEventListener('click', (ev)=>{
const url = ev.currentTarget.dataset.url;
const curl = url ? `curl -s "${url}"` : 'curl コマンドを生成できません (リンクがありません)';
navigator.clipboard.writeText(curl).then(()=>{
ev.currentTarget.textContent = 'コピーしました';
setTimeout(()=> ev.currentTarget.textContent = 'curl をコピー', 1200);
}).catch(()=> alert('クリップボードへの書き込みに失敗しました'));
});
return card;
}
function renderPagination(totalPages){
el.pagination.innerHTML = '';
const first = createPageBtn('<<', ()=>{ page=1; render(); });
const prev = createPageBtn('<', ()=>{ if(page>1) page--; render(); });
el.pagination.appendChild(first); el.pagination.appendChild(prev);
// show pages with window
const win = 5; const start = Math.max(1, page - Math.floor(win/2));
for(let i = start; i <= Math.min(totalPages, start + win -1); i++){
const btn = createPageBtn(i, ()=>{ page = i; render(); });
if(i === page) btn.style.fontWeight = '600';
el.pagination.appendChild(btn);
}
const next = createPageBtn('>', ()=>{ if(page<totalPages) page++; render(); });
const last = createPageBtn('>>', ()=>{ page = totalPages; render(); });
el.pagination.appendChild(next); el.pagination.appendChild(last);
}
function createPageBtn(label, onClick){
const b = document.createElement('button'); b.className='pagebtn'; b.textContent = label; b.addEventListener('click', onClick); return b;
}
// --- ヘルパ ---
function escapeHtml(s){ if(!s) return ''; return String(s).replace(/[&<>"']/g, c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c])); }
// 起動
init();
</script>
</body>
</html>
HTML機能のまとめ:
https://api.publicapis.dev/entriesから一覧を取得して自動表示- 検索(API名・説明)・カテゴリ絞り込み・認証/HTTPSフィルタ・表示件数切替
- ページネーション・各APIの公式リンクを開くボタン・curlコマンドのコピー機能
- クライアント単体(バックエンド不要)


