[WordPress]RestAPI#2 エンドポイント追加

functions.php

/**
 * RestAPIエンドポイント追加
 */
function custom_register_rest_routes() {
    /* カテゴリ一覧 */
    register_rest_route('custom/v1', '/categories/', [
        'methods'  => 'GET',
        'callback' => 'custom_callback_categories',
        'permission_callback' => '__return_true' // 認証不要
    ]);
}
add_action('rest_api_init', 'custom_register_rest_routes');

/**
 * カテゴリ一覧取得
 *
 * @return [type]
 * 
 */
function custom_callback_categories($request) {
    $wpt = new KtyrReport2();
    return new WP_REST_Response([
        'terms' => $wpt->get_terms(),
    ], 200);
}

[React][TypeScript]生のhtmlを返す

npm install dompurify

 

import DOMPurify from "dompurify";

const App= () => {
    const rawHtml = `
    <div>
      <h1 style="color: blue;">Hello, World!</h1>
      <p>これは複数行のHTMLです。</p>
      <ul>
        <li>リスト1</li>
        <li>リスト2</li>
      </ul>
    </div>
    `;
    const cleanHtml = DOMPurify.sanitize(rawHtml); //安全なhtmlへ変換
    return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
};

export default App;

 
dangerouslySetInnerHTMLを使う場合Fragment (<> </>) は使えない

[Vite][TypeScript]出力ファイル名を指定

vite.config.tsのdefineConfigに下記追記

build: {
  rollupOptions: {
    output: {
      entryFileNames: 'assets/[name].js', // エントリーポイントのファイル名
      chunkFileNames: 'assets/[name].js', // チャンクファイル名
      assetFileNames: 'assets/[name].[ext]', // アセットファイル名
    },
  },
},

[TailwindCSS]@apply

@applyでTailwindCSSのユーティリティクラスをそのまま使える
css

@import "tailwindcss";
article .contents a{
  @apply border-b-1 border-dotted border-gray-400 hover:border-gray-800;
}

 
@layer componentsを使うと、TailwindCSSのコンポーネントスタイルとして追加できる。
css

@import "tailwindcss";
@layer components {
  .btn-custom {
    @apply bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition;
  }
}

 
html

<button className="btn-custom">カスタムボタン</button>

[WordPress][TypeScript]RestAPI#1

functions.php
cf_cat_image:画像タイプのカスタムフィールド
cf_cat_disabled:booleanタイプのカスタムフィールド
/wp-json/wp/v2/posts/のデータにカテゴリの詳細情報を追加する

function add_category_details_to_posts($data, $post, $context) {
    $categories = get_the_category($post->ID);
    $category_details = [];

    foreach ($categories as $category) {
        $category_meta = get_term_meta($category->term_id); // カスタムフィールド取得
        $image_id = get_term_meta($category->term_id, 'cf_cat_image', true);
        $category_details[] = [
            'id' => $category->term_id,
            'name' => $category->name,
            'slug' => $category->slug,
            'cf_cat_image' => empty($image_id) ? '' : wp_get_attachment_url($image_id),
            'cf_cat_disabled' => get_term_meta($category->term_id, 'cf_cat_disabled', true),
        ];
    }
    
    $data->data['category_details'] = $category_details;
    return $data;
}
add_filter('rest_prepare_post', 'add_category_details_to_posts', 10, 3);

 
WordPressPosts.tsx

import { useEffect, useState } from 'react';

// 投稿データの型定義
interface CategoryDetail {
  id: number;
  name: string;
  slug: string;
  cf_cat_image: string;
  cf_cat_disabled: "0" | "1"; // 無効フラグが "0" または "1" の文字列
}
interface Post {
  id: number;
  date: string;
  title: { rendered: string };
  content: { rendered: string };
  excerpt: { rendered: string };
  link: string;
  category_details : CategoryDetail[];
}

const WordPressPosts = () => {
  const [posts, setPosts] = useState<Post[]>([]); // 投稿データ
  const [loading, setLoading] = useState<boolean>(true); // ローディング状態
  const [error, setError] = useState<string | null>(null); // エラーメッセージ

  useEffect(() => {
    const fetchPosts = async () => {
      try {
        const response = await fetch('/wp-json/wp/v2/posts/');
        if (!response.ok) {
          throw new Error('投稿データの取得に失敗しました');
        }
        const data: Post[] = await response.json();
        setPosts(data); // データをステートに保存
      } catch (error) {
        setError(error instanceof Error ? error.message : '不明なエラー');
      } finally {
        setLoading(false); // ローディング終了
      }
    };

    fetchPosts();
  }, []); // 初回レンダリング時のみ実行

  // ローディング中の表示
  if (loading) {
    return <div>Loading...</div>;
  }

  // エラー発生時の表示
  if (error) {
    return <div>Error: {error}</div>;
  }

  return (
    <>
    {posts.map(post => (
      <article className="flex">
        <div className="w-7">
          {post.category_details.map(category_detail => (
            <a className="block" href={"/?c="+category_detail.id}><img src={category_detail.cf_cat_image} /></a>
          ))}
        </div>
        <div className="flex-1">
          <h2 className="entry-title">
            <a href={post.link}>{post.title.rendered}</a>
          </h2>
          <div className="relative">
            <ul className="flex">
              {post.category_details.map(category_detail => (
                <li className=""><a href={"/?c="+category_detail.id}>{category_detail.name}</a></li>
              ))}
            </ul>
            <time className="absolute top-0 right-0" dateTime="">{new Date(post.date).toLocaleDateString("ja-JP", {year: "numeric",month: "2-digit",day: "2-digit"}).replace(/\//g, '-')}</time>
          </div>
          <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
        </div>
      </article>
    ))}
    </>
  );
};

export default WordPressPosts;

 
main.tsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import WordPressPosts from './WordPressPosts.tsx'

createRoot(document.getElementById('main')!).render(
  <StrictMode>
    <WordPressPosts />
  </StrictMode>,
)

 
下記のようなReact.FC使用は最近は非推奨

const WordPressPosts: React.FC = () => {

[React]dangerouslySetInnerHTML

dangerouslySetInnerHTMLを使うと、生のHTMLを埋め込むことができる
サンプル

<div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />

[Laravel Sail]Gmailを使ってメール送信

Googleアカウント設定画面で「アプリパスワード」で検索
アプリ名を入力して作成後、生成したアプリパスワードをMAIL_PASSWORDへ設定
 
.env

MAIL_MAILER=smtp
MAIL_SCHEME=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=sample@gmail.com
MAIL_PASSWORD='xxxx xxxx xxxx xxxx'

 
メール送信処理は下記
[Laravel Sail]メール送信備忘録
 
Mailpitを利用する場合は不要
Mailpit利用時のメールの確認は下記
http://localhost:8025/

[TypeScript][ChatGPT]interfaceを定義するプロンプト

typescriptのinterfaceの定義 対象は下記
"category_details": [
{
"id": 15,
"name": "PHP",
"slug": "php",
"cf_cat_image": "/uploads/icon-15.png",
"cf_cat_disabled": "0"
},
{
"id": 68,
"name": "Laravel",
"slug": "laravel",
"cf_cat_image": "/uploads/20210311100757.png",
"cf_cat_disabled": "0"
}
],

[JavaScript]Utils.js

/**
 * 共通処理
 *
 * @class Utils
 * @typedef {Utils}
 */
class Utils{
    /**
     * Creates an instance of Utils.
     *
     * @constructor
     */
    constructor() {}
    /**
     * ブラウザバック判定
     *
     * @static
     * @readonly
     * @type {boolean}
     */
    static get isBrowserBack(){
        const perfEntries = performance.getEntriesByType("navigation");
        let result = false;
        perfEntries.forEach((perfEntry) => {
            if(perfEntry.type == 'back_forward'){
                result = true;
            }
        });
        return result;
    }
    /**
     * URLが/challengeで終わるか否か
     *
     * @static
     * @readonly
     * @type {*}
     */
    static get isChallenge(){
        return /^.*\/challenge$/.test(location.pathname);
    }
    /**
     * 指定要素の指定イベント発火
     *
     * @static
     * @param {HTMLElement} element
     * @param {String} eventName
     * @returns {Boolean}
     */
    static triggerEvent(element, eventName){
        const evt = new CustomEvent(eventName, {bubbles:true,cancelable:true});
        return element.dispatchEvent(evt);
    }
    /**
     * 指定要素の指定イベント発火(古いバージョン)
     *
     * @static
     * @param {HTMLElement} element
     * @param {String} eventName
     * @returns {Boolean}
     */
    static triggerEventOld(element, eventName){
        const evt = document.createEvent('HTMLEvents')
        evt.initEvent(eventName, true, true);
        return element.dispatchEvent(evt);
    }
    /**
     * イベントキャンセル
     *
     * @static
     * @param {Event} e
     */
    static cancelEvent(e){
        e.stopPropagation();/* キャプチャおよびバブリング段階において現在のイベントのさらなる伝播を阻止します。しかし、これは既定の動作の発生を妨げるものではありません。 */
        e.preventDefault();/* ユーザーエージェントに、このイベントが明示的に処理されない場合に、その既定のアクションを通常どおりに行うべきではないことを伝えます */
        e.stopImmediatePropagation();/* 呼び出されている同じイベントの他のリスナーを抑止します。同じイベントタイプで複数のリスナーが同じ要素に装着されている場合、追加された順番に呼び出されます。もし、そのような呼び出しの最中に stopImmediatePropagation() が呼び出された場合、残りのリスナーは呼び出されなくなります。 */
    }
    /**
     * this.elements初期化
     *
     * @static
     * @param {*} instance
     * @param {*} target
     */
    static initDataElements(instance,target){
        if(!target)target = document;
        instance.elements.html = document.getElementsByTagName('html')[0];
        instance.elements.body = document.getElementsByTagName('body')[0];
        let key;
        for(key in instance.elements.selectors){
            this.updateDataElements(instance,key,target);
        }
    }
    /**
     * this.elements更新
     *
     * @static
     * @param {*} instance
     * @param {*} key
     * @param {*} target
     */
    static updateDataElements(instance,key,target){
        if(!target)target = document;
        const keys = typeof key === 'string' ? [key] : key;
        keys.forEach((_key,i)=>{
            if(instance.elements.selectors[_key]) {
                if (/^.+All$/.test(_key)) {
                    instance.elements[_key] = target.querySelectorAll(instance.elements.selectors[_key]);
                }else{
                    instance.elements[_key] = target.querySelector(instance.elements.selectors[_key]);
                    if (/^.+Clone$/.test(_key)) {
                        instance.elements[_key] = instance.elements[_key].cloneNode(true);
                    }
                }
            }
        });
    }
    /**
     * タグがtagNameと一致するか判定
     *
     * @static
     * @param {HTMLElement} elm
     * @param {*} tagName
     * @param {*} type
     * @returns {boolean}
     */
    static tagNameIs(elm,tagName,type){
        let result = false;
        if(Array.isArray(tagName)){
            result = tagName.includes(elm.tagName.toLowerCase());
        }else{
            result = elm.tagName.toLowerCase() === tagName.toLowerCase();
        }
        if(result && type){
            if(Array.isArray(type)){
                result = type.includes(elm.type.toLowerCase());
            }else{
                result = elm.type.toLocaleLowerCase() === type.toLocaleLowerCase();
            }
        }
        return result;
    }
    /**
     * nameで指定した要素へvalueを設定する
     *
     * @static
     * @param {String} name
     * @param {*} value
     * @param {*} target
     */
    static setValueByName(name,value,target){
        if(!target)target = document;
        const elms = target.querySelectorAll(`[name="${name}"]`);
        elms.forEach((elm,i)=>{
            elm.value = value;
        });
    }
    /**
     * queryで指定した要素へvalueを設定する
     *
     * @static
     * @param {String} query
     * @param {*} value
     * @param {*} target
     */
    static setValueBySelector(query,value,target){
        if(!target)target = document;
        const elms = target.querySelectorAll(query);
        elms.forEach((elm,i)=>{
            elm.value = value;
        });
    }
    /**
     * nodeListで指定した要素へvalueを設定する
     *
     * @static
     * @param {NodeList} nodeList
     * @param {*} value
     */
    static setValueByNodeList(nodeList,value){
        nodeList.forEach((elm,i)=>{
            elm.value = value;
        });
    }
    /**
     * nameで指定した要素のdisabledを設定する
     *
     * @static
     * @param {String} name
     * @param {Boolean} disabled
     * @param {*} target
     */
    static setDisabledByName(name,disabled,target){
        if(!target)target = document;
        const elms = target.querySelectorAll(`[name="${name}"]`);
        elms.forEach((elm,i)=>{
            elm.disabled = disabled;
        });
    }
    /**
     * nameで指定した要素がselectタグかどうか判定
     *
     * @static
     * @param {String} name
     * @param {*} target
     * @returns {boolean}
     */
    static isSelectByName(name,target){
        if(!target)target = document;
        const elms = target.querySelectorAll(`[name="${name}"]`);
        if(elms.length){
            return Utils.tagNameIs(elms[0],'select');
        }
        return false;
    }
    /**
     * nameで指定した要素がcheckboxかどうか判定
     *
     * @static
     * @param {String} name
     * @param {*} target
     * @returns {boolean}
     */
    static isCheckboxByName(name,target){
        if(!target)target = document;
        const elms = target.querySelectorAll(`[name="${name}"]`);
        if(elms.length){
            return Utils.tagNameIs(elms[0],'input','checkbox');
        }
        return false;
    }
    /**
     * nameで指定した要素がradioかどうか判定
     *
     * @static
     * @param {String} name
     * @param {*} target
     * @returns {boolean}
     */
    static isRadioByName(name,target){
        if(!target)target = document;
        const elms = target.querySelectorAll(`[name="${name}"]`);
        if(elms.length){
            return Utils.tagNameIs(elms[0],'input','radio');
        }
        return false;
    }
    /**
     * nameで指定した要素のvalueを配列で返す
     * glue指定の場合はglueでjoinした文字列を返す
     *
     * @static
     * @param {String} name
     * @param {*} target
     * @param {String} glue
     * @returns {*}
     */
    static getValuesByName(name,target,glue){
        return Utils.getValuesBySelector(`[name="${name}"]`,target,glue);
    }
    /**
     * selectorで指定した要素のvalueを配列で返す
     * glue指定の場合はglueでjoinした文字列を返す
     *
     * @static
     * @param {String} selector
     * @param {*} target
     * @param {String} glue
     * @returns {*}
     */
    static getValuesBySelector(selector,target,glue){
        if(!target)target = document;
        let values = [];
        const inputs = target.querySelectorAll(selector);
        inputs.forEach((input,i)=>{
            if(Utils.tagNameIs(input,'select') || (Utils.tagNameIs(input,'input',['checkbox','radio']) && input.checked) || (Utils.tagNameIs(input,'input') && !['checkbox','radio'].includes(input.type))){
                values.push(input.value);
            }
        });
        return typeof glue === 'undefined' ? values : values.join(glue);
    }
    /**
     * チェック済みのcheckbox,radioの値を取得
     *
     * @static
     * @param {String} inputName
     * @param {*} target
     * @returns {[]}
     */
    static getCheckedValuesByInputName(inputName,target){
        if(!target)target = document;
        let values = [];
        const inputs = target.querySelectorAll(`input[name="${inputName}"]`);
        inputs.forEach((input,i)=>{
            if(input.checked){
                values.push(input.value);
            }
        });
        return values;
    }
    /**
     * classListを更新するためのメソッドチェーン
     *
     * @static
     * @param {*} elm
     * @returns {{ elm: any; add: (value: any) => ...; remove: (value: any) => ...; update: (add: any, value: any) => ...; switch: (addToLeft: any, value: any) => ...; }}
     */
    static classList(elm){
        const obj = {
            elm   : typeof elm === 'string' ? document.querySelectorAll(elm) : elm,
            add   : function(value){
                if(this.elm){
                    if(NodeList.prototype.isPrototypeOf(this.elm) || Array.isArray(this.elm)){
                        this.elm.forEach((_elm,i)=>{
                            _elm.classList.add(value);
                        });
                    }else{
                        this.elm.classList.add(value);
                    }
                }
                return this;
            },
            remove : function(value){
                if(this.elm){
                    if(NodeList.prototype.isPrototypeOf(this.elm) || Array.isArray(this.elm)){
                        this.elm.forEach((_elm,i)=>{
                            _elm.classList.remove(value);
                        });
                    }else{
                        this.elm.classList.remove(value);
                    }
                }
                return this;
            },
            update : function(add,value){
                if(this.elm){
                    if(add){
                        if(NodeList.prototype.isPrototypeOf(this.elm) || Array.isArray(this.elm)){
                            this.elm.forEach((_elm,i)=>{
                                _elm.classList.add(value);
                            });
                        }else{
                            this.elm.classList.add(value);
                        }
                    }else{
                        if(NodeList.prototype.isPrototypeOf(this.elm) || Array.isArray(this.elm)){
                            this.elm.forEach((_elm,i)=>{
                                _elm.classList.remove(value);
                            });
                        }else{
                            this.elm.classList.remove(value);
                        }
                    }
                }
                return this;
            },
            switch : function(addToLeft,value){
                if(this.elm){
                    if(NodeList.prototype.isPrototypeOf(this.elm) || Array.isArray(this.elm)){
                        if(1 < this.elm.length){
                            if(addToLeft){
                                this.elm[0].classList.add(value);
                                this.elm[1].classList.remove(value);
                            }else{
                                this.elm[0].classList.remove(value);
                                this.elm[1].classList.add(value);
                            }
                        }
                    }
                }
                return this;
            }
        };
        return obj;
    }
    /**
     * initWithScrollY
     *
     * @static
     */
    static initWithScrollY(){
        let scrollY = localStorage.getItem('scrollKey');
        if(!scrollY){
            return;
        }
        localStorage.removeItem('scrollKey');
        scrollY = Number(scrollY);
        window.scrollTo(0, scrollY);
    }
    /**
     * スクロール位置を保存しつつリロード
     *
     * @static
     * @param {*} delay
     */
    static reloadWithScrollY(delay){
        if(delay){
            setTimeout(()=>{
                Utils.reloadWithScrollY();
            },delay);
        }else{
            localStorage.setItem('scrollKey',window.scrollY);
            location.reload();
        }
    }
    /**
     * スクロール位置を保存
     *
     * @static
     */
    static setScrollYToLS(){
        localStorage.setItem('scrollKey',window.scrollY);
    }
    /**
     * ローカルストレージ保存
     *
     * @static
     */
    static setLS(key,value){
        localStorage.setItem(key,value);
    }
    /**
     * ローカルストレージ取得
     *
     * @static
     * @param {String} key
     * @param {Boolean} remove
     * @returns {*}
     */
    static getLS(key,remove){
        const value = localStorage.getItem(key);
        if(remove){
            localStorage.removeItem(key);
        }
        return value;
    }
    /**
     * fetch
     * 
     * @static
     * @param {String} url
     * @returns {Promise}
     */
    static fetch(url){
        return new Promise((resolve,reject) => {
            fetch(url)
                .then((response)=>{
                    if (!response.ok) {
                        throw new Error();
                    }
                    return response.text();
                })
                .then((text)=>{
                    let hasError = false;
                    let obj;
                    try {
                        obj = JSON.parse(text.replace(/<section[^>]+>/,'').replace(/<\/section>/,'').replace(/\s/g,''));
                    } catch (error) {
                        hasError = true;
                    }
                    if(hasError){
                        reject(false);
                    }else{
                        resolve(obj);
                    }
                })
                .catch((error)=>{
                    reject(false);
                });
        });
    }
}