スクロールで要素が見えたら数値をカウントアップさせるギミックを実装する

2026/03/11 (水) - 09:00 JavaScript

LP(ランディングページ)やコーポレートサイトで実績の数値を表示する際、画面をスクロールして数字が現れた瞬間に、「0から目標値まで数字がパラパラと動く演出)」を見たことがあると思います。静止した数字よりも視覚的なインパクトが強く、実績やデータの重要性をよりダイナミックに表現できます。今回はそのギミックの実装です。

表示イメージ

まずはHTML要素の指定。span要素の数を増やせばその分カウントアップする要素を増やせます。

<p>従業員数:<span class="txtCount" data-count="2000">2000</span>人</p>

カウントアップさせたいHTML要素にdata-count属性を指定し、カウントアップさせたい上限の数値を指定します。また、SEOやJavaScriptが無効化されている環境、音声読み上げブラウザのために予め同じ数値をHTMLテキストとしても指定しておきます。

document.addEventListener('DOMContentLoaded', () => {
 function countUp(el){
  const duration = 500; //数字がカウントアップする時間を指定 (ミリ秒)
  const target = +el.getAttribute('data-count');
  const start = 0;
  const startTime = performance.now();
  const updateCount = (currentTime) => {
   const elapsed = currentTime - startTime;
   const progress = Math.min(elapsed / duration, 1);
   const currentNum = Math.floor(progress * (target - start) + start);
   el.textContent = currentNum.toLocaleString();
   if (progress < 1) {
    requestAnimationFrame(updateCount);
   } else {
    el.textContent = target.toLocaleString();
   }
  };
  requestAnimationFrame(updateCount);
 };
 const options = {
  threshold: 0.7 // 70%見えたら実行
 };
 const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
   if (entry.isIntersecting) {
    countUp(entry.target);
    observer.unobserve(entry.target);
   }
  });
 }, options);
 document.querySelectorAll('.txtCount').forEach(num => {
  observer.observe(num);
 });
});

JavaScriptのコード。IntersectionObserverを使い要素が見えたらカウントがアップするイベントを発火させます。durationでカウントアップさせる秒数をミリ秒で指定します。

例だと500ミリ秒(0.5秒)かけてカウントアップが完了するイメージです。

Next.js 16でキャッシュを管理する

2026/03/01 (日) - 09:00 JavaScript

Next.js16では、従来に比べコンポーネントのキャッシュ管理がより簡潔になり何がキャッシュされているかが分かりやすく、デバッグも容易になりました。ページのほか、コンポーネント、関数もキャッシュできるため、パフォーマンスのアップも期待できます。

PPR(Partial Prerendering)の導入

従来のNext.jsでは、SSRSSGISRで構成することが多かったですが、Next.js 16では新たにPPR(Partial Prerendering)が導入されました。これはページ内で静的部分と動的部分を融合でき、パフォーマンスを重視したページの構築が可能になります。

たとえばオウンドメディアなどでヘッダーやフッターは静的として構成し、記事一覧などを動的に管理することができます。ISRと異なりSEOやアクセシビリティの面で非常に有利です。

Cache Components(use cache ディレクティブ)

今回導入されたのがCache Components(キャッシュコンポーネント)です。各ページやコンポーネントにuse cacheを指定することで、対象となったファイルをキャッシュしてくれるようになります。デフォルトでは使えないため、任意で有効化する必要があります。

next.config.jscacheComponents: trueの設定値を加えます。

import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
 cacheComponents: true,
};
export default nextConfig;

そのうえでページやコンポーネントの先頭にuse cacheを指定すると、キャッシュされ静的として扱われます。

async function cacheComponent(){
 "use cache";
 return (
  キャッシュするコンポーネント
 );
}

関数でも同様です。例えばデータ取得のfetchなどもキャッシュすることでパフォーマンス向上を期待できます。

async function getPosts(){
 "use cache";
 const res = await fetch('https://example.com/posts');
 const posts = await res.json();
 return posts;
}

なお、PPRを使用してbuildするとPartial Prerenderとしてパブリッシュされます。

Route (app)         Revalidate  Expire
┌ ○ /              15m      1y
├ ○ /_not-found
└ ◐ /blog           15m      1y
○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

キャッシュの設定

cache機能のcacheLifeを利用することでキャッシュの有効期限なども設定できます。

import { cacheLife } from 'next/cache';
async function getPosts(){
 "use cache";
 cacheLife(days);
 const res = await fetch('https://example.com/posts');
 const posts = await res.json();
 return posts;
}

例だと1日キャッシュするようになります。細かくは公式ドキュメントを御覧ください。

PHPとChatGPT APIでよくある質問のチャットボット(Chatbot)っぽいものを作る

2025/12/01 (月) - 09:00 Program

商品やサービス案内のウェブサイト内に「よくある質問」や「FAQ」ページを設けることが多いと思います。最近はChatbotのソリューションもAIを使って優秀になりましたが、今回はPHPでChatGPT APIから自分のサイトの質問・回答データを参照して答える簡易的なChatbotを自作してみました。

ChatGPTがで質問内容を回答させた例

ChatGPTを利用することでサイト内検索などを使わなくても、会話感覚で自然な情報検索が実現できます。

今回はカフェバーのよくある質問の検索ページを作る想定とします。参照するデータとして予め質問・回答データを集約したJSONファイルを用意しておきます。テキストやCSVでも構いませんし、直接SQLを叩いて呼び出しても構いません。

[
 {
 "question": "営業時間",
 "answer": "営業時間:11:00~23:00。定休日:年中無休です。"
 },
 {
 "question": "喫煙",
 "answer": "店内は全席禁煙になっております。"
 },
 〜略〜
 {
 "question": "飲酒",
 "answer": "ビール、ハイボール、サワー、シャンパン、ワイン、日本酒などのアルコール各種を豊富に用意しております。100種類のドリンクからお選び頂けます。"
 },
 {
 "question": "お会計",
 "answer": "現金、クレジットカード(VISA、MATER、JCB、AMEX、ダイナース、Discover)、電子決済(paypay、楽天Pay、LINE Pay、au Pay、WeChatPay、AliPay)がご利用いただけます。"
 }
]

次にPHPで、JSONファイルを参照してGPT APIが質問を受けるプログラムを実装します。実装する前に予めOpenAI developer platformからAPI Keysを取得しておきましょう。プロンプトは適当に変えてください。そして入力フォームからPOSTで送信された質問内容を取得して処理し、処理した質問文をプロンプトとしてAPIにリクエストします。

define('API_KEY', '取得したAPIキーを指定');
define('API', 'https://api.openai.com/v1/chat/completions');
function findFAQ($userQuestion) {
 $faqData = json_decode(file_get_contents('JSONファイルのURL'), true);
 $faqText = '';
 foreach ($faqData as $faq) {
  $faqText .= "Q: {$faq['question']}\nA: {$faq['answer']}\n\n";
 }
 $faqtext = "以下はFAQデータです。ユーザーからの質問に最適な回答を返してください。\n語尾ににゃんを付けて可愛く喋ってください。\nFAQデータ:${faqText}\n\nユーザーの質問: ${userQuestion}\n答え:";
  $prompt = [
   [
    'role' => 'user',
    'content' => $faqtext
   ]
  ];
  $url = curl_init(API);
  $header = array(
   'Authorization: Bearer '.API_KEY,
   'Content-type: application/json',
  );
  $params = json_encode([
   'messages' => $prompt,
   'model' => 'gpt-3.5-turbo',
  ]);
  $options = array(
   CURLOPT_POST => true,
   CURLOPT_HTTPHEADER =>$header,
   CURLOPT_POSTFIELDS => $params,
   CURLOPT_RETURNTRANSFER => true,
  );
  curl_setopt_array($url, $options);
  $httpResponse = curl_exec($url);
  $httpCode = curl_getinfo($url,CURLINFO_RESPONSE_CODE);
  if($httpCode === 200){
   $jsonArray = json_decode($httpResponse, true);
   $Answer = nl2br($jsonArray['choices'][0]['message']['content']);
  }else{
   $Answer = "申し訳ありませんにゃん。現在検索が使えないにゃん。";
  }
  return $Answer;
  curl_close($ch);
}
$question = htmlspecialchars($_POST['question'],ENT_QUOTES,'UTF-8') ?? '';
if ($question) {
 $answer = findFAQ($question);
} else {
 $answer = "質問を入力してにゃん♪";
}

正常にレスポンスが返ってきたらその値をフロントに表示します。質問文が空欄だったり、APIから正常にレスポンスが受け取れなかった場合はエラーを表示します。デザインはHTMLやCSSで適当に整形してください。

<form method="post">
 <dl>
  <dt><label for="question">質問</label></dt>
  <dd><input type="text" id="question" name="question" placeholder="質問文を入れてにゃん!" /></dd>
 </dl>
 <input type="submit" value="質問する" />
</form>
<hr />
<?php if ($userQuestion){ ?>
 <h2>質問:<?php echo $userQuestion; ?></h2>
 <p>回答:<?php echo nl2br(htmlspecialchars($answer)); ?></p>
<?php } ?>

なお、本格的なXSSなどのセキュリティ対策や、正常に回答が得られなかった場合の模範回答対策などは省いております。答えられなかった場合は問い合わせページへ誘導する処理などを適宜実装してください。

おさわり禁止

よくある質問ページのチャットボット(Chatbot)もどきを導入することでストレスなくユーザーの悩み解決を解決をサポートしし、問い合わせの人的な対応工数を削減できます。ただし、APIにリクエストする度にOpenAIに課金されてしまうので、その部分のコストはご注意ください。

JSで文字をシャッフルしながら1文字ずつ出現させる

2025/09/12 (金) - 10:00 JavaScript

見出しやナビの文字をランダムな文字でシャッフルしながら1文字ずつ出現するやつをJavaScriptで作りました。

昔、ActionScript 3.0(AS3)で作ったライブラリを無理矢理JavaScriptに書き換えた物なので、綺麗なコードではありませんが…。使い方としては、あらかじめHTML要素に文字を用意しておき、シャッフルテキスト用テキスト要素を指定したインスタンスを生成します。その後よきタイミング(ボタンを押下したり、スクロールが動いたタイミングなど)でplay();メソッドを実行すると対象のテキストがシャッフルします。

見出しなどあくまで短い文章向けのエフェクトなので、本文などに指定するのは避けるべきです。

HTMLのコード

<p id="txt">Chu!可愛くてごめん</p>
<button type="button" id="btn">シャッフルする</button>

実行させるためのJavaScript

const txt = document.getElementById('txt');
const btn = document.getElementById('btn');
const shuffle = new shuffleTxt(txt);
btn.addEventListener('click',function(){
 shuffle.play(); //ボタンを押下したら文字をシャッフル
});

本体のコード

const fps = 60; //インターバル 小さい値にすると遅くなる
class shuffleTxt{
 constructor(target){
  this.isFlg = false;
  this.targetEl = target;
  this.strBase = this.targetEl.textContent;
  this.strRandom = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!?#$%&|/*+-=;_:'
  this.strComplete = '';
  this.intRemaining= this.strBase.length;
  this.intNowCnt = 0;
  this.intStartTime = 0;
  this.intNowTime;
  this.intFPS = 1000/fps;
  this.timeID;
 }
 play(){
  if(this.isFlg){
   return false;
  }else{
   this.isFlg = true;
   this.targetEl.textContent = '';
   this.searchTxt();
  }
 }
 stop(){
  this.intNowCnt = 0;
  this.intStartTime = 0;
  this.intNowTime = 0;
  this.intRemaining = this.strBase.length;
  this.targetEl.textContent = this.strBase;
  this.isFlg = false;
 }
 searchTxt(){
  let difTime;
  if(this.strBase.length >= this.intNowCnt){
   this.strComplete = this.strBase.substr(0,this.intNowCnt);
   this.timeID = requestAnimationFrame(() => {
    this.intNowTime = new Date().getTime();
    difTime = this.intNowTime - this.intStartTime;
    if ( difTime >= this.intFPS ) {
     this.intStartTime = this.intNowTime;
     this.randomTxt();
    }else{
     this.searchTxt();
    }
   }); 
  }else{
   cancelAnimationFrame(this.timeID);
   this.stop();
  }
 }
 randomTxt(){
  let str = '';
  for (let i=0; i<=this.intRemaining; i++){
   str += this.strRandom.charAt(Math.floor(Math.random()*this.strRandom.length))
  }
  this.targetEl.textContent = this.strComplete + str;
  this.intNowCnt++;
  this.intRemaining--;
  this.searchTxt();
 }
}

Reactで画像がクロスフェードするスライドショーを作る

2025/08/19 (火) - 09:00 JavaScript

Reactで複数枚の画像をクロスフェードしてループで切り替えるスライドを簡易的に作ってみる。

スライドショーイメージ

JavaScriptのコードは以下の通り。予め画像を用意しておきそれを1枚ずつ切り替える想定です。ポイントとしては画像のクロスフェードを制御をCSS側で制御することで、class属性の有無で表示・非表示を切り替えます。

import React, { useState, useEffect } from 'react';
import './ImageSlider.css';
// 画像の枚数とファイル名を配列で指定
const images = [
 'slide1.webp',
 'slide2.webp',
 'slide3.webp'
];
function ImageSlider() {
 const [currentIndex, setCurrentIndex] = useState(0);
 useEffect(() => {
  // setIntervalで3秒ごとに画像を切り替えるタイマーを設定
  const timerId = setInterval(() => {
   setCurrentIndex(prevIndex => (prevIndex + 1) % images.length);
  }, 3000);
  return () => clearInterval(timerId);
 }, []);
 return (
  <div className="imgSlide">
   {images.map((image, index) => (
    <img
     key={index}
     src={image}
     alt={`画像No:${index + 1}`}
     className={`imgSlideItem ${currentIndex === index ? 'isActive' : ''}`}
     width={640}
     height={480}
    />
   ))}
  </div>
 );
}
export default ImageSlider;

CSSは以下の通りで画像の表示制御をします。transitionプロパティで透明度のスピードなどを制御します。標準では画像の透明度のopacityプロパティを0で不可視にしておき、アクティブになったらclass属性を付与し可視させます。

.imgSlide{
 position: relative;
 width: 640px;
 height: 480px;
 overflow: hidden;
}
.imgSlideItem{
 display: block;
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 object-fit: cover;
 opacity: 0; /* 非アクティブ時は画像の透明度を0 */
 transition: opacity 1s ease-in-out; /* 1秒かけてフェードイン・アウト */
}
.imgSlideItem.isActive{
 opacity: 1; /* アクティブ時は画像の透明度を1 */
}

コンポーネント化して任意の位置に埋め込むのがいいでしょう。

特定のHTML要素以外をクリックした時にイベントを発火

2025/08/13 (水) - 09:00 JavaScript

Webページで特定の要素をクリックした時と、それ以外の要素をクリックした時とで処理を分岐したい…。例えば親以上の要素にイベントハンドラを設定してしまうと、自身の要素もイベントハンドラの対象になってしまうのでそれを防ぎたい。

モーダルウィンドウなどで指定した要素以外をクリックしたらイベントを発火させたい場合は以下のように実装します。closestメソッドを使うと指定したセレクタ自身、または最も近い親要素を取得できます。targetと組み合わせて指定したセレクタが含まれなかったら〜で判別します。HTMLの例は以下の通り。

<div id="modal">モーダルウィンドウ</div>

JavaScript

document.addEventListener('click', function(e){
 if(!e.target.closest('#modal')) {
  console.log('モーダル以外をクリック!');
 } else {
  console.log('モーダルをクリック!');
 }
});

jQueryで似たようなことを再現する場合

$(document).on('click',function(e) {
 if(!$(e.target).closest('#modal').length) {
  console.log('モーダル以外をクリック!');
 } else {
  console.log('モーダルをクリック!');
 }
});

Reactで似たようなことを再現する場合

useRefを使うことでDOMを参照し、イベントハンドラを実行できるようにします。

import React, { useEffect, useRef } from 'react';
function App() {
 const modalRef = useRef<HTMLDivElement | null>(null);
 useEffect(() => {
  const handleClickOutside = (e: MouseEvent) => {
   if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
    console.log('モーダル以外をクリック!');
   } else {
    console.log('モーダルをクリック!');
   }
  };
  document.addEventListener('click', handleClickOutside);
 }, []);
 return (
  <div id="modal" ref={modalRef}>モーダルウィンドウ</div>
 );
}
export default App;

これを使うことで例えば、モーダルウィンドウやドロップダウンメニュー以外の外側をクリックしたら要素を非表示にする…など実現できます。

Movable Type(MT)で年ごと分割した月別アーカイブリストを実装

2025/08/01 (金) - 09:00 Program

Movable Type(MT)で以下のように年度ごとで見出しを分け、月ごとのアーカイブリストを作成する場合の実装方法。

Movable Type実装イメージ

環境は以下の通り。

  • Movable Type 8.x

実装するため、予め月別アーカイブを出力するように設定したうえでMovable Typeのデザインテンプレートの任意のモジュール等に以下のコードを入れて再構築する。

<MTSetVar name="current_year" value="0">
<MTArchiveList archive_type="Monthly" sort_order="descend">
 <MTSetVarBlock name="entry_year"><MTArchiveDate format="%Y"></MTSetVarBlock>
 <MTIf name="entry_year" ne="$current_year">
  <MTIf name="current_year" ne="0">
</ul>
  </MTIf>
<h2><MTArchiveDate format="%Y年"></h2>
<ul>
 <MTSetVar name="current_year" value="$entry_year">
 </MTIf>
 <li><a href="<MTArchiveLink>"><MTArchiveDate format="%Y年%m月"></a></li>
 <MTIf name="__last__">
</ul>
 </MTIf>
</MTArchiveList>

マークアップやデザインはサイトに合わせて調整すること。

Dockerを使いローカルでWordPress動かす(nginx/PHP/MySQL)

2025/07/18 (金) - 09:00 Server

DockerでLAMPを構築しWordPress 6.8を動かしたときのメモ。あらかじめDockerの環境をインストールしている前提で、環境は以下の通りです。

  • nginx 1.29
  • PHP 8.3
  • MySQL 8.0

ファイル構成は以下の通り作成します。

[作業ディレクトリ]
├── docker-compose.yml
├── app
│   └── WordPressのファイル群
├── nginx
│   └── default.conf
└── php
 └── Dockerfile

appディレクトリにはWordPressの第るを用意しておきます。MySQLの基本情報(データベース名、ユーザ名、パスワードなど)やブラウザでアクセスする際のnginxのポート番号を指定します。こちらは今回8080とし、http://localhost:8080でアクセスできるようにします。

Dockerの設定

docker-compose.ymlに以下の通り記述します。

version: '3.8'
services:
 # MySQL
 mysql:
 image: mysql:8.0
 container_name: wp_mysql
 environment:
  MYSQL_ROOT_PASSWORD: root_password #MySQLのrootパスワード
  MYSQL_DATABASE: wp_db #MySQLのDBパスワード
  MYSQL_USER: wp_user #MySQLのDBユーザ
  MYSQL_PASSWORD: wp_password #MySQLのパスワード
 volumes:
  - db_data:/var/lib/mysql
 restart: unless-stopped
 # PHP(PHP-FPM)
 php:
 build: ./php
 container_name: wp_php
 volumes:
  - ./app:/var/www/html
 expose:
  - 9000
 restart: unless-stopped
 # Nginx
 nginx:
 image: nginx:latest
 container_name: wp_nginx
 volumes:
  - ./app:/var/www/html
  - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
 ## ブラウザでアクセスする際のポート番号
 ports:
  - "8080:80"
 depends_on:
  - php
 restart: unless-stopped
volumes:
 db_data:

nginxの設定

nginx/default.confでWebサーバーの設定をします。ディレクトリの設定やphp-fpmとの連携などを行います。

server {
 listen 80;
 server_name localhost;
 root /var/www/html;
 index index.php index.html index.htm;
 location / {
  try_files $uri $uri/ /index.php?$args;
 }
 location ~ \.php$ {
  # PHPコンテナ(docker-compose.ymlのサービス名)とポートを合わせる
  fastcgi_pass php:9000;
  fastcgi_index index.php;
  include fastcgi_params;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_param PATH_INFO $fastcgi_path_info;
 }
 location ~ /\. {
  deny all;
 }
}

PHPの設定

php/Dockerfileに以下を記述します。WordPressの動作に必要なモジュールや実行ユーザ名を設定します。

FROM php:8.3-fpm
# WordPressに必要なPHP拡張モジュール
RUN apt-get update && apt-get install -y \
 libfreetype6-dev \
 libjpeg62-turbo-dev \
 libpng-dev \
 libzip-dev \
 libonig-dev \
 && docker-php-ext-configure gd --with-freetype --with-jpeg \
 && docker-php-ext-install -j$(nproc) gd mysqli pdo_mysql zip mbstring opcache
WORKDIR /var/www/html
USER www-data

Dockerの起動

すべての作業が終わったら、作業ディレクトリに移動しDockerのコンテナを実行します。

$ cd 作業ディレクトリ(置き換えてね)
$ docker-compose up -d

正常に起動したらhttp://localhost:8080にアクセスするとインストールが起動するので、通常通りインストールします。MySQLの設定は冒頭のdocker-compose.ymlの設定を割り当てます。

WordPress動作イメージ

起動した実行例。

Dockerの停止

Dockerを停止する場合は以下のコマンドを実行します。

$ docker-compose down

WordPressの投稿内にウィジェットを配置する

2025/07/14 (月) - 09:00 Program

WordPressの投稿や固定ページの本文内に任意のウィジェットを挿入したい場合。まず、あらかじめにWordPressのfunction.phpにウィジェットの作成をし、WordPress管理画面の[外観]→[ウィジェット]からウィジェットの設定しておきます。

if(function_exists('register_sidebar')) {
 register_sidebar(array(
  'name' => 'ウィジェットのタイトル' ,
  'id' => 'ウィジェットのID', //半角英数小文字
  'description' => 'ウィジェットの説明文',
  'before_widget' => '<aside>',
  'after_widget'  => '</aside>',
  'before_title'  => '<h2>',
  'after_title'   => '</h2>'
 ));
}

さらにfunction.phpにショートコードでウィジェットを呼び出す関数を作成します。

function widget_post_func($atts) {
 $atts = shortcode_atts(array(
  'id' => '',
 ), $atts, 'widget');
 if (empty($atts['id'])) {
  return false;
 }
 ob_start();
 dynamic_sidebar($atts['id']);
 $widget_content = ob_get_clean();
 return $widget_content;
}
add_shortcode('widget', 'widget_post_func');

最後にWordPressの投稿画面の任意の位置にショートコードを埋め込みます。idの属性には設定したウィジェットのIDを入れておきます。

[widget id="ウィジェットのID"]

プレビューすると表示されると思います。ウィジェットを更新するとページや記事に埋め込んだすべてのウィジェットが一括更新されるので便利です。

ウィジェットを表示した例

例えば広告バナーやリンク集、CTAなどを設置したい場合に有効です。

OpenAI APIとNext.js(React)でGPTの回答をマークダウン(Markdown)で取得する

2025/06/02 (月) - 09:00 JavaScript

Next.js(AppRouter)とOpenAI APIを利用して、テキストフォームから入力した質問の回答をマークダウン形式で取得し、HTMLで表示するサンプル。Next.jsやReactでマークダウンを扱う場合はreact-markdownのモジュールを使うと便利です。

$ npm i react-markdown

予めモジュールを入れておきましょう。あとはOpenAI PlatformでAPIキーの発行とクレジットの確保も忘れずに。今回は以下の環境下で実装しています。

  • Next.js 14.2.x (AppRouter)
  • React 18.3.x
  • react-markdown 1.9.x

取得したAPIキーは.envで設定します。

API_KEY=sk-********************

フロント部分のpage.tsxで質問フォームと表示エリアを実装。ポイントは回答部分をReactMarkdownでマークアップすることです。これによりマークダウン形式のテキストがHTMLに変換されて表示されます。

'use client';
import { useState } from 'react';
import ReactMarkdown from 'react-markdown';
export default function Home() {
 const [prompt, setPrompt] = useState('');
 const [response, setResponse] = useState('');
 const [loading, setLoading] = useState(false);
 const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  setLoading(true);
  const res = await fetch('/api/chatgpt', {
   method: "POST",
   headers: {
    'Content-Type': 'application/json',
   },
   body: JSON.stringify({ prompt })
  });
  const data = await res.json();
  setResponse(data.text);
  setLoading(false);
 };
 return (
  <main>
   <form onSubmit={handleSubmit}>
    <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="質問を入力してください"></textarea>
    <button type="submit" disabled={loading}>質問する</button>
   </form>
   {loading && <p>生成中…</p>}
   {response && (
    <div>
     <ReactMarkdown>{response}</ReactMarkdown>
    </div>
   )}
  </main>
 );
}

API部分の実装。Next.jsのAPI Routesを利用して実装しました。srcディレクトリに/api/chatgpt/route.tsを配置し実装します。フロントからリクエストされたされたプロンプトをPOSTで処理します。

import { NextResponse } from 'next/server';
export async function POST(req: Request) {
 try {
  const { prompt } = await req.json();
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
   method: 'POST',
   headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.API_KEY}`,
   },
   body: JSON.stringify({
    model: 'gpt-4o-mini', //モデルを選択
    messages: [
     { role: 'user', content: prompt }
    ],
   }),
  });
  const data = await response.json();
  const text = data.choices[0].message?.content?.trim() || '';
  return NextResponse.json({ text });
 } catch (error) {
  console.error(API エラー:', error);
  return NextResponse.json({ error: '生成に失敗しました' }, { status: 500 });
 }
}

実際に実装し質問を実行した際の表示例。蔦屋重三郎の生い立ちについて質問し正常にHTMLフォーマットで回答文が表示されています。

回答結果

フロントのCSS側はマークダウンで再現できる一般的なHTML要素(見出し、リスト、table等)が綺麗に見やすく表示されるように整えておく必要があります。

ページの先頭へ