さくらのレンタルサーバーのphpMyAdminで巨大なSQLファイルをインポートできない時

2025/05/09 (金) - 09:00 Server

さくらのレンタルサーバーのスタンダードプラン以上では標準でMySQLが搭載されており、簡単にデータベースを扱えます。それに伴いデータベース管理ツールのphpMyAdminも装備されておりGUIでデータベース操作が行え、外部からSQLのファイルをインポートできます。しかしさくらのレンサバ標準のphpMyAdminでインポートできるファイルの上限は32MBまでのようです。

数百MBあるような巨大なSQLファイルを読み込ませることができません。SQLファイルをdata.sql.zipのようなファイルの命名規則でzipで圧縮し、32MB以下まで落としてアップもできますが、展開とインポートに時間がかかってタイムアウトすることがあります。そこでどうするか?考えました。

phpMyAdminのインポート画面

自前でphpMyAdminをインストールする

借りているサーバーに自分でphpMyAdminをインストールし、尚且つphp.iniなどでupload_max_filesizeの値を256MBなどに引き上げて、SQLファイルをアップロードできるようにします。

インポートには時間がかかるので、set_time_limitで実行時間を引き上げたりmemory_limitでメモリの上限を上げる必要もあります。ただしあくまで共用のレンタルサーバのためあまり高負荷をかけ続けるとアカウントの利用制限などに抵触する可能性もあるため注意が必要です。

SSHでサーバに接続してSQLファイルをインポートする

一部サーバーコマンドやSQLコマンドの知識が必要ですがこれが一番確実です。予めインポートしたいSQLファイルをレンタルサーバの任意の場所にアップロードしてきます。次にさくらのレンタルサーバにSSHで接続します。ログインパスワードはFTPと同じパスワードです。

ssh ユーザー名@ユーザー名.sakura.ne.jp

ログインしたらMySQLにログインします。予めさくらのレンタルサーバのコントロールパネルよりデータベースとユーザ名を作成しておきます。レンタルサーバーのコントロールパネルからデータベースの収容サーバ名(mysql****.db.sakura.ne.jp)も控えておきます。パスワードはデータベースを発行したときに設定したデータベースのパスワードです。

mysql -u データベースユーザー名 -h mysql****.db.sakura.ne.jp -p

ログインしたらデータベースを選択します。show databasesでデータベースがあることを確認し、useコマンドで使用するデータベースを選択します。

show databases;
〜データベース一覧が表示される〜
use 使用するデータベース名;

先ほどアップロードしたSQLファイルをインポートします。データベースの構造にもよりますが、数万レコードあるSQLでも1分もかからず終わるはずです。

source /home/ユーザー名/www/data.sql

インポートが終わると入力待ち状態になるので、MySQLを終了します。

exit;

さきほどレンタルサーバにアップロードしたSQLファイルは不要なので削除しておきます。

rm /home/ユーザー名/www/data.sql

さらにサーバからログアウトします。

exit

この方法のがおそらくインポートを失敗せずに行けると思います。

AWS CloudFrontで特定ファイルのみキャッシュを無効にする

2025/05/02 (金) - 09:00 Server

CloudFrontでウェブサイトを配信するとき、特定のファイルのみのキャッシュを無効化したいときの設定方法。例えば頻繁に内容が変わるHTMLファイルはキャッシュせず、更新頻度が少なかったり重いデータの画像やCSS・JavaScriptなどはCloudFrontでキャッシュさせたいときなど。

AWS CloudFrontの設定

今回は例として、HTMLファイルだけキャッシュの除外をする設定とします。

AWS CloudFrontから該当のディストリビューションを選択し、[ビヘイビア]に移動。[ビヘイビアを作成]を選択し新規作成します。[パスパターン]に*.htmlと入力。[オリジンとオリジングループ]でオリジンを選択してから、他のオプションは以下のように設定。

CloudFront Behavior

* オブジェクトを自動的に圧縮:Yes
* ビューワープロトコルポリシー:Redirect HTTP to HTTPS
* 許可された HTTP メソッド:GET, HEAD
* ビューワーのアクセスを制限する:No
* キャッシュキーとオリジンリクエスト:Legacy cache settings
* ヘッダー:なし
* クエリ文字列:すべて
* cookie:すべて
* オブジェクトキャッシュ:Customize
* 最小 TTL:0
* 最大 TTL:0
* デフォルト TTL:0

すべてのTTLを0にすることで、キャッシュされずに読み込まれます。上記を設定したら保存します。

設定の反映の確認方法

Webブラウザのネットワークヘッダーで確認できます。ChromeのWeb開発者ツールであれば[Network]からファイルを選択しレスポンスヘッダーのX-Cacheから確認できます。

X-Cache:Hit from cloudfront

CloudFrontのキャッシュになっている場合は、X-CacheHit from cloudfrontになります。今回の場合CSSや画像ファイルはキャッシュされます。

X-Cache:Miss from cloudfront

一方キャッシュされていない場合はX-CacheMiss from cloudfrontの値になります。これはキャッシュされずにオリジンから呼び出しているものになります。

注意点としては、該当のファイルはキャッシュされないためオリジンに負荷がかかるということです。S3やEC2から呼び出す場合は転送量やプロセッサの振る舞いに注意しましょう。

Claude APIでWordPressの記事を翻訳する

2025/04/28 (月) - 09:00 PHP&CMS

今回はWordPressの記事タイトルと本文を生成AIを組み合わせて自動的に機械翻訳する機能を簡易的に実装しました。記事詳細ページに「英訳を見る」ボタンを配置し、押下したら英訳ページに遷移しClaude APIを利用し翻訳します。

ブラウザの機能やGoogleなどの翻訳サービスで簡単かつ無料で全文翻訳出来るので不要と言えば不要ですがネタ的に。

実装ポイントとしては以下。

  • 翻訳に時間とAPI使用料金が取られるので初回のみ翻訳
  • 2回目以降は翻訳内容をキャッシュして表示
  • 翻訳ページへの切り替えはクエリパラメータで判断
  • アクセス解析の時にパラメータの有無で翻訳ページのアクセス数を判断できる

翻訳前のブログ記事

翻訳前のブログ記事の表示(日本語)。デメリットは以下の通り

  • 翻訳内容の精度は元の文章の品質とAIモデルに依存する
  • あまり長い文章は非対応(max-tokenを超えると文章が途中で切れる)
  • APIの使用利用料金に注意

あらかじめAnthropic ConsoleでAPIキーの取得と課金は済ませておきましょう。

翻訳後のブログ記事

翻訳後のブログ記事の表示(英語)。環境は以下の通り

  • WordPress 6.5
  • PHP 8.2

細かいエラーやセキュリティ処理はしていないので、参考までに。また一部マークアップが崩れる可能性もあるのでその点も注意しましょう。

コード例

function.phpに以下を記述。指定したクエリパラメータの記事詳細にアクセスすると初回翻訳が実行されます。1度翻訳した内容はカスタムフィールドに格納するため、2回以降はカスタムフィールドの翻訳内容を表示します。

function translationContent(){
 $API = 'https://api.anthropic.com/v1/messages';
 $API_KEY = '取得したAPIキーを設定';
 if( get_post_meta(get_the_ID(), 'translation_en') ) {
  //翻訳後ならキャッシュを表示
  $the_content = get_post_meta(get_the_ID(),'translation_en',true);
  return $the_content;
 }
 //翻訳前なら翻訳を実行
 $system = '指定したテキストを英語で翻訳する。HTMLタグは変えずにテキスト内容だけ翻訳する。';
 $single_title = get_the_title();
 $single_date = get_the_time('Y/m/d (D) - H:i');
 $single_content = get_the_content();
 $content = "<h1>{$single_title}</h1><p><time>{$single_date}</time></p>{$single_content}";
 $data = json_encode([
  'model' => 'claude-3-5-haiku-20241022', //モデルを選択
  'max_tokens' => 8192, //最大トークン(モデルに依存)
  'system' => $system,
  'messages' => [
   ['role' => 'user', 'content' => $content]
  ]
 ]);
 $ch = curl_init($API);
 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
 curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 curl_setopt($ch, CURLOPT_HTTPHEADER, [
  'Content-Type: application/json',
  'anthropic-version: 2023-06-01',
  'x-api-key: '.$API_KEY
 ]);  
 $result = curl_exec($ch);
 $httpcode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
 curl_close($ch);
 $output = json_decode($result, true);
 if($httpcode===200){
  $contents = $output['content'][0]['text'];
  update_post_meta(get_the_ID(), 'translation_en', $contents );
  return $contents;
 }else{
  return 'エラー: '.$result;
 }
}

single.phpの任意の位置に以下を記述。クエリパラメータの有無で本来の日本語の記事と、翻訳の記事を分岐表示します。このとき英訳のページへの同線を設置します。

<?php
if($_GET['lang']==='en'){
 echo translationContent();
}else{
?>
 <h1><?php the_title(); ?></h1>
 <p><time><?php the_time('Y/m/d (D) - H:i') ?></time></p>
 <p><a href="<?php the_permalink() ?>?lang=en">この記事を英語で見る(English)</a></p>
<?php
 the_content();
}
?>

もし翻訳内容を変えたい場合はカスタムフィールドを直接変更します。また翻訳内容をリセットして再翻訳したい場合はカスタムフィールドを削除して保存した後、再度翻訳をします。

本格的に翻訳記事を用意したり、機密情報を扱うのであれば専門の翻訳業者に依頼し、WordPressの多言語プラグインを利用するのが最適です。しかし生成AIの翻訳精度もかなりの物ですね。

[GPT4o,DALL-E 3,PHP] 指定画像と似たような画像を自動生成する

2025/04/11 (金) - 09:00 PHP&CMS

GPT4oで画像の解析や生成がパワーアップして、かなり自然なイラストや画像が出来るようになって「この写真の人物をジブリ風にして!」みたいなことがさらに簡単にできるようになりました。今回はGPTで指定した画像を解析し、さらにその画像に近い画像をDALL-E 3で生成できるようなものをPHPで作ってみました。

猫の生成イメージ

まずchatGPTのAPIのGPT4oのモデルを使って指定した画像の分析を行い、解析結果をもとに生成プロンプトを出力させます。次にAPIのDALL-Eのモデルを使ってプロンプトを元に画像を生成します。

<?php
define('API_KEY', '{GPTのAPIキー}');
define('API_CHAT', 'https://api.openai.com/v1/chat/completions');
define('API_IMG', 'https://api.openai.com/v1/images/generations');
$base_url = '{画像のURL}';
$image_url = '';
function analyzeImage($base_url) {
 $imageData = file_get_contents($base_url);
 $base64Image = base64_encode($imageData);
 $dataUrl = 'data:image/jpeg;base64,' . $base64Image; //画像の形式によって変更
 $headers = [
  'Content-Type: application/json',
  'Authorization: Bearer ' . API_KEY,
 ];
 $data = [
  'model' => 'gpt-4o-mini',
  'messages' => [
   [
    'role' => 'user',
    'content' => [
     [
      'type' => 'text',
      'text' => 'この画像と同じ画像を生成したいので、プロンプトを生成してください'
     ],
     [
      'type' => 'image_url',
      'image_url' => ['url' => $dataUrl]
     ]
    ]
   ]
  ],
  'max_tokens' => 1000,
 ];
 $ch = curl_init(API_CHAT);
 curl_setopt($ch, CURLOPT_POST, true);
 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 $response = curl_exec($ch);
 $responseCode = curl_getinfo($ch,CURLINFO_RESPONSE_CODE);
 curl_close($ch);
 if($responseCode === 200){
  $responseBody = json_decode($response, true);
  $responseMessage = $responseBody['choices'][0]['message']['content'];
  imageGenerator($responseMessage,$base_url);
 }
}
function imageGenerator($prompt,$base_url){
 $headers = [
  'Content-Type: application/json',
  'Authorization: Bearer ' . API_KEY,
 ];
 $data = [
  'model' => 'dall-e-3',
  'prompt' => $prompt,
  'n' => 1, //生成する画像の個数
  'size' => '1024x1024',
 ]
 $ch = curl_init(API_IMG);
 curl_setopt($ch, CURLOPT_POST, true);
 curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
 curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 $response = curl_exec($ch);
 $responseCode = curl_getinfo($ch,CURLINFO_RESPONSE_CODE);
 curl_close($ch);
 if($responseCode === 200){
  $result = json_decode($response, true);
  $image_url = $result['data'][0]['url'];
  echo '<p>元画像:<img src="'.$base_url.'" alt="" loading="lazy"></p>';
  echo '<p>生成画像:<img src="'.$image_url.'" alt="" loading="lazy"></p>';
 }
}
analyzeImage($base_url);
?>

画像の解析はURL指定だとうまく行かなかったので、base64にエンコードして指定しました。

コードは汚いけど上記のような感じです。実行すると画像の解析と画像の生成によりすこし待たされますし課金もされるので、生成する個数は少ない方が良いかもしれないです。また正常に解析や生成できなかった場合のエラー処理なども省いています。

人物の生成イメージ

結果については元画像の内容やガチャのようなところもあるので、実際はプロンプトの出力まで行ってからimageFXやStable Diffusionで納得いくまで生成を試した方が効率的かもしれません。

Next.js[App Router]&microCMSでRSSフィードを実装する

2025/03/31 (月) - 09:00 JavaScript

microCMSとNext.js[App Router]でRSSを実装してみる。RSSはブログやニュース配信サイトでは必ず実装しておくべきものです。今回は以下の環境で実装します。

  • Next.js 14.2.x
  • microcms-js-sdk 3.1.x

.envで、サイト名、サイトの説明文、サイトのドメイン、microCMSのAPIキーを設定をしておきます。

SITE_URL=https://example.com
SITE_NAME=ゆかちぃのブログ
SITE_DISCRIPTION=ゆかちぃの日々を書き残すブログです♪
API_KEY={APIキー}

src/feed/route.ts に以下のように実装。タイトル、パーマリンク、サマリー、公開日の最低限の要素のみ実装しています。

APIスキーマは以下を想定します。

  • 記事タイトル:title
  • 本文:content
  • パーマリンク:id
  • 公開日:publishedAt
import { NextResponse } from 'next/server';
import { client } from '../libs/Client.js';
import { format } from 'date-fns';
export async function GET() {
const posts = await client.get({
 endpoint: 'post',
});
 const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
 <channel>
  <title>${process.env.SITE_NAME}</title>
  <link>${process.env.SITE_URL}</link>
  <description>${process.env.SITE_DISCRIPTION}</description>
  ${posts.contents.map((post) => `
  <item>
   <title>${sanitizeStr(post.title)}</title>
   <link>${process.env.SITE_URL}/${post.id}</link>
   <description><![CDATA[${post.content}]]></description>
   <pubDate>${format(new Date(post.publishedAt), 'EEE, dd MMM yyyy HH:mm:ss xx')}</pubDate>
  </item>`).join('')}
 </channel>
</rss>`;
 return new NextResponse(rss, {
  headers: {
   'Content-Type': 'application/rss+xml',
  },
 });
}
function sanitizeStr(str: string): string {
 return String(str).replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/'/g,"&apos;").replace(/"/g,"&quot;")
}

また、出力したRSSをすべてのページのhead要素内に設定し、このサイトではRSS配信しているよ!ということを認識させましょう。

<link rel="alternate" type="application/rss+xml" href="https://example.com/feed/" />

[Next.js+microCMS] 1ページ内でカテゴリごと記事一覧を表示

2025/03/21 (金) - 09:00 JavaScriptPHP&CMS

Jamstackの開発が活発になり、ヘッドレスCMSとして和製のmicroCMSを採用するケースもあるかと思います。今回はmicroCMSとNext.jsを使って記事カテゴリごとに記事一覧を分割して表示させる方法をメモします。

Next.js+microCMS 作成イメージ

記事カテゴリAPIと記事APIの2つを使います。記事のAPIスキーマにはタイトルや記事本文のほか、カテゴリ用のフィールドを用意しコンテンツ参照で記事カテゴリAPIを参照します。また開発にはmicrocms-js-sdkを使っています。今回は社員のスタッフリストをつくりました。役職ごとカテゴリを作りその下に所属するスタッフを一覧で表示します。

今回は下記の環境にて作成。

  • Node.js 20.18.x
  • Next.js 14.2.x
  • microcms-js-sdk 3.1.x

microCMSカテゴリ管理

スタッフリスト部分はコンポーネントとしてimportします。スタッフ一覧ページのファイルを/staff/page.jsとします。

import { client } from '@/libs/client';
import StaffList from '@/component/StaffList';
export default async function Home() {
 const category = await client.get({
 endpoint: 'staff_category',
 });
 return (
 <div>
 {category.contents.map((category) => (
  <article key={category.id}>
   <h2>{category.name}一覧</h2>
   <StaffList id={category.id} />
  </article>
 ))}
 </div>
 );
}

スタッフ一覧コンポーネントとして/component/StaffList.js用意します。

import Link from 'next/link';
import { client } from '@/libs/client';
import { format } from 'date-fns';
export default async function StaffList({id}){
 const category_staff = await client.get({
 endpoint: 'post',
 queries: { filters: `category[equals]${id}`}
 });
 return (
 <ul>
 {category_staff.contents.map((staff_data) => (
  <li key={staff_data.id}>
  <Link href={`/staff/${staff_data.id}`}>
  <img src={staff_data.eyecatch.url} alt="" loading="lazy" width="100" height="100" />
   <h3>{staff_data.title}</h3>
   <p><time>入社日:{format(new Date(staff_data.date), 'yyyy.MM')}</time></p>
  </Link>
  </li>
 ))}
 </ul>
 );
}

スタッフ詳細ページとして/staff/[id]/page.js用意します。

import { client } from '@/libs/client';
import { format } from 'date-fns';
export async function generateStaticParams() {
 const data = await client.get({
  endpoint: 'post',
 });
 return data.contents.map((data) => {
  return { id: data.id}
 })
}
export default async function DetailPage( props ) {
 const data = await client.get({
  endpoint: `post/${props.params.id}`,
 });
 return (
  <div>
   <p>{data.category.name}</p>
   <h1>{data.title}</h1>
   <img src={data.eyecatch.url} alt="" loading="lazy" width="100" height="100" />
   <p><time>入社日:{format(new Date(data.date), 'yyyy.MM')}</time></p>
   <div dangerouslySetInnerHTML={{ __html: data.content }}></div>
  </div>
 );
}

所属部署(カテゴリ)、名前、顔写真、入社日、自己紹介メッセージが表示されました。

詳細画面イメージ

コードの細かい内容についてはそれぞれのCMSに合わせてください。ところで、microCMSって2025年6月10日から値上がりするんですよね。むむー…

Astro&WordPressで記事一覧(ページネーション)と記事詳細ページの実装

2025/03/14 (金) - 09:00 JavaScriptPHP&CMS

以前に副業でWordPressとAstroでヘッドレスCMSでブログを実装しましたのでそのメモです。取り急ぎ記事一覧と記事詳細のみでファイル構成は最低限のものだけ。

src
├─ components
│    └─ Pagination.astro
├─ layouts
│    └─ Layout.astro
└─ pages
     ├─ [slug].astro
     ├─ index.astro
     └─ page
         └─ [paged].astro

WordPressの記事の呼び出しは、WordPressのREST APIを使います。

WordPress連携実装例

REST APIのエンドポイントは.envに記載しておきます。ついでにブログ名も設定しておくと一括で変更できます。

BLOG_TITLE=ひなちゃんの日記
API_JSON=https://example.com/wp-json/wp/v2/posts/

レイアウトを定義するにLayout.astroに大枠のHTMLとブラウザに表示されるタイトルのtitle要素の中身を取得するコードを記載します。必要最低限の記載のみです。

---
const {meta} = Astro.props;
---
<!doctype html>
<html lang="ja">
 <head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
  <title>{meta.title}</title>
 </head>
 <body>
  <slot />
 </body>
</html>

トップページの記事一覧をindex.astroに実装します。ブラウザに表示されるタイトルは[トップページ – ブログ名]となります。

---
import { format } from 'date-fns';
import Layout from '../layouts/Layout.astro';
import Pagination from '../components/Pagination.astro';
const res = await fetch(`${import.meta.env.API_JSON}`);
const posts = await res.json();
const total = await fetch(import.meta.env.API_JSON).then((res) => {return res.headers.get('X-WP-TotalPages')});
const page: {
 current: number,
 total: number 
} = {
 current: 1,
 total: Number(total)
}
const meta: {
 title: string
} = {
 title: `トップページ - ${import.meta.env.BLOG_TITLE}`
}
---
<Layout meta={meta}>
 <main>
 {posts.map((post: { slug: string; title: { rendered: string }; content: { rendered: string }; date: string }) => (
 <article><a href={`/${post.slug}`}>
  <h2>{post.title.rendered}</h2>
  <p>{format(new Date(post.date), 'yyyy年MM月dd日')}</p>
 </a></article>
 ))}
 <Pagination page={page} />
</main>
</Layout>

ページネーションの記事一覧は/page/[paged].astroで動的パスで取得し、getStaticPathsで生成します。ブラウザに表示されるタイトルは[nページ目 – ブログ名]となります。

---
import { format } from 'date-fns';
import Layout from '../../layouts/Layout.astro';
import Pagination from '../../components/Pagination.astro';
export async function getStaticPaths(){
 const total = await fetch(import.meta.env.API_JSON).
 then((res) => {return Number(res.headers.get('X-WP-TotalPages'))});
 return Array.from({ length: total }, (num,i) => ({
  params: { paged: `${i + 1}` }
 }))
}
const {paged} = Astro.params;
const res = await fetch(`${import.meta.env.API_JSON}?page=${paged}`);
const posts = await res.json();
const total = await fetch(import.meta.env.API_JSON).then((res) => {return res.headers.get('X-WP-TotalPages')});
const page: {
 current: number,
 total: number 
} = {
 current: Number(paged),
 total: Number(total)
}
const meta: {
 title: string
} = {
 title: `${paged}ページ目 - ${import.meta.env.BLOG_TITLE}`
}
---
<Layout meta={meta}>
 <main>
 {posts.map((post: { slug: string; title: { rendered: string }; content: { rendered: string }; date: string }) => (
  <article><a href={`/${post.slug}`}>
   <h2>{post.title.rendered}</h2>
   <p>{format(new Date(post.date), 'yyyy年MM月dd日')}</p>
  </a></article>
 ))}
 <Pagination page={page} />
</main>
</Layout>

ページネーション部分は別コンポーネントで管理しました。components/Pagination.astroで実装し呼び出します。

---
const { page } = Astro.props;
const prev = page.current-1;
const next = page.current+1;
---
<p>{page.current} / {page.total}</p>
<p>
{page.current!=1 && (
 <a href={`/page/${String(prev)}`}>前のページへ</a>
)}
{page.current!=page.total && (
 <a href={`/page/${String(next)}`}>次のページへ</a>
)}
</p>

記事詳細は[slug].astroで動的パスで取得し、getStaticPathsで生成します。ブラウザに表示されるタイトルは[記事タイトル – ブログ名]となります。

---
import { format } from 'date-fns';
import Layout from '../layouts/Layout.astro';
export async function getStaticPaths(){
 const res = await fetch(import.meta.env.API_JSON);
 const posts = await res.json();
 const paths = posts.map((post: { slug: string }) => ({
  params: { slug: post.slug }
 }));
 return paths;
}
const {slug} = Astro.params;
const post_res = await fetch(`${import.meta.env.API_JSON}?slug=${slug}`);
const [post] = await post_res.json();
const meta: {
 title: string
} = {
 title: `${post.title.rendered} - ${import.meta.env.BLOG_TITLE}`
}
---
<Layout meta={meta}>
 <main>
  <h1>{post.title.rendered}</h1>
  <p>{format(new Date(post.date), 'yyyy年MM月dd日')}</p>
  <div set:html={post.content.rendered} />
 </main>
</Layout>

実装し終わったらプレビューしてテストします。

$ npm run dev

CSSでのデザイン実装や、metaタグ、フロント周りのマークアップで整えれば出来上がり。

関連記事

OpenAI API(DALL·E 3)とNext.js(React)で画像生成プログラム

2025/03/10 (月) - 09:00 JavaScript

Next.js(AppRouter)とOpenAI APIを利用して、テキストフォームから入力した画像を生成するプログラムを作成してみました。今回はDALL·E 3のモデルを利用しています。予めOpenAI PlatformでAPIキーの発行とクレジットを確保しておきましょう。取得したAPIキーは.envで設定します。

API_KEY=取得したAPIキー

生成された画像例

また今回は勉強がてらAPI RoutesとAxiosを使って実装を試みました。実装は以下の環境にて。

  • Next.js 14.2.x (AppRouter)
  • React 18.3.x
  • Axios 1.7.x

まず、page.jsに以下のようにフロントエンドを実装。プロンプトのテキストボックスと、作風を選ぶラジオボタンを配置しました。ここのHTMLやCSSは適当に。

'use client';
import { useState } from 'react';
import axios from 'axios';
export default function Home() {
 const [prompt, setPrompt] = useState('');
 const [type, setType] = useState('写真風');
 const [imageUrl, setImageUrl] = useState('');
 const [message, setMessage] = useState('');
 const [loading, setLoading] = useState(false);
 const generateImage = async () => {
  setLoading(true); //生成ボタンを非活性
  setImageUrl('');
  setMessage('');
  try {
   const requestPrompt = `${prompt} ${type}で生成してください`;
   const response = await axios.post('/api/imagegenerate', { prompt : requestPrompt });
   setImageUrl(response.data.imageUrl);
   setMessage('画像ができました!');
  } catch (error) {
   setMessage(`画像が生成できませんでした:${error}`);
  }
  setLoading(false); //生成ボタンを活性
 };
 return (
  <div>
   <label htmlFor="type1"><input type="radio" name="type" id="type1" onChange={(e) => setType(e.target.value)} value="写真風" defaultChecked="true" />写真風</label>
   <label htmlFor="type2"><input type="radio" name="type" id="type2" onChange={(e) => setType(e.target.value)} value="イラスト風" />イラスト風</label>
   <label htmlFor="type3"><input type="radio" name="type" id="type3" onChange={(e) => setType(e.target.value)} value="水彩画風" />水彩画風</label>
   <hr />
   <textarea name="prompt" id="prompt" onChange={(e) => setPrompt(e.target.value)} placeholder="プロンプトを入力" defaultValue={prompt}></textarea>
   <hr />
   <button onClick={generateImage} disabled={loading}>{loading ? '生成中です' : '画像を生成する'}</button>
   <p>{message}</p>
   {imageUrl && (
    <div>
     <h2>生成された画像</h2>
     <img src={imageUrl} alt="生成された画像です" width="1024" height="1024" />
    </div>
   )}
  </div>
 );
}

API面をフロントエンドからのリクエストとDALL·E APIの中継を実装します。ソースディレクトリに/api/imagegenerate/route.jsを配置し、以下のように実装しました。page.jsからPOSTでパラメータでリクエストされたプログラムを処理します。

import { NextResponse } from 'next/server';
import axios from 'axios';
export async function POST(req) {
 try {
  const { prompt } = await req.json();
  if (!prompt) {
   return NextResponse.json({ error: 'プロンプトを入力してください' }, { status: 400 });
  }
  const response = await axios.post(
   'https://api.openai.com/v1/images/generations',
   {
    prompt,
    n: 1,
    size: '1024x1024', //サイズ
    model: 'dall-e-3'
   },
   {
    headers: {
     Authorization: `Bearer ${process.env.API_KEY}`,
     'Content-Type': 'application/json',
    },
   }
  );
  return NextResponse.json({ imageUrl: response.data.data[0].url }, { status: 200 });
 } catch (error) {
  console.error('画像生成エラー:', error.response?.data || error.message);
  return NextResponse.json({ error: '画像生成に失敗しました' }, { status: 500 });
 }
}

イラスト風を選択して生成した場合の表示例。

生成された画像例

プロンプトを色々アレンジすれば面白い画像ジェネレータが作れそうです。

CSSでスクロールバーの色を変更する

2025/03/04 (火) - 09:00 CSS

CSSでスクロールバーの色を変えたり、可視・不可視を変更することが出来ます。デザインの幅が広がりますがアクセシビリティを損なわないように注意しましょう。

html{
 scrollbar-color: #fff #e587cb; /* つまみの色 背景色 */
 scrollbar-width: auto;
}

scrollbar-colorプロパティでスクロールバーのつまみの色と背景色を指定します。scrollbar-widthで幅を指定できます。こちらは以下の値を指定します。

  • auto:デフォルト幅
  • thin:スクロールバーを細くする
  • none:スクロールバーを非表示

スクロールバーの色を変更した例

Safari(Webkit)で指定する場合

古いSafariではまだ未対応なので独自実装のCSSセレクタで指定します。こちらのほうが細かくデザインを指定できます。

html::-webkit-scrollbar {
 width: 15px; /* スクロールバーの横幅 */
}
html::-webkit-scrollbar-track-piece {
 background-color: #e587cb; /* Safari スクロールバーの背景色 */
}
html::-webkit-scrollbar-thumb {
 background-color: #fff; /* Safari スクロールバーのつまみの色 */
 border: 3px solid #e587cb; /* Safari スクロールバーのつまみの枠色 */
 border-radius: 10px;
 background-clip: content-box;
}

スクロールバーを非表示にする場合はdisplay: none;を指定します。

html::-webkit-scrollbar {
 display: none;
}

おまけ

かつて覇権を握っていた憎きIE(Internet Explorer)でも、CSS独自実装でスクロールバーの色を変えたり出来ました。一部のバージョンのWindows Operaでも再現します。

IEでスクロールバーの色を変更した例

IE5とかでサポートされており、他のサイトとデザインの差別化が出来たとか出来ないとか。

WordPress 記事のタグで絞り込み検索を実装

2025/02/28 (金) - 09:00 PHP&CMS

WordPressの記事タグで絞り込み検索をする機能を実装。例えばドラクエの技で「メラ」&「ブレス」、「ヒャド」&「呪文」の条件にあった技だけを検索したいときなどに使える方法。

検索結果

実際に実装した表示例。「デイン」&「斬撃」で検索した結果を表示。実装は以下の環境にて。

  • WordPress 6.5.x
  • PHP 8.2.x

まず、任意の場所に検索エリアを実装。タグを選ぶチェックボックスとテキストエリアを実装します。

<form role="search" method="get" id="search" action="<?php echo esc_url( home_url( '/' ) ); ?>">
 <input type="text" value="<?php echo get_search_query(); ?>" name="s" id="s" placeholder="キーワードを入力" />
<?php
 $tags = get_tags();
 if ( $tags ) {
  foreach ( $tags as $tag ) {
?>
<label for="tag-<?php echo esc_attr( $tag->slug ); ?>"><input type="checkbox" name="tag[]" value="<?php echo esc_attr( $tag->slug ); ?>" id="tag-<?php echo esc_attr( $tag->slug ); ?>"><?php echo esc_html( $tag->name ); ?></label>
<?php
  }
 }
?>
<input type="submit" id="buttonsubmit" name="buttonsubmit" value="検索する" />
</form>

検索結果画面の実装。search.phpの任意の位置に以下を実装します。必要な要素などはWebサイトの内容に合わせて各自で設定ください。

<?php
 $paged = ( get_query_var( 'paged' ) ) ? get_query_var( 'paged' ) : 1;
 $search_query = isset( $_GET['s'] ) ? sanitize_text_field( $_GET['s'] ) : '';
 $search_tags = isset( $_GET['tag'] ) && is_array( $_GET['tag'] ) ? array_map( 'sanitize_text_field', $_GET['tag'] ) : [];
 $args = array(
  's' => $search_query,
  'tag_slug__and' => $search_tags,
  'paged' => $paged,
 );
 $query = new WP_Query( $args );
 if ( $query->have_posts() ) {
?>
<ul>
<?php
  while ( $query->have_posts() ) {
   $query->the_post();
?>
 <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
<?php
  }
?>
</ul>
<?php
  the_posts_pagination( array(
   'total' => $query->max_num_pages,
  ));
  wp_reset_postdata();
 }else{
?>
<p>残念ながら、技は見つかりませんでした。</p>
<?php
 }
?>

あくまで例ですが、店舗情報でサービス内容(駐車場、キャシュレス決済)で絞り込みや、アパレル商品のスペック(サイズやカラー)検索でも活用できます。

ページの先頭へ