「アプリ紹介」を毎回カードを手打ちせず、
WordPress の投稿データ(CPT=app)を REST API で取得→きれいなカードに自動整形する方法です。ChatGPT有効活用してわからないところを都度聞いていきます。トライアンドエラーの繰り返しで完成。
以下の手順をそのままコピペで導入できます。
ゴール
- 固定ページ
/apptop/
に「アプリ一覧」を表示 - 検索(キーワード)+カテゴリフィルタ(チップ)付き
- 新しいアプリ投稿(CPT=app)を公開するだけで自動で一覧に反映
- 日本語ページ用(多言語を使う場合は英語版も簡単に増やせます)
0. 事前準備(1回だけ)
- **カスタム投稿タイプ(CPT)**を用意
例:
- 投稿タイプキー:app
- ラベル:Apps(お好みで)
- REST API 有効化(ON) - **タクソノミー(カテゴリ)**を用意
例:
- タクソノミーキー:app_cat
-app
に紐付け
- REST API 有効化(ON)
- 代表的なスラッグを作成(例)
–study
(学習)、tool
(ツール)、game
(ゲーム/クイズ)
CPT/UI は「Custom Post Type UI」などのプラグイン or
functions.php
で作成してOK。
- REST API が有効か確認
ブラウザで次を開き、JSON が返ればOK:
https://あなたのサイト/wp-json/wp/v2/app?per_page=5&_embed=1
1. 固定ページを作成(/apptop/)
固定ページを作成し、カスタムHTML ブロックに次の HTML を貼り付けます。
(CSS/JSは後述の固定配置を使います)
<!-- Apps 一覧(日本語)— HTMLのみ(CSS/JSは別配置を使用) -->
<div id="app-list-ja">
<div class="al-wrap">
<header class="head">
<h1>アプリ一覧</h1>
<p>Webで即使える学習・ユーティリティを中心に公開中。随時アップデートします。</p>
<div class="controls" aria-label="アプリの検索と絞り込み">
<label class="search" for="app-q-ja" aria-label="検索">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M21 21l-4.3-4.3" stroke="#6B7280" stroke-width="2" stroke-linecap="round"></path>
<circle cx="11" cy="11" r="7" stroke="#6B7280" stroke-width="2"></circle>
</svg>
<input id="app-q-ja" type="search" placeholder="アプリ名や説明で検索… (例: 天気, 簿記, 原付)" autocomplete="off" />
</label>
<!-- data-filter は app_cat の“スラッグ”と一致させてください -->
<div class="filter" role="group" aria-label="カテゴリ">
<button class="chip active" data-filter="all" type="button">すべて</button>
<button class="chip" data-filter="study" type="button">学習</button>
<button class="chip" data-filter="tool" type="button">ツール</button>
<button class="chip" data-filter="game" type="button">ゲーム/クイズ</button>
</div>
</div>
</header>
<!-- JS が自動でカードを差し込みます -->
<section class="grid" id="app-grid-ja"></section>
<footer class="foot">© 東京app工房 — Apps are continuously improved.</footer>
</div>
</div>
2. CSS を固定配置
「外観 > カスタマイズ > 追加CSS」 でも、
「Simple Custom CSS & JS(プラグイン)」の CSS でもOK。
次の CSS をまるごと貼り付けて保存します。
/* ===== Apps List JA (scoped) ===== */
#app-list-ja{
--bg:#F8FAFC;--txt:#111827;--sub:#6B7280;--card:#ffffff;--line:#E5E7EB;
--accent:#3B82F6;--accent2:#F59E0B;--ok:#10B981;--warn:#F97316;--muted:#9CA3AF;
background:var(--bg);color:var(--txt);
font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans JP",sans-serif;
}
#app-list-ja .al-wrap{max-width:1080px;margin:0 auto;padding:24px;box-sizing:border-box}
/* Head */
#app-list-ja .head{padding:20px 0 10px;border-bottom:1px solid var(--line)}
#app-list-ja .head h1{margin:0 0 8px;font-size:32px;line-height:1.2}
#app-list-ja .head p{margin:0;color:var(--sub)}
/* Controls */
#app-list-ja .controls{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin:16px 0}
#app-list-ja .search{flex:1 1 260px;display:flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:10px 12px}
#app-list-ja .search input{border:none;outline:none;background:transparent;width:100%;font-size:16px;color:var(--txt)}
#app-list-ja .filter{display:flex;gap:8px;flex-wrap:wrap}
#app-list-ja .chip{padding:8px 12px;border:1px solid var(--line);border-radius:999px;background:#fff;color:var(--txt);font-weight:600;cursor:pointer}
#app-list-ja .chip.active{background:var(--accent);color:#fff;border-color:transparent}
/* Grid & Card */
#app-list-ja .grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
#app-list-ja .card{background:var(--card);border:1px solid var(--line);border-radius:16px;overflow:hidden;display:flex;flex-direction:column}
#app-list-ja .thumb{background:#EEF2F7;aspect-ratio:16/9;display:flex;align-items:center;justify-content:center}
#app-list-ja .thumb img{display:block;width:100%;height:auto;object-fit:cover}
#app-list-ja .thumb span{font-size:40px;opacity:.85}
#app-list-ja .inner{padding:14px 14px 16px}
#app-list-ja .title{margin:0 0 6px;font-size:20px;line-height:1.3}
#app-list-ja .meta{font-size:13px;color:var(--sub);margin-bottom:10px}
#app-list-ja .desc{font-size:15px;color:#374151;margin:0 0 12px}
/* Badges */
#app-list-ja .badges{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
#app-list-ja .badge{display:inline-block;background:#EEF2FF;color:#1E3A8A;font-weight:700;font-size:12px;padding:3px 8px;border-radius:999px}
/* Buttons */
#app-list-ja .btns{display:flex;gap:8px;margin-top:auto}
#app-list-ja .btn{display:inline-block;padding:10px 14px;border-radius:12px;font-weight:700;text-decoration:none}
#app-list-ja .btn.primary{background:var(--accent);color:#fff}
#app-list-ja .btn.ghost{background:#E5E7EB;color:#111827}
/* Footer */
#app-list-ja .foot{padding:20px 0 8px;color:var(--sub);font-size:13px}
/* Small */
@media (max-width:480px){
#app-list-ja .head h1{font-size:26px}
}
3. JavaScript を固定配置
Simple Custom CSS & JS(プラグイン推奨)のJavaScriptとして登録し、
フロントエンド/フッターで有効化して保存します。
以下をそのまま貼り付け。
/* ===== Apps List JA (CPT=app / TAX=app_cat) ===== */
(function(){
const root = document.getElementById('app-list-ja');
if(!root) return;
const grid = root.querySelector('#app-grid-ja');
const q = root.querySelector('#app-q-ja');
const chips = Array.from(root.querySelectorAll('.chip'));
// あなたの環境:CPT=app、カテゴリ=app_cat、Polylang想定
const CPT_ENDPOINT = '/wp-json/wp/v2/app';
const TAXONOMY = 'app_cat';
const LANG = 'ja'; // まず ja で取得、0件なら言語なしでフォールバック
loadApps();
async function loadApps(){
// 1) lang=ja をトライ
let url = new URL(location.origin + CPT_ENDPOINT);
url.searchParams.set('per_page','12');
url.searchParams.set('_embed','1');
url.searchParams.set('lang', LANG);
try {
let res = await fetch(url.toString(), {cache:'no-store'});
let items = await res.json();
if (Array.isArray(items) && items.length){
render(items); attachFilter(); return;
}
} catch(e){ /* ignore and fallback */ }
// 2) 言語指定なしでフォールバック
url = new URL(location.origin + CPT_ENDPOINT);
url.searchParams.set('per_page','12');
url.searchParams.set('_embed','1');
try {
let res = await fetch(url.toString(), {cache:'no-store'});
let items = await res.json();
if (Array.isArray(items) && items.length){
render(items); attachFilter(); return;
}
} catch(e){
console.error('[app-list-ja] fetch error:', e);
}
grid.innerHTML = '<p style="padding:16px;color:#64748b">アプリ投稿がまだありません。</p>';
}
function render(items){
grid.innerHTML = items.map(p => {
const title = p.title?.rendered || 'Untitled';
const link = p.link;
const raw = (p.excerpt?.rendered || p.content?.rendered || '')
.replace(/<[^>]+>/g,'').trim();
const excerpt = raw.length > 140 ? raw.slice(0,140) + '…' : raw;
const media = p._embedded?.['wp:featuredmedia']?.[0];
const thumb = media?.source_url
? `<img src="${media.source_url}" alt="" loading="lazy" decoding="async">`
: `<span>🧩</span>`;
// app_cat タームをバッジ & フィルタ用タグに
const terms = (p._embedded?.['wp:term'] || [])
.flat()
.filter(t => !!t && t.taxonomy === TAXONOMY);
const badges = terms.map(t => `<span class="badge">${t.name}</span>`).join('');
const tags = terms.map(t => t.slug).join(' '); // ← .chip の data-filter と一致
return `
<article class="card" data-tags="${tags}">
<div class="thumb" aria-hidden="true">${thumb}</div>
<div class="inner">
<h2 class="title">${title}</h2>
<div class="meta">App</div>
<div class="badges">${badges}</div>
<p class="desc">${excerpt}</p>
<div class="btns">
<a class="btn primary" href="${link}">開く</a>
<a class="btn ghost" href="${link}">詳細</a>
</div>
</div>
</article>`;
}).join('');
}
function attachFilter(){
const apply = ()=>{
const kw = (q?.value || '').toLowerCase();
const active = chips.find(c => c.classList.contains('active'))?.dataset.filter || 'all';
Array.from(grid.children).forEach(card=>{
const text = card.innerText.toLowerCase();
const tagStr = card.getAttribute('data-tags') || '';
const okKw = !kw || text.includes(kw);
const okCat = active === 'all' || tagStr.includes(active);
card.style.display = (okKw && okCat) ? '' : 'none';
});
};
q?.addEventListener('input', apply);
chips.forEach(chip => chip.addEventListener('click', ()=>{
chips.forEach(c=>c.classList.remove('active'));
chip.classList.add('active');
apply();
}));
}
})();
4. アプリ投稿の作り方(運用)
- 管理画面 → マイ Apps → 新規Appを追加
- 入力推奨
- タイトル(カードの見出し)
- アイキャッチ画像(カードのサムネ)
- 抜粋(説明文。未入力なら本文の先頭を自動抽出)
- app_cat(カテゴリを最低1つ。スラッグはフィルタと一致させる)
- 公開すると
/apptop/
の一覧に自動で反映されます
5. よくある質問・トラブル
- 何も出ない/404
→https://サイト/wp-json/wp/v2/app?per_page=5&_embed=1
を開いてJSONが返るか確認
→ 投稿タイプキーがapp
か、REST API 有効か確認 - カテゴリフィルタが効かない
→ フィルタのdata-filter
とapp_cat
のスラッグが一致しているか確認(study
など) - 多言語対応(Polylang など)
→ 上記JSはまずlang=ja
で取得、0件なら言語なしでフォールバック
→ 英語版を別ページに作る場合は ID をapp-list-en / app-q-en / app-grid-en
に変え、
JS のLANG = 'en'
で同様に動きます(CSSはセレクタを#app-list-en
に変更) - CSP“eval をブロック”警告
→ セキュリティ上のブロックで、通常は無視してOK(テーマやプラグインの内部挙動) - IDの重複警告
→ 検索ボックスのid
をページごとに変える(app-q-ja
/app-q-en
など)
6. ちょい足し(必要に応じて)
- 表示件数の変更:JS 内
per_page
を調整(例:24) - サムネイルの無い投稿の絵文字:
🧩
をお好みに変更 - 抜粋の最大文字数:
140
を変更
まとめ
- CPT=app / TAX=app_cat を用意し、
- 固定ページに HTML枠 を置き、
- CSS/JS を固定配置 するだけ。
以後はアプリ投稿を公開するだけでカードが自動生成され、検索&カテゴリフィルタにも対応。
運用が一気にラクになります。
コメント