HP作成開発「記事投稿(カスタム投稿)」から“アプリ一覧”を自動生成する手順

HP開発日記

「アプリ紹介」を毎回カードを手打ちせず、
WordPress の投稿データ(CPT=app)を REST API で取得→きれいなカードに自動整形する方法です。ChatGPT有効活用してわからないところを都度聞いていきます。トライアンドエラーの繰り返しで完成。
以下の手順をそのままコピペ
で導入できます。


ゴール

  • 固定ページ /apptop/ に「アプリ一覧」を表示
  • 検索(キーワード)+カテゴリフィルタ(チップ)付き
  • 新しいアプリ投稿(CPT=app)を公開するだけで自動で一覧に反映
  • 日本語ページ用(多言語を使う場合は英語版も簡単に増やせます)

0. 事前準備(1回だけ)

  1. **カスタム投稿タイプ(CPT)**を用意
     例:
     - 投稿タイプキー:app
     - ラベル:Apps(お好みで)
     - REST API 有効化(ON
  2. **タクソノミー(カテゴリ)**を用意
     例:
     - タクソノミーキー:app_cat
     - app に紐付け
     - REST API 有効化(ON
     - 代表的なスラッグを作成(例)
      – study(学習)、tool(ツール)、game(ゲーム/クイズ)

CPT/UI は「Custom Post Type UI」などのプラグイン or functions.php で作成してOK。

  1. 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-filterapp_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 を固定配置 するだけ。
    以後はアプリ投稿を公開するだけでカードが自動生成され、検索&カテゴリフィルタにも対応。
    運用が一気にラクになります。

コメント