カスタムHTMLコード

<!--
  WordPressの「カスタムHTML」ブロックに、このファイルの内容を貼り付けてください。

  データ元の切り替え:
  - HTML内データ: data-source="html"
  - 外部CSV:      data-source="csv" data-csv-url="CSVのURL"

  data-detail-urlには、個別表示用に作成したWordPress固定ページのURLを指定します。
  CSVの1行目の項目名は、各thのdata-keyと一致させてください。
-->
<div class="wpdv" data-wp-dataviewer="" data-source="html" data-csv-url="https://amanogawaginga.jp/wp-content/uploads/products.csv" data-detail-url="https://amanogawaginga.jp/product-detail/" data-detail-key="id" data-detail-param="id" data-page-size="6" data-page-sizes="3,6,12,24">
  <div class="wpdv__controls">
    <label class="wpdv__search">
      <span class="wpdv__label">キーワード検索</span>
      <input type="search" data-wpdv-search="" placeholder="商品名・カテゴリーなど">
    </label>

    <label class="wpdv__page-size">
      <span class="wpdv__label">表示件数</span>
      <select data-wpdv-page-size=""></select>
    </label>

    <div class="wpdv__view-switch" role="group" aria-label="表示形式">
      <button type="button" data-wpdv-view="list" aria-pressed="true">リスト</button>
      <button type="button" data-wpdv-view="tile" aria-pressed="false">タイル</button>
    </div>
  </div>

  <p class="wpdv__status" data-wpdv-status="" aria-live="polite"></p>
  <div class="wpdv__content" data-wpdv-content=""></div>
  <nav class="wpdv__pagination" data-wpdv-pagination="" aria-label="ページ送り"></nav>

  <!--
    この非表示テーブルが、列設定とHTMLモードの元データです。

    data-type:
      string / number / date / image / link

    data-sortable:
      "false"にすると並べ替え対象外

    data-searchable:
      "false"にするとキーワード検索対象外

    data-tile-role:
      タイル表示での役割。image / title / meta / body を指定
  -->
  <table class="wpdv__source" data-wpdv-source="" hidden="">
    <thead>
      <tr>
        <th data-key="id" data-type="string" data-tile-role="meta">商品ID</th>
        <th data-key="name" data-type="string" data-tile-role="title">商品名</th>
        <th data-key="category" data-type="string" data-tile-role="meta">カテゴリー</th>
        <th data-key="price" data-type="number" data-tile-role="meta">価格</th>
        <th data-key="description" data-type="string" data-sortable="false" data-tile-role="body">説明</th>
        <th data-key="image" data-type="image" data-sortable="false" data-searchable="false" data-tile-role="image">画像</th>
        <th data-key="url" data-type="detail-link" data-sortable="false" data-searchable="false" data-tile-role="meta">詳細</th>
      </tr>
    </thead>
    <tbody>
      <tr><td>P001</td><td>有機ブレンドコーヒー</td><td>飲料</td><td>1280</td><td>香り豊かな中深煎りのブレンドです。</td><td>https://picsum.photos/seed/coffee/720/480</td><td>https://amanogawaginga.jp/products/p001</td></tr>
      <tr><td>P002</td><td>瀬戸内レモンティー</td><td>飲料</td><td>980</td><td>爽やかなレモンの香りを楽しめます。</td><td>https://picsum.photos/seed/lemontea/720/480</td><td>https://amanogawaginga.jp/products/p002</td></tr>
      <tr><td>P003</td><td>手織りコットンタオル</td><td>生活雑貨</td><td>1650</td><td>やわらかな肌触りの国産タオルです。</td><td>https://picsum.photos/seed/towel/720/480</td><td>https://amanogawaginga.jp/products/p003</td></tr>
      <tr><td>P004</td><td>真鍮デスクトレイ</td><td>文具</td><td>3200</td><td>小物を美しくまとめるデスクトレイです。</td><td>https://picsum.photos/seed/tray/720/480</td><td>https://amanogawaginga.jp/products/p004</td></tr>
      <tr><td>P005</td><td>国産蜂蜜</td><td>食品</td><td>2450</td><td>季節の花から採れたまろやかな蜂蜜です。</td><td>https://picsum.photos/seed/honey/720/480</td><td>https://amanogawaginga.jp/products/p005</td></tr>
      <tr><td>P006</td><td>木製カードスタンド</td><td>文具</td><td>750</td><td>無垢材を使ったシンプルなカード立てです。</td><td>https://picsum.photos/seed/cardstand/720/480</td><td>https://amanogawaginga.jp/products/p006</td></tr>
      <tr><td>P007</td><td>和紅茶ティーバッグ</td><td>飲料</td><td>1180</td><td>渋みが少なく甘い香りの和紅茶です。</td><td>https://picsum.photos/seed/blacktea/720/480</td><td>https://amanogawaginga.jp/products/p007</td></tr>
      <tr><td>P008</td><td>帆布ポーチ</td><td>服飾雑貨</td><td>2100</td><td>丈夫な帆布で作った収納ポーチです。</td><td>https://picsum.photos/seed/pouch/720/480</td><td>https://amanogawaginga.jp/products/p008</td></tr>
      <tr><td>P009</td><td>陶器のマグカップ</td><td>食器</td><td>2800</td><td>手になじむ形の落ち着いたマグカップです。</td><td>https://picsum.photos/seed/mug/720/480</td><td>https://amanogawaginga.jp/products/p009</td></tr>
      <tr><td>P010</td><td>スパイスクッキー</td><td>食品</td><td>860</td><td>数種類のスパイスを使った焼き菓子です。</td><td>https://picsum.photos/seed/cookie/720/480</td><td>https://amanogawaginga.jp/products/p010</td></tr>
      <tr><td>P011</td><td>リネンハンカチ</td><td>服飾雑貨</td><td>1350</td><td>使うほどになじむリネン素材です。</td><td>https://picsum.photos/seed/linen/720/480</td><td>https://amanogawaginga.jp/products/p011</td></tr>
      <tr><td>P012</td><td>ガラスの一輪挿し</td><td>生活雑貨</td><td>3600</td><td>窓辺に似合う小さなガラス花器です。</td><td>https://picsum.photos/seed/vase/720/480</td><td>https://amanogawaginga.jp/products/p012</td></tr>
    </tbody>
  </table>
</div>

<style>
  .wpdv {
    --wpdv-accent: #176b5d;
    --wpdv-accent-soft: #e7f2ef;
    --wpdv-border: #d9dfdd;
    --wpdv-muted: #63706d;
    --wpdv-surface: #fff;
    color: #17211f;
    font-family: inherit;
  }

  .wpdv *,
  .wpdv *::before,
  .wpdv *::after {
    box-sizing: border-box;
  }

  .wpdv__controls {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: end;
    margin-bottom: 16px;
    padding: 16px;
    border: 1px solid var(--wpdv-border);
    border-radius: 12px;
    background: #f7f9f8;
  }

  .wpdv__search {
    flex: 1 1 260px;
  }

  .wpdv__page-size {
    flex: 0 0 110px;
  }

  .wpdv__label {
    display: block;
    margin-bottom: 5px;
    color: var(--wpdv-muted);
    font-size: 13px;
    font-weight: 700;
  }

  .wpdv input,
  .wpdv select,
  .wpdv button {
    min-height: 42px;
    border: 1px solid var(--wpdv-border);
    border-radius: 8px;
    background: var(--wpdv-surface);
    color: inherit;
    font: inherit;
  }

  .wpdv input,
  .wpdv select {
    width: 100%;
    padding: 8px 12px;
  }

  .wpdv button {
    cursor: pointer;
    padding: 8px 13px;
  }

  .wpdv button:hover,
  .wpdv button:focus-visible {
    border-color: var(--wpdv-accent);
  }

  .wpdv button:focus-visible,
  .wpdv input:focus-visible,
  .wpdv select:focus-visible {
    outline: 3px solid color-mix(in srgb, var(--wpdv-accent) 25%, transparent);
    outline-offset: 2px;
  }

  .wpdv__view-switch {
    display: flex;
    gap: 4px;
  }

  .wpdv__view-switch button[aria-pressed="true"],
  .wpdv__pagination button[aria-current="page"] {
    border-color: var(--wpdv-accent);
    background: var(--wpdv-accent);
    color: #fff;
  }

  .wpdv__status {
    margin: 0 0 10px;
    color: var(--wpdv-muted);
    font-size: 14px;
  }

  .wpdv__table-wrap {
    overflow-x: auto;
    border: 1px solid var(--wpdv-border);
    border-radius: 12px;
  }

  .wpdv__table {
    width: 100%;
    min-width: 760px;
    margin: 0;
    border-collapse: collapse;
    background: var(--wpdv-surface);
  }

  .wpdv__table th,
  .wpdv__table td {
    padding: 12px;
    border: 0;
    border-bottom: 1px solid var(--wpdv-border);
    text-align: left;
    vertical-align: middle;
  }

  .wpdv__table tbody tr:last-child td {
    border-bottom: 0;
  }

  .wpdv__table th {
    background: #f3f6f5;
    white-space: nowrap;
  }

  .wpdv__sort {
    display: inline-flex;
    gap: 6px;
    align-items: center;
    min-height: auto !important;
    padding: 2px !important;
    border: 0 !important;
    border-radius: 3px !important;
    background: transparent !important;
    font-weight: 700;
  }

  .wpdv__sort-mark {
    min-width: 1em;
    color: var(--wpdv-accent);
  }

  .wpdv__thumb {
    display: block;
    width: 72px;
    height: 54px;
    border-radius: 7px;
    object-fit: cover;
    background: #edf0ef;
  }

  .wpdv__tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 235px), 1fr));
    gap: 16px;
  }

  .wpdv__card {
    overflow: hidden;
    border: 1px solid var(--wpdv-border);
    border-radius: 14px;
    background: var(--wpdv-surface);
    box-shadow: 0 5px 18px rgb(30 50 45 / 7%);
  }

  .wpdv__card-image {
    width: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
    background: #edf0ef;
  }

  .wpdv__card-body {
    padding: 15px;
  }

  .wpdv__card-title {
    margin: 0 0 9px;
    font-size: 18px;
    line-height: 1.4;
  }

  .wpdv__card-meta,
  .wpdv__card-text {
    margin: 6px 0 0;
    font-size: 14px;
    line-height: 1.65;
  }

  .wpdv__card-meta {
    color: var(--wpdv-muted);
  }

  .wpdv__field-label {
    font-weight: 700;
  }

  .wpdv__link {
    color: var(--wpdv-accent);
    font-weight: 700;
  }

  .wpdv__empty,
  .wpdv__error {
    padding: 28px;
    border: 1px dashed var(--wpdv-border);
    border-radius: 12px;
    text-align: center;
  }

  .wpdv__error {
    color: #a12626;
    background: #fff6f6;
  }

  .wpdv__pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: center;
    margin-top: 20px;
  }

  .wpdv__pagination button {
    min-width: 42px;
    padding-inline: 10px;
  }

  .wpdv__pagination button:disabled {
    cursor: not-allowed;
    opacity: 0.45;
  }

  @media (max-width: 600px) {
    .wpdv__controls {
      align-items: stretch;
    }

    .wpdv__page-size,
    .wpdv__view-switch {
      flex: 1 1 120px;
    }

    .wpdv__view-switch button {
      flex: 1;
    }
  }
</style>

<script>
(() => {
  "use strict";

  const viewerSelector = "[data-wp-dataviewer]";

  function parseCsv(text) {
    const rows = [];
    let row = [];
    let field = "";
    let quoted = false;

    text = text.replace(/^\uFEFF/, "");

    for (let i = 0; i < text.length; i += 1) {
      const char = text[i];
      const next = text[i + 1];

      if (char === '"' && quoted && next === '"') {
        field += '"';
        i += 1;
      } else if (char === '"') {
        quoted = !quoted;
      } else if (char === "," && !quoted) {
        row.push(field);
        field = "";
      } else if ((char === "\n" || char === "\r") && !quoted) {
        if (char === "\r" && next === "\n") i += 1;
        row.push(field);
        if (row.some((value) => value !== "")) rows.push(row);
        row = [];
        field = "";
      } else {
        field += char;
      }
    }

    row.push(field);
    if (row.some((value) => value !== "")) rows.push(row);
    return rows;
  }

  function normalize(value, type) {
    if (type === "number") {
      const number = Number(String(value).replace(/[,¥¥円\s]/g, ""));
      return Number.isNaN(number) ? Number.NEGATIVE_INFINITY : number;
    }
    if (type === "date") {
      const time = new Date(value).getTime();
      return Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time;
    }
    return String(value ?? "").toLocaleLowerCase("ja");
  }

  function formatValue(value, type) {
    if (type === "number" && value !== "") {
      const number = Number(String(value).replace(/[,¥¥円\s]/g, ""));
      return Number.isNaN(number) ? value : number.toLocaleString("ja-JP");
    }
    return value;
  }

  function makeElement(tag, className, text) {
    const element = document.createElement(tag);
    if (className) element.className = className;
    if (text !== undefined) element.textContent = text;
    return element;
  }

  function safeUrl(value) {
    try {
      const url = new URL(value, document.baseURI);
      return ["http:", "https:"].includes(url.protocol) ? url.href : "";
    } catch {
      return "";
    }
  }

  function makeDetailUrl(baseUrl, param, id) {
    const safeBaseUrl = safeUrl(baseUrl);
    if (!safeBaseUrl || !id) return "";
    const url = new URL(safeBaseUrl);
    url.searchParams.set(param || "id", id);
    return url.href;
  }

  class WordPressDataViewer {
    constructor(root) {
      this.root = root;
      this.sourceTable = root.querySelector("[data-wpdv-source]");
      this.content = root.querySelector("[data-wpdv-content]");
      this.status = root.querySelector("[data-wpdv-status]");
      this.pagination = root.querySelector("[data-wpdv-pagination]");
      this.searchInput = root.querySelector("[data-wpdv-search]");
      this.pageSizeSelect = root.querySelector("[data-wpdv-page-size]");
      this.viewButtons = [...root.querySelectorAll("[data-wpdv-view]")];
      this.state = {
        rows: [],
        keyword: "",
        sortKey: null,
        sortDirection: "asc",
        currentPage: 1,
        pageSize: Number(root.dataset.pageSize) || 12,
        view: "list"
      };
    }

    async init() {
      if (!this.sourceTable || !this.content) {
        throw new Error("元データ用tableまたは表示領域がありません。");
      }

      this.columns = [...this.sourceTable.querySelectorAll("thead th")].map((th, index) => ({
        index,
        key: th.dataset.key || `column${index + 1}`,
        label: th.textContent.trim(),
        type: th.dataset.type || "string",
        sortable: th.dataset.sortable !== "false",
        searchable: th.dataset.searchable !== "false",
        tileRole: th.dataset.tileRole || "meta"
      }));

      this.setupControls();
      this.bindEvents();
      this.setLoading(true);

      if (this.root.dataset.source === "csv") {
        this.state.rows = await this.loadCsv(this.root.dataset.csvUrl);
      } else {
        this.state.rows = this.loadHtml();
      }

      this.setLoading(false);
      this.render();
    }

    setupControls() {
      const sizes = (this.root.dataset.pageSizes || "12,24,48")
        .split(",")
        .map(Number)
        .filter((size) => Number.isInteger(size) && size > 0);

      if (!sizes.includes(this.state.pageSize)) sizes.unshift(this.state.pageSize);
      [...new Set(sizes)].forEach((size) => {
        const option = new Option(`${size}件`, String(size), false, size === this.state.pageSize);
        this.pageSizeSelect.add(option);
      });
    }

    bindEvents() {
      this.searchInput.addEventListener("input", (event) => {
        this.state.keyword = event.target.value.trim().toLocaleLowerCase("ja");
        this.state.currentPage = 1;
        this.render();
      });

      this.pageSizeSelect.addEventListener("change", (event) => {
        this.state.pageSize = Number(event.target.value);
        this.state.currentPage = 1;
        this.render();
      });

      this.viewButtons.forEach((button) => {
        button.addEventListener("click", () => {
          this.state.view = button.dataset.wpdvView;
          this.viewButtons.forEach((item) => {
            item.setAttribute("aria-pressed", String(item === button));
          });
          this.renderContent(this.getPageRows());
        });
      });

      this.content.addEventListener("click", (event) => {
        const button = event.target.closest("[data-wpdv-sort]");
        if (!button) return;
        const key = button.dataset.wpdvSort;
        this.state.sortDirection =
          this.state.sortKey === key && this.state.sortDirection === "asc" ? "desc" : "asc";
        this.state.sortKey = key;
        this.state.currentPage = 1;
        this.render();
      });

      this.pagination.addEventListener("click", (event) => {
        const button = event.target.closest("[data-wpdv-page]");
        if (!button || button.disabled) return;
        this.state.currentPage = Number(button.dataset.wpdvPage);
        this.render();
        this.status.scrollIntoView({ behavior: "smooth", block: "nearest" });
      });
    }

    loadHtml() {
      return [...this.sourceTable.querySelectorAll("tbody tr")].map((tr) => {
        const cells = [...tr.children];
        return Object.fromEntries(
          this.columns.map((column) => [column.key, cells[column.index]?.textContent.trim() || ""])
        );
      });
    }

    async loadCsv(url) {
      if (!url) throw new Error("data-csv-urlにCSVのURLを指定してください。");
      const response = await fetch(url, { credentials: "same-origin" });
      if (!response.ok) throw new Error(`CSVを取得できませんでした(${response.status})。`);

      const csvRows = parseCsv(await response.text());
      const headers = csvRows.shift()?.map((header) => header.trim()) || [];
      return csvRows.map((values) =>
        Object.fromEntries(this.columns.map((column) => [column.key, values[headers.indexOf(column.key)] || ""]))
      );
    }

    setLoading(loading) {
      if (loading) {
        this.status.textContent = "データを読み込んでいます…";
        this.content.setAttribute("aria-busy", "true");
      } else {
        this.content.removeAttribute("aria-busy");
      }
    }

    getFilteredRows() {
      let rows = this.state.rows;

      if (this.state.keyword) {
        rows = rows.filter((row) =>
          this.columns.some(
            (column) =>
              column.searchable &&
              String(row[column.key] ?? "").toLocaleLowerCase("ja").includes(this.state.keyword)
          )
        );
      }

      if (this.state.sortKey) {
        const column = this.columns.find((item) => item.key === this.state.sortKey);
        const direction = this.state.sortDirection === "asc" ? 1 : -1;
        rows = [...rows].sort((a, b) => {
          const aValue = normalize(a[column.key], column.type);
          const bValue = normalize(b[column.key], column.type);
          if (typeof aValue === "number") return (aValue - bValue) * direction;
          return aValue.localeCompare(bValue, "ja", { numeric: true }) * direction;
        });
      }

      return rows;
    }

    getPageRows() {
      const filtered = this.getFilteredRows();
      const totalPages = Math.max(1, Math.ceil(filtered.length / this.state.pageSize));
      this.state.currentPage = Math.min(this.state.currentPage, totalPages);
      const start = (this.state.currentPage - 1) * this.state.pageSize;
      return filtered.slice(start, start + this.state.pageSize);
    }

    render() {
      const filtered = this.getFilteredRows();
      const totalPages = Math.max(1, Math.ceil(filtered.length / this.state.pageSize));
      this.state.currentPage = Math.min(this.state.currentPage, totalPages);
      const start = (this.state.currentPage - 1) * this.state.pageSize;
      const pageRows = filtered.slice(start, start + this.state.pageSize);

      this.status.textContent = `${filtered.length.toLocaleString("ja-JP")}件中 ${
        filtered.length ? start + 1 : 0
      }〜${Math.min(start + this.state.pageSize, filtered.length)}件を表示`;

      this.renderContent(pageRows);
      this.renderPagination(totalPages);
    }

    renderContent(rows) {
      this.content.replaceChildren();
      if (!rows.length) {
        this.content.append(makeElement("p", "wpdv__empty", "該当するデータがありません。"));
        return;
      }

      if (this.state.view === "tile") {
        this.renderTiles(rows);
      } else {
        this.renderList(rows);
      }
    }

    renderList(rows) {
      const wrap = makeElement("div", "wpdv__table-wrap");
      const table = makeElement("table", "wpdv__table");
      const thead = document.createElement("thead");
      const headRow = document.createElement("tr");

      this.columns.forEach((column) => {
        const th = document.createElement("th");
        th.scope = "col";
        if (this.state.sortKey === column.key) {
          th.setAttribute(
            "aria-sort",
            this.state.sortDirection === "asc" ? "ascending" : "descending"
          );
        }
        if (column.sortable) {
          const button = makeElement("button", "wpdv__sort");
          button.type = "button";
          button.dataset.wpdvSort = column.key;
          button.setAttribute("aria-label", `${column.label}で並べ替え`);
          button.append(makeElement("span", "", column.label));
          const mark =
            this.state.sortKey === column.key
              ? this.state.sortDirection === "asc"
                ? "\u25B2"
                : "\u25BC"
              : "\u2195";
          button.append(makeElement("span", "wpdv__sort-mark", mark));
          th.append(button);
        } else {
          th.textContent = column.label;
        }
        headRow.append(th);
      });

      thead.append(headRow);
      table.append(thead);
      const tbody = document.createElement("tbody");

      rows.forEach((row) => {
        const tr = document.createElement("tr");
        this.columns.forEach((column) => {
          const td = document.createElement("td");
          td.append(this.renderValue(row[column.key], column, row));
          tr.append(td);
        });
        tbody.append(tr);
      });

      table.append(tbody);
      wrap.append(table);
      this.content.append(wrap);
    }

    renderTiles(rows) {
      const tiles = makeElement("div", "wpdv__tiles");
      const titleColumn = this.columns.find((column) => column.tileRole === "title") || this.columns[0];
      const imageColumn = this.columns.find((column) => column.tileRole === "image");

      rows.forEach((row) => {
        const card = makeElement("article", "wpdv__card");

        const imageUrl = imageColumn ? safeUrl(row[imageColumn.key]) : "";
        if (imageUrl) {
          const image = makeElement("img", "wpdv__card-image");
          image.src = imageUrl;
          image.alt = row[titleColumn.key] || imageColumn.label;
          image.loading = "lazy";
          card.append(image);
        }

        const body = makeElement("div", "wpdv__card-body");
        body.append(makeElement("h3", "wpdv__card-title", row[titleColumn.key]));

        this.columns
          .filter((column) => column !== titleColumn && column !== imageColumn)
          .forEach((column) => {
            if (!row[column.key]) return;
            const line = makeElement(
              "p",
              column.tileRole === "body" ? "wpdv__card-text" : "wpdv__card-meta"
            );
            line.append(makeElement("span", "wpdv__field-label", `${column.label}: `));
            line.append(this.renderValue(row[column.key], column, row));
            body.append(line);
          });

        card.append(body);
        tiles.append(card);
      });

      this.content.append(tiles);
    }

    renderValue(value, column, row) {
      if (column.type === "image") {
        const imageUrl = safeUrl(value);
        if (!imageUrl) return document.createTextNode("");
        const image = makeElement("img", "wpdv__thumb");
        image.src = imageUrl;
        image.alt =
          row[this.columns.find((item) => item.tileRole === "title")?.key] || column.label;
        image.loading = "lazy";
        return image;
      }

      if (column.type === "link" && value) {
        const linkUrl = safeUrl(value);
        if (!linkUrl) return document.createTextNode("");
        const link = makeElement("a", "wpdv__link", "詳細を見る");
        link.href = linkUrl;
        return link;
      }

      if (column.type === "detail-link") {
        const detailKey = this.root.dataset.detailKey || "id";
        const detailUrl = makeDetailUrl(
          this.root.dataset.detailUrl,
          this.root.dataset.detailParam,
          row[detailKey]
        );
        if (!detailUrl) return document.createTextNode("");
        const link = makeElement("a", "wpdv__link", "詳細を見る");
        link.href = detailUrl;
        return link;
      }

      return document.createTextNode(formatValue(value, column.type));
    }

    renderPagination(totalPages) {
      this.pagination.replaceChildren();
      if (totalPages <= 1) return;

      const addButton = (label, page, options = {}) => {
        const button = makeElement("button", "", label);
        button.type = "button";
        button.dataset.wpdvPage = page;
        button.disabled = Boolean(options.disabled);
        if (options.current) button.setAttribute("aria-current", "page");
        if (options.label) button.setAttribute("aria-label", options.label);
        this.pagination.append(button);
      };

      addButton("前へ", this.state.currentPage - 1, {
        disabled: this.state.currentPage === 1
      });

      const start = Math.max(1, this.state.currentPage - 2);
      const end = Math.min(totalPages, start + 4);
      for (let page = Math.max(1, end - 4); page <= end; page += 1) {
        addButton(String(page), page, {
          current: page === this.state.currentPage,
          label: `${page}ページ目`
        });
      }

      addButton("次へ", this.state.currentPage + 1, {
        disabled: this.state.currentPage === totalPages
      });
    }
  }

  async function initialize(root) {
    if (root.dataset.wpdvInitialized === "true") return;
    root.dataset.wpdvInitialized = "true";
    try {
      await new WordPressDataViewer(root).init();
    } catch (error) {
      const content = root.querySelector("[data-wpdv-content]");
      const message = makeElement("p", "wpdv__error", `表示できませんでした: ${error.message}`);
      content?.replaceChildren(message);
      console.error(error);
    }
  }

  document.querySelectorAll(viewerSelector).forEach(initialize);
})();
</script>

CSVサンプル

id,name,category,price,description,image,url
P001,有機ブレンドコーヒー,飲料,1280,香り豊かな中深煎りのブレンドです。,https://picsum.photos/seed/coffee/720/480,https://amanogawaginga.jp/products/p001
P002,瀬戸内レモンティー,飲料,980,爽やかなレモンの香りを楽しめます。,https://picsum.photos/seed/lemontea/720/480,https://amanogawaginga.jp/products/p002
P003,手織りコットンタオル,生活雑貨,1650,やわらかな肌触りの国産タオルです。,https://picsum.photos/seed/towel/720/480,https://amanogawaginga.jp/products/p003
P004,真鍮デスクトレイ,文具,3200,小物を美しくまとめるデスクトレイです。,https://picsum.photos/seed/tray/720/480,https://amanogawaginga.jp/products/p004
P005,国産蜂蜜,食品,2450,季節の花から採れたまろやかな蜂蜜です。,https://picsum.photos/seed/honey/720/480,https://amanogawaginga.jp/products/p005
P006,木製カードスタンド,文具,750,無垢材を使ったシンプルなカード立てです。,https://picsum.photos/seed/cardstand/720/480,https://amanogawaginga.jp/products/p006
P007,和紅茶ティーバッグ,飲料,1180,渋みが少なく甘い香りの和紅茶です。,https://picsum.photos/seed/blacktea/720/480,https://amanogawaginga.jp/products/p007
P008,帆布ポーチ,服飾雑貨,2100,丈夫な帆布で作った収納ポーチです。,https://picsum.photos/seed/pouch/720/480,https://amanogawaginga.jp/products/p008
P009,陶器のマグカップ,食器,2800,手になじむ形の落ち着いたマグカップです。,https://picsum.photos/seed/mug/720/480,https://amanogawaginga.jp/products/p009
P010,スパイスクッキー,食品,860,数種類のスパイスを使った焼き菓子です。,https://picsum.photos/seed/cookie/720/480,https://amanogawaginga.jp/products/p010
P011,リネンハンカチ,服飾雑貨,1350,使うほどになじむリネン素材です。,https://picsum.photos/seed/linen/720/480,https://amanogawaginga.jp/products/p011
P012,ガラスの一輪挿し,生活雑貨,3600,窓辺に似合う小さなガラス花器です。,https://picsum.photos/seed/vase/720/480,https://amanogawaginga.jp/products/p012

WordPress データ一覧の使い方

WordPress データ一覧の使い方

構成

・一覧ページ: wordpress-data-viewer.html
・個別ページ: wordpress-data-detail.html
・CSVサンプル: products-sample.csv

1. HTML内データを使う場合

「wordpress-data-viewer.html」の内容をWordPressのカスタムHTMLブロックへ貼り付けます。

先頭付近の設定は次のままにします。

data-source="html"

非表示テーブルのtbody内に、表示したいデータを追加してください。
個別ページにも同じデータを設定してください。

2. 外部CSVを使う場合

CSVファイルをWordPressと同じドメイン内へアップロードします。

「wordpress-data-viewer.html」先頭付近の設定を変更します。

data-source="csv"
data-csv-url="アップロードしたCSVのURL"

CSVの1行目の項目名は、非表示テーブル内の各thにあるdata-keyと一致させます。
CSVモードでは、非表示テーブルのtbody内のデータは使用されません。
一覧ページと個別ページの両方に同じCSVのURLを指定します。

3. 列を変更する場合

非表示テーブルのthead内にあるthを追加・削除します。

data-key CSVの項目名
data-type string / number / date / image / link
data-sortable falseにすると並べ替え不可
data-searchable falseにするとキーワード検索対象外
data-tile-role image / title / meta / body

4. 表示件数を変更する場合

data-page-size="6" 最初に表示する件数
data-page-sizes="3,6,12,24" 選択肢として表示する件数

5. 個別ページを作る場合

WordPressで「商品詳細」などの固定ページを1枚作り、
「wordpress-data-detail.html」の内容をカスタムHTMLブロックへ貼り付けます。

固定ページのURLが次の場合:

一覧ページの設定を次のように変更します。

data-detail-url="https://amanogawaginga.jp/product-detail/"
data-detail-key="id"
data-detail-param="id"

一覧の「詳細を見る」をクリックすると、次の形式で個別ページが開きます。

個別ページ側の設定:

data-id-key="id" IDが入る項目名
data-id-param="id" URLで使うパラメーター名
data-list-url="一覧ページのURL" 「一覧へ戻る」のリンク先

6. 個別ページの表示項目を変更する場合

個別ページ内の非表示テーブルにあるthのdata-detail-roleを変更します。

title ページタイトル
image メイン画像
meta 商品ID・価格などの基本情報
body 本文
link 外部リンクボタン
hidden 個別ページには表示しない

data-prefix・data-suffixで、値の前後へ文字を追加できます。

例:

data-suffix="円"

注意点

・CSVは同じWordPressサイト内に置くのが簡単です。別ドメインの場合は、配信側のCORS設定が必要です。
・WordPressの権限やセキュリティ設定によっては、カスタムHTMLブロック内のscriptタグが削除されます。
その場合は、管理者権限で投稿するか、JavaScript部分をテーマ・子テーマ・コード追加用プラグインへ配置してください。
・WordPressは標準状態でCSVアップロードを許可しない場合があります。その場合はメディア設定または専用プラグインでCSVを許可してください。