ブログ

  • Rust × ESP32開発が本格化:esp-hal 1.0のインパクトと始め方

    2025年10月、ESP32向けRust HAL「esp-hal」がバージョン1.0.0に到達しました。これはマイコンベンダーが公式にサポートする初の安定版Rust HALであり、組込みRust界にとって画期的な出来事です。この記事では、esp-hal 1.0の意義と、ESP32でRust開発を始める方法を解説します。

    esp-hal 1.0.0とは

    esp-halはEspressifが公式に開発・メンテナンスしている、ESP32ファミリー向けのRustハードウェア抽象化レイヤー(HAL)です。2025年2月にベータ版、10月に正式リリースされました。

    1.0で安定化された機能:

    • HAL初期化(esp_hal::init)と設定
    • GPIO、UART、SPI、I2Cドライバ
    • 時間モジュール(Instant、Duration、Rate)
    • async/blockingの両方のドライバモード
    • Embassyなどの非同期ランタイムとの互換性

    その他の機能はunstableフィーチャーフラグの下に配置され、段階的に安定化される予定です。次の大きな安定化ターゲットはWi-Fi/Bluetooth/ESP-NOWスタック「esp-radio」です。

    2つの開発アプローチ

    ESP32でRustを使う方法は2つあります。

    1. no_std + esp-hal(ベアメタル)

    OSを使わず、ハードウェアを直接制御するアプローチです。バイナリサイズが小さく、起動が速く、リアルタイム性が必要な場面に適しています。esp-hal 1.0はこのパスの安定版です。

    2. std + esp-idf-hal(IDF上で動作)

    ESP-IDFの上にRust標準ライブラリを載せるアプローチです。スレッド、ネットワーキング、ファイルシステムなど標準ライブラリの機能がフルに使えます。バイナリは大きくなりますが、機能の網羅性は高いです。

    LLVM Xtensaアップストリーム化

    ESP32・ESP32-S2・ESP32-S3が採用するXtensaアーキテクチャのLLVMサポートが、本家LLVMへのアップストリーム化が進んでいます。基本ISAの大部分はすでにLLVM本体に取り込まれており、将来的にはEspressifのカスタムLLVMフォークなしで、標準のRustツールチェーンからXtensaターゲットにコンパイルできるようになります。

    なお、RISC-VベースのESP32-C系/H系チップはすでに標準ツールチェーンでコンパイル可能です。

    始め方

    ESP32でRust開発を始める手順は以下のとおりです。

    1. RustインストールrustupでRustをインストール
    2. espupインストールcargo install espupでESP32用ツールチェーンマネージャを導入
    3. ツールチェーン構築espup installでXtensa対応ツールチェーンを自動セットアップ
    4. プロジェクト作成cargo generate esp-rs/esp-halテンプレートからプロジェクトを生成
    5. ビルド&フラッシュcargo runで実機にフラッシュして実行

    まとめ

    esp-hal 1.0.0のリリースにより、ESP32でのRust開発は「実験的」から「プロダクション対応」へと段階が上がりました。メモリ安全性とゼロコスト抽象化というRustの利点を、IoTデバイス開発で活かせる時代が来ています。C/C++に慣れた組込みエンジニアも、ぜひRust on ESP32を試してみてください。

  • ESP32-S3でTinyML入門:エッジAI推論を実装する方法

    クラウドに頼らず、マイコン上で直接AI推論を行う「TinyML」が注目を集めています。ESP32-S3はベクトル命令拡張を備え、TinyMLに最適なチップの一つです。この記事では、ESP32-S3でのエッジAI開発について、ツールチェーンからモデルデプロイまで解説します。

    なぜESP32-S3なのか

    ESP32-S3はデュアルコアXtensa LX7(最大240MHz)に加え、ベクトル命令拡張を搭載しています。これにより畳み込みニューラルネットワーク(CNN)の演算が高速化されます。ESP-NNライブラリがこれらの命令を最適に活用し、推論電力は約130〜157mW、レイテンシはモデルサイズに応じて7ms〜536msです。

    実用的な性能として、小型CNNなら約20FPS、中規模モデルでも約1FPSで推論可能です。顔検出、動体検知、音声コマンド認識などのユースケースに十分な性能です。

    ESP-DL:Espressif公式ディープラーニングライブラリ

    Espressifが提供するESP-DLは、ESP32向けの公式ディープラーニングフレームワークです。2025年に大幅なアップデートが行われました。

    ESP-DL v3.2(2025年10月)の主な特徴:

    • 独自のモデルフォーマット「.espdl」(FlatBuffersベースで高速なゼロコピー読み込み)
    • 8bit/16bit量子化サポート
    • ESP32-S3/P4向けのアセンブリ最適化カーネル(PIE命令活用)
    • Conv、Gemm、Add、Mulなどの主要演算をハードウェアアクセラレーション

    ESPDet-Pico:リアルタイム物体検出

    2025年4月にリリースされたESPDet-PicoはUltralytics YOLOv11ベースの軽量物体検出モデルです。歩行者検出、顔検出、COCO(YOLO11n)、猫・犬・手の検出など、事前学習済みモデルが提供されており、ESP32-S3上でリアルタイム推論が可能です。

    開発フロー

    ESP32でTinyMLを始める基本的な流れは以下のとおりです。

    1. モデル学習:PC上でPyTorch/TensorFlowでモデルを学習
    2. 量子化:esp-ppqツールで8bit/16bitに量子化し、.espdlフォーマットに変換
    3. デプロイ:ESP-DLライブラリを使ってESP32上でモデルをロード・推論
    4. 最適化:ESP-NN/PIE命令による自動最適化が適用される

    より手軽に始めたい場合はEdge Impulseがおすすめです。Webブラウザ上でデータ収集からモデル学習、ESP32へのデプロイまで一貫して行えます。深いML知識がなくても物体検出や音声認識モデルを構築できます。

    ESP32-S3 + カメラの活用例

    ESP32-S3はカメラインターフェースを備えているため、映像系のAIアプリケーションに強みがあります。DFRobotの「ESP32-S3 AI Camera」(8MB PSRAM、16MBフラッシュ)は、顔認識・物体検出・音声対話をエッジで実行できる開発ボードとして2025年に発売されました。

    代表的なユースケース:

    • Ringの代替となるローカル動作のドアベルカメラ
    • ペット検知・見守りカメラ
    • 製造ラインでの外観検査
    • 来客カウンターや人流分析

    まとめ

    ESP32-S3はTinyMLの実用的なプラットフォームとして成熟しました。ESP-DL v3.2とESPDet-Picoにより、YOLOベースの物体検出まで手の届く範囲になっています。数百円のマイコンでAI推論ができる時代を、ぜひ体験してみてください。

  • ESP32の最新動向まとめ(2025-2026):新チップ・Wi-Fi 6・AI対応まで完全解説

    IoT開発者にとって定番のESP32ファミリーですが、2025〜2026年にかけて大きな進化を遂げています。新チップの量産開始、Wi-Fi 6対応、AI推論機能の強化など、注目すべきアップデートが目白押しです。この記事では、ESP32エコシステムの最新動向を網羅的にまとめます。

    新チップラインナップの拡充

    Espressifは従来のESP32シリーズに加え、用途特化型の新チップを続々とリリースしています。

    ESP32-P4:高性能HMI向けSoC

    ESP32-P4はEspressif史上最もパワフルなチップです。デュアルコアRISC-V(最大400MHz)を搭載し、MIPI-CSI/DSIによる1080pカメラ入力・ディスプレイ出力、ハードウェアH.264エンコーダ(1080p@30fps)、55本のGPIOを備えています。

    注目点はWi-Fi/Bluetoothを内蔵しないこと。無線通信はESP32-C6などのコンパニオンチップと組み合わせる設計です。WaveshareやGUITIONからP4+C6を統合した開発ボード(約14ドル〜)が発売されています。

    ESP32-C5:業界初デュアルバンドWi-Fi 6対応MCU

    2025年5月に量産開始されたESP32-C5は、RISC-Vベースで2.4GHz + 5GHzのデュアルバンドWi-Fi 6(802.11ax)に対応した業界初のMCUです。従来のESP32はすべて2.4GHzのみだったため、5GHz帯が使えるのは大きな進化です。さらにBluetooth 5やIEEE 802.15.4(Zigbee/Thread)にも対応しています。

    CES 2026で発表:ESP32-E22とESP32-H21

    ESP32-E22はWi-Fi 6E(2.4/5/6GHz トライバンド)に対応し、2×2 MIMOで最大2.4Gbpsのデータレートを実現。デュアルコア500MHz RISC-Vプロセッサを搭載し、無線コプロセッサとして動作します。

    ESP32-H21は超低消費電力BLE MCUで、96MHz RISC-V、320KB RAM、BLE + IEEE 802.15.4対応。ウェアラブルやバッテリー駆動IoTノード向けです。

    開発フレームワークの進化

    ESP-IDF 6.0(2026年2月リリース)

    ESP-IDFのメジャーアップデートであるv6.0が2026年2月13日にリリースされました。主な変更点はMbedTLS v4シリーズへのアップグレードとPSA Crypto APIへの移行です。暗号化APIの使い方に破壊的変更があるため、既存プロジェクトの移行には注意が必要です。

    Arduino ESP32 Core 3.x

    Arduino ESP32 Core 3.0はESP-IDF 5.1ベースに刷新され、ESP32-C6やESP32-H2のサポートが追加されました。8つの周辺機器APIが更新され、ネイティブI2Cオーディオライブラリ、Ethernet SPIサポート(W5500、DM9051等)も追加されています。2.x系からの移行には破壊的変更があるため注意が必要です。

    セキュリティ:CVE-2025-27840「バックドア」騒動

    2025年3月、ESP32に29個の文書化されていないBluetooth HCIコマンドが見つかり「バックドア」として話題になりました。しかしEspressifは、これらはデバッグ用コマンドであり物理アクセスなしにはリモートで悪用できないと説明。ESP-IDF v5.5でこれらのコマンドをデフォルト無効化する修正が行われました。

    Matter/Thread対応の成熟

    スマートホーム標準プロトコル「Matter」への対応が着実に進んでいます。ESP32ファミリーはMatter 1.0認証を取得済みで、Thread Border RouterソリューションもThread V1.3認証を取得しています。ESPHome 2025.6.0ではOpenThreadサポートが追加され、ESP32-C6/H2をThreadエンドポイントとしてHome Assistantに直接接続できるようになりました。

    まとめ

    ESP32ファミリーは単なるWi-Fiマイコンから、Wi-Fi 6/6E、Thread/Matter、AI推論、1080p映像処理まで対応する総合IoTプラットフォームへと進化しています。用途に応じて最適なチップを選べるラインナップの充実が最大の魅力です。次回はESP32でのAI/TinyMLについて詳しく解説します。

  • WordPress プラグイン開発入門:OOP設計でメンテナブルなプラグインを作る

    WordPressプラグインの開発は、functions.phpにコードを追加する延長線上にありますが、本格的なプラグインはOOP(オブジェクト指向)で設計すべきです。この記事では、保守性が高く拡張しやすいプラグインの作り方を解説します。

    プラグインのディレクトリ構成

    my-awesome-plugin/
    ├── my-awesome-plugin.php    # メインファイル(エントリポイント)
    ├── includes/
    │   ├── class-plugin.php     # プラグインのメインクラス
    │   ├── class-admin.php      # 管理画面の処理
    │   ├── class-api.php        # REST APIエンドポイント
    │   └── class-db.php         # データベース操作
    ├── admin/
    │   ├── views/               # 管理画面テンプレート
    │   ├── css/
    │   └── js/
    ├── public/
    │   ├── css/
    │   └── js/
    ├── languages/               # 翻訳ファイル
    └── readme.txt               # WordPress.org用

    メインファイル

    <?php
    /**
     * Plugin Name: My Awesome Plugin
     * Description: プラグインの説明
     * Version: 1.0.0
     * Author: Your Name
     * Text Domain: my-awesome-plugin
     */
    
    if (!defined("ABSPATH")) exit;
    
    define("MAP_VERSION", "1.0.0");
    define("MAP_PLUGIN_DIR", plugin_dir_path(__FILE__));
    define("MAP_PLUGIN_URL", plugin_dir_url(__FILE__));
    
    require_once MAP_PLUGIN_DIR . "includes/class-plugin.php";
    
    // プラグイン初期化
    function map_init() {
        return MyAwesomePluginPlugin::get_instance();
    }
    add_action("plugins_loaded", "map_init");
    
    // アクティベーション・ディアクティベーション
    register_activation_hook(__FILE__, ["MyAwesomePluginPlugin", "activate"]);
    register_deactivation_hook(__FILE__, ["MyAwesomePluginPlugin", "deactivate"]);

    プラグインのメインクラス(シングルトン)

    <?php
    namespace MyAwesomePlugin;
    
    class Plugin {
        private static $instance = null;
    
        public static function get_instance() {
            if (self::$instance === null) {
                self::$instance = new self();
            }
            return self::$instance;
        }
    
        private function __construct() {
            $this->load_dependencies();
            $this->init_hooks();
        }
    
        private function load_dependencies() {
            require_once MAP_PLUGIN_DIR . "includes/class-admin.php";
            require_once MAP_PLUGIN_DIR . "includes/class-api.php";
            require_once MAP_PLUGIN_DIR . "includes/class-db.php";
        }
    
        private function init_hooks() {
            // 管理画面
            if (is_admin()) {
                new Admin();
            }
            // REST API
            add_action("rest_api_init", [new Api(), "register_routes"]);
            // フロントエンド
            add_action("wp_enqueue_scripts", [$this, "enqueue_public_assets"]);
        }
    
        public function enqueue_public_assets() {
            wp_enqueue_style(
                "map-public",
                MAP_PLUGIN_URL . "public/css/style.css",
                [],
                MAP_VERSION
            );
            wp_enqueue_script(
                "map-public",
                MAP_PLUGIN_URL . "public/js/main.js",
                [],
                MAP_VERSION,
                true
            );
            // JSにデータを渡す
            wp_localize_script("map-public", "mapConfig", [
                "ajaxUrl" => admin_url("admin-ajax.php"),
                "apiUrl" => rest_url("my-plugin/v1/"),
                "nonce" => wp_create_nonce("wp_rest"),
            ]);
        }
    
        public static function activate() {
            Db::create_tables();
            flush_rewrite_rules();
        }
    
        public static function deactivate() {
            flush_rewrite_rules();
        }
    }

    管理画面クラス

    <?php
    namespace MyAwesomePlugin;
    
    class Admin {
        public function __construct() {
            add_action("admin_menu", [$this, "add_menu"]);
            add_action("admin_init", [$this, "register_settings"]);
        }
    
        public function add_menu() {
            add_options_page(
                "My Plugin設定",
                "My Plugin",
                "manage_options",
                "my-awesome-plugin",
                [$this, "render_settings_page"]
            );
        }
    
        public function register_settings() {
            register_setting("map_settings", "map_api_key");
            register_setting("map_settings", "map_enabled");
        }
    
        public function render_settings_page() {
            include MAP_PLUGIN_DIR . "admin/views/settings.php";
        }
    }

    データベースクラス

    <?php
    namespace MyAwesomePlugin;
    
    class Db {
        public static function create_tables() {
            global $wpdb;
            $table = $wpdb->prefix . "map_logs";
            $charset = $wpdb->get_charset_collate();
    
            $sql = "CREATE TABLE IF NOT EXISTS $table (
                id bigint(20) NOT NULL AUTO_INCREMENT,
                user_id bigint(20) DEFAULT NULL,
                action varchar(50) NOT NULL,
                data longtext,
                created_at datetime DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (id),
                KEY user_id (user_id),
                KEY created_at (created_at)
            ) $charset;";
    
            require_once ABSPATH . "wp-admin/includes/upgrade.php";
            dbDelta($sql);
        }
    
        public static function insert_log($action, $data = null) {
            global $wpdb;
            return $wpdb->insert(
                $wpdb->prefix . "map_logs",
                [
                    "user_id" => get_current_user_id(),
                    "action" => $action,
                    "data" => is_array($data) ? json_encode($data) : $data,
                ],
                ["%d", "%s", "%s"]
            );
        }
    }

    セキュリティのベストプラクティス

    • ABSPATH チェック: 全PHPファイルの先頭で if (!defined("ABSPATH")) exit; を記述
    • Nonceの検証: フォーム送信やAjaxリクエストでは必ずnonce検証を行う
    • 権限チェック: current_user_can() で適切な権限を確認
    • サニタイズ: 入力値は sanitize_text_field()、出力は esc_html() で処理
    • SQLインジェクション対策: $wpdb->prepare() を使用

    まとめ

    OOP設計でプラグインを作ることで、コードの見通しが良くなり、チーム開発やメンテナンスが格段に楽になります。最初は少し面倒に感じるかもしれませんが、一度フレームワークを作ってしまえば、新しいプラグインでも使い回せます。WordPress公式ディレクトリへの公開を目指す場合も、この設計であればレビューを通過しやすいでしょう。

  • WordPress REST APIでヘッドレスCMS構築:Next.jsフロントエンドと連携する実践テクニック

    WordPressをヘッドレスCMSとして使い、フロントエンドをNext.jsで構築するアーキテクチャが注目されています。コンテンツ管理はWordPressの使いやすい管理画面で、表示はモダンなReactアプリで、という両方の良いところを取る手法です。

    WordPress REST APIの基本

    WordPress 4.7以降、REST APIが標準搭載されています。/wp-json/wp/v2/ 以下のエンドポイントで投稿・ページ・カテゴリなどのデータをJSON形式で取得できます。

    # 投稿一覧を取得
    curl https://your-site.com/wp-json/wp/v2/posts
    
    # 特定の投稿を取得
    curl https://your-site.com/wp-json/wp/v2/posts/123
    
    # カテゴリ一覧
    curl https://your-site.com/wp-json/wp/v2/categories
    
    # 検索
    curl "https://your-site.com/wp-json/wp/v2/posts?search=React"
    
    # ページネーション
    curl "https://your-site.com/wp-json/wp/v2/posts?per_page=10&page=2"

    Next.jsプロジェクトのセットアップ

    npx create-next-app@latest wp-frontend --typescript --app
    cd wp-frontend

    WordPress APIクライアント

    // lib/wordpress.ts
    const API_URL = process.env.WORDPRESS_API_URL || "https://your-site.com/wp-json/wp/v2";
    
    export interface WPPost {
      id: number;
      slug: string;
      title: { rendered: string };
      content: { rendered: string };
      excerpt: { rendered: string };
      date: string;
      categories: number[];
      _embedded?: {
        "wp:featuredmedia"?: Array<{ source_url: string }>;
      };
    }
    
    export async function getPosts(page = 1, perPage = 10): Promise<WPPost[]> {
      const res = await fetch(
        `${API_URL}/posts?_embed&per_page=${perPage}&page=${page}`,
        { next: { revalidate: 3600 } } // ISR: 1時間キャッシュ
      );
      if (!res.ok) throw new Error("Failed to fetch posts");
      return res.json();
    }
    
    export async function getPostBySlug(slug: string): Promise<WPPost | null> {
      const res = await fetch(
        `${API_URL}/posts?_embed&slug=${slug}`,
        { next: { revalidate: 3600 } }
      );
      const posts = await res.json();
      return posts[0] || null;
    }
    
    export async function getCategories() {
      const res = await fetch(`${API_URL}/categories`);
      return res.json();
    }

    投稿一覧ページ

    // app/page.tsx
    import { getPosts } from "@/lib/wordpress";
    import Link from "next/link";
    
    export default async function Home() {
      const posts = await getPosts();
    
      return (
        <main>
          <h1>ブログ</h1>
          {posts.map((post) => (
            <article key={post.id}>
              <Link href={`/posts/${post.slug}`}>
                <h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
              </Link>
              <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
              <time>{new Date(post.date).toLocaleDateString("ja-JP")}</time>
            </article>
          ))}
        </main>
      );
    }

    投稿詳細ページ

    // app/posts/[slug]/page.tsx
    import { getPostBySlug, getPosts } from "@/lib/wordpress";
    import { notFound } from "next/navigation";
    
    export async function generateStaticParams() {
      const posts = await getPosts(1, 100);
      return posts.map((post) => ({ slug: post.slug }));
    }
    
    export default async function PostPage({ params }: { params: { slug: string } }) {
      const post = await getPostBySlug(params.slug);
      if (!post) notFound();
    
      return (
        <article>
          <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
          <time>{new Date(post.date).toLocaleDateString("ja-JP")}</time>
          <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
        </article>
      );
    }

    カスタムエンドポイントの追加

    functions.phpでカスタムREST APIエンドポイントを追加できます。人気記事ランキングや関連記事など、標準APIにない機能を実装する場合に使います。

    // functions.php
    add_action("rest_api_init", function() {
        register_rest_route("custom/v1", "/popular", [
            "methods" => "GET",
            "callback" => function() {
                $posts = get_posts([
                    "meta_key" => "post_views",
                    "orderby" => "meta_value_num",
                    "order" => "DESC",
                    "numberposts" => 5,
                ]);
                return array_map(function($p) {
                    return [
                        "id" => $p->ID,
                        "title" => $p->post_title,
                        "slug" => $p->post_name,
                        "views" => get_post_meta($p->ID, "post_views", true),
                    ];
                }, $posts);
            },
            "permission_callback" => "__return_true",
        ]);
    });

    CORS設定

    ヘッドレス構成ではフロントエンドとWordPressが別ドメインになるため、CORS設定が必要です。

    // functions.php
    add_action("rest_api_init", function() {
        remove_filter("rest_pre_serve_request", "rest_send_cors_headers");
        add_filter("rest_pre_serve_request", function($value) {
            header("Access-Control-Allow-Origin: https://your-frontend.com");
            header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
            header("Access-Control-Allow-Headers: Content-Type, Authorization");
            return $value;
        });
    });

    まとめ

    WordPressのREST APIとNext.jsを組み合わせることで、編集者にはWordPressの使いやすいUIを、ユーザーにはReactベースの高速な表示体験を提供できます。ISRを使えばビルド不要で最新コンテンツが反映される、最強のブログ基盤が構築できます。

  • WordPress 6.x ブロックエディタの進化まとめ:サイトエディタ・パターン・スタイルブック完全ガイド

    WordPress 6.x系では、ブロックエディタ(Gutenberg)が大幅に進化しました。サイトエディタの安定化、パターンシステムの刷新、スタイルブックの導入など、サイト構築のワークフローが根本から変わっています。この記事では、WordPress 6.0〜6.7で追加された主要機能を実践的に解説します。

    サイトエディタ(Full Site Editing)の成熟

    WordPress 6.2でサイトエディタがベータから正式版に昇格しました。ヘッダー、フッター、サイドバーを含むサイト全体をブロックエディタで編集できます。

    サイトエディタでは「テンプレート」と「テンプレートパーツ」を直接編集できます。PHPテンプレートファイルを触らずに、ビジュアルエディタ上でレイアウトを組み替えられるのは革命的です。

    テンプレートの種類

    • ページテンプレート: 固定ページ・投稿ごとに異なるレイアウトを適用
    • アーカイブテンプレート: カテゴリ・タグ一覧ページのレイアウト
    • 検索結果テンプレート: 検索結果ページの表示形式
    • 404テンプレート: ページが見つからない場合の表示

    パターンシステムの刷新

    WordPress 6.3で「再利用ブロック」が「パターン」に統合されました。パターンには2種類あります。

    • 同期パターン(旧:再利用ブロック): 一箇所を編集すると全ての使用箇所に反映される
    • 非同期パターン: テンプレートとして挿入。挿入後は独立して編集可能

    テーマ開発者は、patternsディレクトリにPHPファイルを配置するだけでカスタムパターンを登録できます。

    // patterns/hero-section.php
    <?php
    /**
     * Title: ヒーローセクション
     * Slug: mytheme/hero-section
     * Categories: featured
     */
    ?>
    <!-- wp:cover {"dimRatio":50} -->
    <div class="wp-block-cover">
      <div class="wp-block-cover__inner-container">
        <!-- wp:heading {"textAlign":"center","level":1} -->
        <h1 class="has-text-align-center">サイトタイトル</h1>
        <!-- /wp:heading -->
        <!-- wp:paragraph {"align":"center"} -->
        <p class="has-text-align-center">サブタイトルテキスト</p>
        <!-- /wp:paragraph -->
      </div>
    </div>
    <!-- /wp:cover -->

    スタイルブック

    WordPress 6.2で導入されたスタイルブックは、サイト内の全ブロックの見た目を一覧で確認・編集できる機能です。「外観」→「エディター」→「スタイル」からアクセスできます。

    見出し、段落、ボタン、テーブルなど全てのブロックタイプのスタイルを、実際のプレビューを見ながらカスタマイズできます。CSSを一行も書かずに、サイト全体のデザインシステムを構築できるのは非常に強力です。

    theme.jsonの進化

    theme.jsonはブロックテーマの心臓部です。バージョン2(WordPress 6.1〜)では、設定項目が大幅に増えました。

    {
      "$schema": "https://schemas.wp.org/trunk/theme.json",
      "version": 3,
      "settings": {
        "color": {
          "palette": [
            { "slug": "primary", "color": "#1a73e8", "name": "プライマリ" },
            { "slug": "secondary", "color": "#333", "name": "セカンダリ" }
          ],
          "gradients": [],
          "custom": false
        },
        "typography": {
          "fontFamilies": [
            {
              "fontFamily": "-apple-system, BlinkMacSystemFont, sans-serif",
              "slug": "system",
              "name": "システムフォント"
            }
          ],
          "fontSizes": [
            { "slug": "small", "size": "14px", "name": "小" },
            { "slug": "medium", "size": "16px", "name": "中" },
            { "slug": "large", "size": "24px", "name": "大" }
          ]
        },
        "spacing": {
          "units": ["px", "rem", "%"]
        },
        "layout": {
          "contentSize": "800px",
          "wideSize": "1200px"
        }
      },
      "styles": {
        "blocks": {
          "core/heading": {
            "typography": { "fontWeight": "700" }
          }
        }
      }
    }

    WordPress 6.5〜6.7の注目機能

    • フォントライブラリ(6.5): Google Fontsやローカルフォントをアップロードして管理画面から適用
    • ブロックバインディングAPI(6.5): ブロックの属性をカスタムフィールドや外部データに動的にバインド
    • データビュー(6.6): 投稿・ページ一覧がグリッド表示に対応、フィルタ・ソート機能が強化
    • セクションスタイル(6.7): グループブロックにスタイルバリエーションを適用して、セクション単位でデザインを切り替え

    クラシックテーマからの移行

    既存のクラシックテーマ(PHPテンプレートベース)からブロックテーマへの移行は段階的に行えます。まずは「テーマサポート」を追加してブロックエディタの機能を有効にし、徐々にテンプレートをブロックベースに置き換えていくアプローチが現実的です。

    まとめ

    WordPress 6.x系のブロックエディタは、もはや単なる記事エディタではなく、サイト全体を構築するフルサイトエディティングツールへと進化しました。theme.jsonとパターンを活用すれば、コードを最小限に抑えつつ、高品質なテーマを開発できます。

  • PythonのFastAPIで爆速REST API開発:非同期処理とバリデーション

    FastAPIはPython製のWebフレームワークで、型ヒントを活用した自動バリデーション、自動ドキュメント生成、非同期処理対応が特徴です。Flask比で200%以上のパフォーマンス向上が見込めます。

    FastAPIの特徴

    • Python型ヒントによる自動バリデーション
    • OpenAPI(Swagger)ドキュメント自動生成
    • async/await対応の非同期処理
    • Pydanticモデルによるデータシリアライゼーション
    • StarletteベースのASGIフレームワーク

    インストールと最初のAPI

    pip install "fastapi[standard]"
    # main.py
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel, Field
    from typing import Optional
    import uvicorn
    
    app = FastAPI(title="商品管理API", version="1.0.0")
    
    class Product(BaseModel):
        name: str = Field(..., min_length=1, max_length=100)
        price: int = Field(..., gt=0, description="価格(円)")
        description: Optional[str] = None
        in_stock: bool = True
    
    # インメモリDB
    products: dict[int, Product] = {}
    next_id = 1
    
    @app.post("/products", status_code=201)
    async def create_product(product: Product):
        global next_id
        product_id = next_id
        products[product_id] = product
        next_id += 1
        return {"id": product_id, **product.model_dump()}
    
    @app.get("/products/{product_id}")
    async def get_product(product_id: int):
        if product_id not in products:
            raise HTTPException(status_code=404, detail="商品が見つかりません")
        return {"id": product_id, **products[product_id].model_dump()}
    
    @app.get("/products")
    async def list_products(
        min_price: Optional[int] = None,
        max_price: Optional[int] = None,
        in_stock: Optional[bool] = None,
    ):
        result = []
        for pid, p in products.items():
            if min_price and p.price  max_price:
                continue
            if in_stock is not None and p.in_stock != in_stock:
                continue
            result.append({"id": pid, **p.model_dump()})
        return result
    
    if __name__ == "__main__":
        uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

    自動ドキュメント

    FastAPIは起動するだけで以下のドキュメントが自動生成されます。

    • Swagger UI: http://localhost:8000/docs – インタラクティブなAPI操作画面
    • ReDoc: http://localhost:8000/redoc – 読みやすいドキュメント形式

    非同期処理

    FastAPIはasync/awaitをネイティブサポートしています。DB操作やHTTPリクエストなどのI/O処理を非同期で実行できます。

    import httpx
    
    @app.get("/external-data")
    async def get_external_data():
        async with httpx.AsyncClient() as client:
            response = await client.get("https://api.example.com/data")
            return response.json()

    まとめ

    FastAPIは、型安全性・パフォーマンス・開発体験の全てにおいて優れたフレームワークです。AI APIのバックエンド、マイクロサービス、プロトタイプ開発など幅広い場面で活躍します。

  • Fly.ioでNode.jsアプリを無料デプロイ:Heroku代替の本命

    Herokuの無料プランが廃止されて以降、個人開発者のデプロイ先として注目を集めているFly.io。東京リージョンがあり、日本からのレイテンシも低い。Node.jsアプリのデプロイ手順を実際のプロジェクトを例に解説します。

    Fly.ioの特徴

    • 東京リージョン(nrt)あり
    • Dockerベースでデプロイ
    • 無料枠: 3つのshared-cpu-1x VM、160GB転送量/月
    • PostgreSQL、Redis、S3互換ストレージも提供
    • 自動HTTPS対応

    セットアップ

    # Fly CLIインストール
    curl -L https://fly.io/install.sh | sh
    
    # ログイン
    fly auth login
    
    # プロジェクト作成
    cd my-node-app
    fly launch

    Dockerfile の準備

    FROM node:20-alpine
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --omit=dev
    COPY src/ ./src/
    EXPOSE 3000
    CMD ["node", "src/index.js"]

    fly.tomlの設定

    app = "my-awesome-app"
    primary_region = "nrt"  # 東京リージョン
    
    [build]
    
    [http_service]
      internal_port = 3000
      force_https = true
      auto_stop_machines = "stop"
      auto_start_machines = true
      min_machines_running = 0
    
    [env]
      NODE_ENV = "production"

    シークレット管理

    # 環境変数(シークレット)の設定
    fly secrets set DATABASE_URL="postgresql://..."
    fly secrets set API_KEY="sk-..."
    
    # 設定済みシークレット一覧
    fly secrets list

    デプロイ

    # デプロイ実行
    fly deploy
    
    # ログ確認
    fly logs
    
    # ステータス確認
    fly status
    
    # ブラウザで開く
    fly open

    カスタムドメイン設定

    # カスタムドメイン追加
    fly certs create example.com
    
    # DNS設定を確認
    fly certs show example.com

    まとめ

    Fly.ioは「flyctl deploy」一発でDockerアプリをデプロイできる手軽さと、東京リージョンの低レイテンシが魅力です。個人プロジェクトからスタートアップまで、幅広い用途で活用できます。

  • RAG(検索拡張生成)をゼロから実装する:LangChainとFAISSで社内ドキュメント検索

    RAG(Retrieval-Augmented Generation)は、LLMに外部知識を与えて回答精度を向上させる手法です。社内ドキュメントやFAQをベクトルデータベースに格納し、質問に関連する情報を検索してからLLMに回答させます。

    RAGの仕組み

    RAGは大きく2つのフェーズで動作します。

    1. インデックス作成: ドキュメントをチャンクに分割→ベクトル化→ベクトルDBに格納
    2. 検索+生成: ユーザーの質問をベクトル化→類似チャンクを検索→検索結果+質問をLLMに渡して回答生成

    環境準備

    pip install langchain langchain-openai faiss-cpu tiktoken

    ドキュメントの読み込みとチャンク分割

    from langchain.text_splitter import RecursiveCharacterTextSplitter
    from langchain_community.document_loaders import TextLoader
    
    # ドキュメント読み込み
    loader = TextLoader("company_faq.txt", encoding="utf-8")
    documents = loader.load()
    
    # チャンク分割(500文字ごと、100文字オーバーラップ)
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=500,
        chunk_overlap=100,
        separators=["nn", "n", "。", "、", " "]
    )
    chunks = splitter.split_documents(documents)
    print(f"チャンク数: {len(chunks)}")

    ベクトルDBの構築

    from langchain_openai import OpenAIEmbeddings
    from langchain_community.vectorstores import FAISS
    
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    
    # FAISSインデックスを作成
    vectorstore = FAISS.from_documents(chunks, embeddings)
    
    # ローカルに保存(永続化)
    vectorstore.save_local("faiss_index")
    
    # 読み込み
    vectorstore = FAISS.load_local("faiss_index", embeddings, allow_dangerous_deserialization=True)

    RAGチェーンの構築

    from langchain_openai import ChatOpenAI
    from langchain.chains import RetrievalQA
    
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
    
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm,
        chain_type="stuff",
        retriever=retriever,
        return_source_documents=True,
    )
    
    result = qa_chain.invoke({"query": "有給休暇の申請方法を教えてください"})
    print(result["result"])
    print("---参照ドキュメント---")
    for doc in result["source_documents"]:
        print(f"  - {doc.page_content[:100]}...")

    精度向上のコツ

    • チャンクサイズ: 小さすぎると文脈が失われ、大きすぎるとノイズが増える。300-500文字が目安
    • オーバーラップ: チャンク境界での情報欠落を防ぐ。チャンクサイズの20%程度
    • 検索件数(k): 多すぎるとコンテキストウィンドウを圧迫。3-5件が推奨
    • リランキング: 検索結果をLLMで再評価してから使うと精度が上がる

    まとめ

    RAGを使えば、LLMが学習していない最新情報や社内固有の知識に基づいた回答が可能になります。FAISSは無料で使えるベクトルDBとして優秀で、小〜中規模のドキュメントであれば十分な性能を発揮します。

  • Next.js 14 App RouterでSSR・SSG・ISRを使い分ける実践ガイド

    Next.js 14のApp Routerでは、レンダリング方式の選択が柔軟になりました。SSR(サーバーサイドレンダリング)、SSG(静的サイト生成)、ISR(増分静的再生成)をページ単位・コンポーネント単位で使い分ける方法を解説します。

    App Routerの基本

    Next.js 13以降のApp Routerでは、デフォルトですべてのコンポーネントがServer Componentになります。これにより、クライアントに送信されるJavaScriptの量が大幅に削減されます。

    // app/page.tsx - デフォルトでServer Component
    export default async function HomePage() {
      // サーバー側で実行される
      const data = await fetch("https://api.example.com/posts");
      const posts = await data.json();
    
      return (
        <main>
          <h1>最新記事</h1>
          {posts.map(post => (
            <article key={post.id}>
              <h2>{post.title}</h2>
              <p>{post.excerpt}</p>
            </article>
          ))}
        </main>
      );
    }

    SSG(静的生成)

    ビルド時にHTMLを生成する方式です。ブログ記事やドキュメントなど、更新頻度が低いコンテンツに最適です。

    // app/blog/[slug]/page.tsx
    export async function generateStaticParams() {
      const posts = await fetch("https://api.example.com/posts").then(r => r.json());
      return posts.map(post => ({ slug: post.slug }));
    }
    
    export default async function BlogPost({ params }) {
      const post = await fetch(
        `https://api.example.com/posts/${params.slug}`,
        { cache: "force-cache" } // SSG: ビルド時にキャッシュ
      ).then(r => r.json());
    
      return <article><h1>{post.title}</h1><div>{post.content}</div></article>;
    }

    SSR(サーバーサイドレンダリング)

    リクエストごとにサーバーでHTMLを生成します。ユーザーごとに異なるコンテンツを表示する場合に使います。

    // app/dashboard/page.tsx
    export default async function Dashboard() {
      const data = await fetch("https://api.example.com/user/dashboard", {
        cache: "no-store", // SSR: キャッシュしない
        headers: { Authorization: `Bearer ${getToken()}` }
      }).then(r => r.json());
    
      return <div>ようこそ、{data.user.name}さん</div>;
    }

    ISR(増分静的再生成)

    SSGとSSRの良いとこ取り。静的に生成されたページを一定時間後にバックグラウンドで再生成します。

    // app/products/page.tsx
    export default async function Products() {
      const products = await fetch("https://api.example.com/products", {
        next: { revalidate: 3600 } // ISR: 1時間ごとに再生成
      }).then(r => r.json());
    
      return (
        <div>
          {products.map(p => <div key={p.id}>{p.name} - ¥{p.price}</div>)}
        </div>
      );
    }

    使い分けの判断基準

    どのレンダリング方式を使うかは、コンテンツの特性で決めます。更新頻度が低い(ブログ・ドキュメント)→ SSG、リアルタイム性が必要(ダッシュボード)→ SSR、適度に更新される(商品一覧・ニュース)→ ISR、が基本的な選び方です。

    まとめ

    App Routerでは、fetchのオプションを変えるだけでレンダリング方式を切り替えられます。ページの特性に合わせて最適な方式を選ぶことで、パフォーマンスとユーザー体験を両立できます。

IP: 取得中...
216.73.216.31216.73.216.31