KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

4月2日から4月8日までキンドル新刊ピックアップ

4月2日発売の本

4月2日発売のそのほかの本 kyukyunyorituryo.github.io

4月3日発売の本

4月3日発売のそのほかの本 kyukyunyorituryo.github.io

4月4日発売の本

4月4日発売のそのほかの本 kyukyunyorituryo.github.io

4月5日発売の本

現代農業2026年5月号 [雑誌]

現代農業2026年5月号 [雑誌]

  • 農山漁村文化協会(農文協)
Amazon

4月5日発売のそのほかの本 kyukyunyorituryo.github.io

4月6日発売の本

4月6日発売のそのほかの本 kyukyunyorituryo.github.io

4月7日発売の本

4月7日発売のそのほかの本 kyukyunyorituryo.github.io

4月8日発売の本

魔法使いの嫁 24 (ブシロードコミックス)

魔法使いの嫁 24 (ブシロードコミックス)

Amazon

4月8日発売のそのほかの本 kyukyunyorituryo.github.io

AozoraEpub3JSの外字フォントの追加

外字の情報はsrc\info\GaijiInfo.jsに入っていて、ファイル名をCSSやOPFに書き込む際のデータの取り方に苦戦した。

You:

FileNameがGaijiInfoから取得できない

// GaijiInfo.js

import path from 'path';

export default class GaijiInfo {
    /**
     * @param {string} className - クラス名
     * @param {string} gaijiFile - 外字フォントのファイルパス
     */
    constructor(className, gaijiFile) {
        this.className = className;
        this.gaijiFile = gaijiFile;
    }

    /**
     * ファイル名を取得する
     * @returns {string}
     */
    getFileName() {
        return path.basename(this.gaijiFile);
    }

    /**
     * ファイルパスを取得する
     * @returns {string}
     */
    getFile() {
        return this.gaijiFile;
    }

    /**
     * ファイルパスを設定する
     * @param {string} gaijiFile
     */
    setFile(gaijiFile) {
        this.gaijiFile = gaijiFile;
    }

    /**
     * クラス名を取得する
     * @returns {string}
     */
    getClassName() {
        return this.className;
    }

    /**
     * クラス名を設定する
     * @param {string} className
     */
    setClassName(className) {
        this.className = className;
    }
}

ChatGPT:

これもパターンはほぼ特定できます 👍
結論からいきます。


🎯 結論

👉 gaijiFile が undefined / null になっている


🔍 あなたのコード

JavaScript

getFileName() {  
    return path.basename(this.gaijiFile);  
}

❗ 問題が起きる条件

JavaScript

this.gaijiFile \=== undefined

このとき👇

JavaScript

path.basename(undefined)

👉 💥 エラー or 期待しない値


🔥 よくある原因(かなり当たりやすい)

① コンストラクタに値が入ってない

JavaScript

new GaijiInfo(className) // ← gaijiFile渡してない

👉 this.gaijiFile = undefined


② 途中で消えてる

JavaScript

gaijiInfo.setFile(null);

③ JSON化で壊れてる(よくある)

JavaScript

const obj \= JSON.parse(JSON.stringify(gaijiInfo));

👉 一見OKだが…

JavaScript

obj.getFileName // undefined

👉 クラスじゃなくなる


④ Promiseのまま(前と同じ罠)

JavaScript

const gaijiInfo \= getGaijiInfo(); // async

👉 Promise


✅ まずこれを確認

JavaScript

console.log(this.gaijiFile);  
console.log(typeof this.gaijiFile);

✅ 安全な修正版(おすすめ)

JavaScript

getFileName() {  
    if (!this.gaijiFile) return null;  
    return path.basename(this.gaijiFile);  
}

🚀 さらに強化版(デバッグ付き)

JavaScript

getFileName() {  
    if (!this.gaijiFile) {  
        console.warn("gaijiFile is empty:", this);  
        return null;  
    }  
  
    try {  
        return path.basename(this.gaijiFile);  
    } catch (e) {  
        console.error("basename error:", this.gaijiFile, e);  
        return null;  
    }  
}

🎯 追加チェック(かなり重要)

呼び出し側で👇

JavaScript

console.log(gaijiInfo);  
console.log(gaijiInfo instanceof GaijiInfo);

結果の意味

結果 原因
true OK
false JSON化 or 別オブジェクト

💡 AozoraEpub3移植あるある

今回の流れかなり典型👇

  • Java:型が保証される

  • JS:簡単に壊れる

👉 特にこれ危険

JavaScript

map / reduce / JSON / spread

👍 改善版(最終おすすめ)

JavaScript

getFileName() {  
    if (typeof this.gaijiFile !== "string") return null;  
    return path.basename(this.gaijiFile);  
}

🚀 もしまだダメなら

これ貼ってください👇

  • GaijiInfo を生成してるコード

  • getFileName 呼んでる箇所

👉 どこで壊れてるか100%特定できます

You:

[ GaijiInfo { className: 'u3042-u3099', gaijiFile: 'gaiji\\dakuten\\u3042-u3099.ttf' }, GaijiInfo { className: 'u3044-u3099', gaijiFile: 'gaiji\\dakuten\\u3044-u3099.ttf' }, GaijiInfo { className: 'u3048-u3099', gaijiFile: 'gaiji\\dakuten\\u3048-u3099.ttf' }, GaijiInfo { className: 'u304a-u3099', gaijiFile: 'gaiji\\dakuten\\u304a-u3099.ttf' } ]

ChatGPT:

このログを見る限り👇

JavaScript

GaijiInfo {  
  className: 'u3042-u3099',  
  gaijiFile: 'gaiji\\\\dakuten\\\\u3042-u3099.ttf'  
}

👉 データ自体は完全に正常です(原因は別)


🎯 結論(ほぼ確定)

👉 GaijiInfoの配列に対して getFileName() を呼んでいる


🔥 典型的なミス

たぶんどこかで👇やってます

JavaScript

gaijiInfos.getFileName()

でも実際は👇

JavaScript

gaijiInfos \= \[ GaijiInfo, GaijiInfo, ... \]

👉 配列なのでメソッドなし ❌


✅ 正しい使い方

✔ 1件ずつ処理

JavaScript

for (const gaijiInfo of gaijiInfos) {  
    console.log(gaijiInfo.getFileName());  
}

✔ map

JavaScript

const names \= gaijiInfos.map(g => g.getFileName());

✔ 1つだけ

JavaScript

gaijiInfos\[0\].getFileName();

🔍 もう1つの可能性(これも多い)

👉 関数に渡してる引数がズレてる

JavaScript

function write(gaijiInfo) {  
    gaijiInfo.getFileName(); // ← ここ  
}

でも実際👇

JavaScript

write(gaijiInfos); // ← 配列渡してる

🧠 今回の状況まとめ

状態 結果
中身 正しい(GaijiInfo)
配列 OK
呼び方 ❌ 間違ってる

🔍 一発で特定する方法

エラー直前に👇

JavaScript

console.log(Array.isArray(gaijiInfo));

👉 true なら確定


🚀 修正ポイント

エラー箇所たぶん👇

JavaScript

gaijiInfo.getFileName()

👉 修正:

JavaScript

gaijiInfos\[i\].getFileName()

または

JavaScript

for (const g of gaijiInfos) {  
    g.getFileName();  
}

👍 まとめ

👉 今回の本質

「オブジェクト」と「配列」を取り違えている」


💡 おまけ(安全にするなら)

JavaScript

if (Array.isArray(gaijiInfo)) {  
    throw new Error("gaijiInfo is array!");  
}

もしよければ
👉 getFileName() を呼んでる箇所貼ってください

ピンポイントで1行修正出せます 👍

You:

/** 外字用フォントを追加 */ addGaijiFont(className, gaijiFile) { if (this.gaijiNameSet.has(className)) return; this.vecGaijiInfo.push(new GaijiInfo(className, gaijiFile)); this.gaijiNameSet.add(className); }

ChatGPT:

このコード自体は 完全に正しいです 👍(問題なし)

JavaScript

addGaijiFont(className, gaijiFile) {  
    if (this.gaijiNameSet.has(className)) return;  
    this.vecGaijiInfo.push(new GaijiInfo(className, gaijiFile));  
    this.gaijiNameSet.add(className);  
}

🎯 じゃあなぜ「FileNameが取れない」のか

ここまでの情報を全部つなげると👇

  • vecGaijiInfo の中身 → 正常(あなたのログで確認済み)

  • GaijiInfo → 正常

  • gaijiFile → 正常

👉 原因は100%これです


🔥 原因(確定)

👉 vecGaijiInfo(配列)に対して getFileName() を呼んでる


❌ やっている可能性が高いコード

JavaScript

this.vecGaijiInfo.getFileName()

✅ 正しいコード

✔ ループする

JavaScript

for (const g of this.vecGaijiInfo) {  
    console.log(g.getFileName());  
}

✔ map

JavaScript

const fileNames \= this.vecGaijiInfo.map(g => g.getFileName());

✔ 1件だけ

JavaScript

this.vecGaijiInfo\[0\].getFileName();

🔍 ここがズレてる可能性大

例えば👇

JavaScript

writeGaijiFont(gaijiInfo) {  
    gaijiInfo.getFileName(); // ← ここ  
}

呼び出し👇

JavaScript

writeGaijiFont(this.vecGaijiInfo); // ← 配列渡してる

👉 💥 これが今回のバグ


🧠 なぜ起きたか(Aozora移植あるある)

Javaだと👇

Java

List<GaijiInfo\>

型があるから防げる

でもJSだと👇

JavaScript

any

👉 配列でも単体でも通る


🚀 一発で確認する方法

エラー箇所の直前で👇

JavaScript

console.log("isArray:", Array.isArray(gaijiInfo));  
console.log("value:", gaijiInfo);

🎯 まとめ

今回のバグはこれ👇

👉 GaijiInfo単体のつもりで配列を扱っている


💡 さらに安全にするなら

JavaScript

getFileName() {  
    if (!this.gaijiFile) return null;  
    return path.basename(this.gaijiFile);  
}

JavaScript

if (Array.isArray(gaijiInfo)) {  
    throw new Error("Expected GaijiInfo but got array");  
}

👍 次やるといいこと

もし可能なら👇貼ってください

👉 getFileName() を呼んでるコード

1行で完全修正出せます

You:

// 外字のcssを格納 this.ejsData.vecGaijiInfo = this.vecGaijiInfo; // メンバ変数解放 this.velocityContext = null; this.bookInfo = null; this.imageInfoReader = null; if (!bookInfo.imageOnly) { const textCssEntry = ${Epub3Writer.OPS_PATH}${Epub3Writer.CSS_PATH}${Epub3Writer.TEXT_CSS}; let textcss = fs.readFileSync(path.resolve(__dirname, ${this.templatePath}${Epub3Writer.OPS_PATH}${Epub3Writer.CSS_PATH}${Epub3Writer.TEXT_CSS_EJS}), 'utf-8'); const text_css = ejs.render(textcss, this.ejsData) this.zos.file(textCssEntry, text_css, { compression: "DEFLATE", compressionOptions: { level: 9 } }); }

ChatGPT:

ここまでで原因は完全に繋がりました 👍
犯人はこの行です👇

JavaScript

this.ejsData.vecGaijiInfo \= this.vecGaijiInfo;

🎯 結論

👉 EJS側で vecGaijiInfo(配列)を単体として扱っている


🔥 何が起きているか

JS側:

JavaScript

this.vecGaijiInfo \= \[ GaijiInfo, GaijiInfo, ... \];  
this.ejsData.vecGaijiInfo \= this.vecGaijiInfo;

👉 問題なし


でもEJSテンプレート側で👇やってるはず

ejs

<%= vecGaijiInfo.getFileName() %>

👉 ❌ 配列に対して呼んでる


✅ 正しいEJSの書き方

✔ ループする

ejs

<% vecGaijiInfo.forEach(function(g) { %>  
  <%= g.getFileName() %>  
<% }) %>

✔ map風

ejs

<%= vecGaijiInfo.map(g => g.getFileName()).join(",") %>

🧠 ここが重要(今回の本質)

JSでは👇

JavaScript

Array<GaijiInfo\>

EJSでは👇

👉 ただの配列

なので👇はダメ

ejs

vecGaijiInfo.getFileName()

🔍 ほぼ確実にあるテンプレートバグ

AozoraEpub3的にありがち👇

ejs

@font-face {  
  src: url("<%= vecGaijiInfo.getFileName() %>");  
}

👉 ❌


✅ 正しい書き方(CSS生成)

ejs

<% vecGaijiInfo.forEach(function(g) { %>  
@font-face {  
  font-family: "<%= g.getClassName() %>";  
  src: url("../fonts/<%= g.getFileName() %>");  
}  
<% }) %>

🚀 デバッグ方法(確定させる)

EJS内に一時的に👇

ejs

<%= Array.isArray(vecGaijiInfo) %>

👉 true が出る


💡 さらに安全にするなら(JS側)

JavaScript

this.ejsData.vecGaijiInfo \= this.vecGaijiInfo ?? \[\];

👍 まとめ

今回のバグ:

👉 「EJSで配列を単体として扱っている」


🎯 修正箇所

👉 TEXT_CSS_EJS の中


🚀 次にやるといい

もしよければ👇貼ってください

  • TEXT_CSS_EJS の中身

👉 完全に動く形に書き直します(Aozora互換) 👍

You:

/** 外字フォント */ <% vecGaijiInfo.forEach(gaijiInfo => { %> @font-face { font-family: "<%= gaijiInfo.className %>"; src: url(../gaiji/<%= gaijiInfo.fileName %>); } .<%= gaijiInfo.className %> { font-family: "<%= gaijiInfo.className %>"; } <% }); %>

ChatGPT:

原因はここで確定です 👍


🎯 問題の行

ejs

<%= gaijiInfo.fileName %>

👉 fileName プロパティは存在しない


🔍 あなたのクラス

JavaScript

getFileName() {  
    return path.basename(this.gaijiFile);  
}

👉 メソッドはある
👉 でもプロパティは無い


❌ 今の状態

ejs

gaijiInfo.fileName // undefined

✅ 修正方法(2パターン)


✔ 方法①(シンプル)

👉 メソッドを呼ぶ

ejs

<%= gaijiInfo.getFileName() %>

✔ 方法②(おすすめ:Java互換)

👉 getter追加(JS側)

JavaScript

get fileName() {  
    return this.getFileName();  
}

すると👇

ejs

<%= gaijiInfo.fileName %>

がそのまま使える


🚀 修正後のEJS(完成版)

ejs

/\*\* 外字フォント \*/  
<% vecGaijiInfo.forEach(gaijiInfo => { %>  
@font-face {  
    font-family: "<%= gaijiInfo.className %>";  
    src: url(../gaiji/<%= gaijiInfo.getFileName() %>);  
}  
.<%= gaijiInfo.className %> {  
    font-family: "<%= gaijiInfo.className %>";  
}  
<% }); %>

💡 なぜ起きたか

Java版だと👇

Java

getFileName() → テンプレートから property 的に見える

でもJS + EJSでは👇

👉 メソッドは明示的に呼ばないとダメ


👍 おすすめ最終形(これが一番安全)

GaijiInfo.js に追加

JavaScript

get fileName() {  
    return this.getFileName();  
}

EJS(そのまま使える)

ejs

<%= gaijiInfo.fileName %>

🎯 まとめ

今回の原因👇

👉 「メソッド」と「プロパティ」を取り違えた」


🚀 次の一手

今のAozora移植だと👇

  • getXXX().xxx にしたほうが楽

👉 全体をJava風→JS風に整理もできます

必要ならまとめて最適化出します 👍

You:

<% for(var i = 0; i < vecGaijiInfo.length; i++) { -%> <% }; -%>

ChatGPT:

ここも同じ原因です 👍
完全に特定できます


🎯 問題の行

ejs

<%= vecGaijiInfo\[i\].FileName -%>

👉 ❌ FileName は存在しない(しかも大文字)


🔍 あなたのクラス

JavaScript

getFileName()

👉 メソッド名はこれだけ


❌ 現状

JavaScript

vecGaijiInfo\[i\].FileName // undefined

✅ 修正方法

✔ 方法①(そのまま直す)

ejs

<%= vecGaijiInfo\[i\].getFileName() -%>

✔ 方法②(おすすめ:互換対応)

👉 GaijiInfo.js に追加

JavaScript

get fileName() {  
    return this.getFileName();  
}

すると👇

ejs

<%= vecGaijiInfo\[i\].fileName -%>

🚀 修正後コード(安全版)

ejs

<% for (var i = 0; i < vecGaijiInfo.length; i++) { -%>  
    <item id="gaiji\_<%= i + 1 -%>" href="gaiji/<%= vecGaijiInfo\[i\].getFileName() -%>" media-type="application/font-sfnt"/>  
<% } -%>

💡 さらに改善(読みやすい書き方)

ejs

<% vecGaijiInfo.forEach((g, i) => { -%>  
    <item id="gaiji\_<%= i + 1 -%>" href="gaiji/<%= g.getFileName() -%>" media-type="application/font-sfnt"/>  
<% }); -%>

🔥 今回の本質まとめ

今回のバグ全部同じ原因👇

  • fileName → ❌ プロパティじゃない

  • FileName → ❌ 大文字も違う

  • 正解 → getFileName()


👍 Aozora移植での鉄則

👉 JavaのgetterはJSではそのまま使えない


🚀 最終おすすめ(これやると全部解決)

GaijiInfo.js に追加

JavaScript

get fileName() {  
    return this.getFileName();  
}

すると全部こう書ける

ejs

g.fileName

👉 Java感覚で書けるのでバグ激減


必要なら👇

👉 ImageInfo / GaijiInfo / BookInfo 全部Java互換プロパティ化
まとめてやるとかなり安定します 👍 [ GaijiInfo { className: 'u3042-u3099', gaijiFile: 'gaiji\dakuten\u3042-u3099.ttf' }, GaijiInfo { className: 'u3044-u3099', gaijiFile: 'gaiji\dakuten\u3044-u3099.ttf' }, GaijiInfo { className: 'u3048-u3099', gaijiFile: 'gaiji\dakuten\u3048-u3099.ttf' }, GaijiInfo { className: 'u304a-u3099', gaijiFile: 'gaiji\dakuten\u304a-u3099.ttf' } ]

/* 外字フォント / <% vecGaijiInfo.forEach(gaijiInfo => { %> @font-face { font-family: "<%= gaijiInfo.ClassName %>"; src: url(../gaiji/<%= gaijiInfo.FileName %>); } .<%= gaijiInfo.ClassName %> { font-family: "<%= gaijiInfo.ClassName %>"; } <% }); %>

Java版

/* 外字フォント / @font-face {font-family:"u3042-u3099"; src:url(../gaiji/u3042-u3099.ttf);} .u3042-u3099 {font-family:"u3042-u3099";} @font-face {font-family:"u3044-u3099"; src:url(../gaiji/u3044-u3099.ttf);} .u3044-u3099 {font-family:"u3044-u3099";} @font-face {font-family:"u3048-u3099"; src:url(../gaiji/u3048-u3099.ttf);} .u3048-u3099 {font-family:"u3048-u3099";} @font-face {font-family:"u304a-u3099"; src:url(../gaiji/u304a-u3099.ttf);} .u304a-u3099 {font-family:"u304a-u3099";}

astroでbuild時の出力先を絶対パスで指定

出力先は指定できたが、出力するたびにフォルダーを削除されるので.gitが同じフォルダーに置けない。

You:

tailwindcssを使ったナビゲーションに書き換えて

サイトトップkindleセールチェック紙の新刊電書ニュース

ChatGPT:

ナビゲーションっぽくするなら、<nav>+リスト構造にして Tailwind で横並び・ホバー装飾を付けるのが定番です。

シンプルで使いやすい形に書き換えるとこんな感じ👇

HTML

<nav class\="bg-white border-b"\>  
  <ul class\="max-w-5xl mx-auto flex flex-wrap gap-4 px-4 py-3 text-sm md:text-base font-semibold"\>  
    <li>  
      <a href\="https://kyukyunyorituryo.github.io/" class\="text-gray-700 hover:text-blue-600 transition"\>  
        サイトトップ  
      </a>  
    </li>  
    <li>  
      <a href\="https://kyukyunyorituryo.github.io/kindle\_sale/" class\="text-gray-700 hover:text-blue-600 transition"\>  
        Kindleセールチェック  
      </a>  
    </li>  
    <li>  
      <a href\="https://kyukyunyorituryo.github.io/new\_pub/" class\="text-gray-700 hover:text-blue-600 transition"\>  
        紙の新刊  
      </a>  
    </li>  
    <li>  
      <a href\="https://kyukyunyorituryo.github.io/i/" class\="text-gray-700 hover:text-blue-600 transition"\>  
        電書ニュース  
      </a>  
    </li>  
  </ul>  
</nav>

ポイント

  • flex + gap-4 → 横並び&間隔

  • flex-wrap → スマホで折り返し

  • hover:text-blue-600 → ホバー時の色変化

  • max-w-5xl mx-auto → 中央寄せ

  • border-b → ナビっぽい区切り線


もう少しナビっぽく(現在ページ強調)

もし「今いるページ」を強調したいなら👇

HTML

<a href\="#" class\="text-blue-600 border-b-2 border-blue-600 pb-1"\>

必要なら「ハンバーガーメニュー(モバイル対応)」や「stickyヘッダー化」も作れるので言ってください 👍

You:

tailwindcssを使って書き換えて

ChatGPT:

シンプルに Tailwind で「見出し+説明文」らしく整えるとこんな感じになります👇

HTML

<div class\="max-w-3xl mx-auto px-4 py-6"\>  
  <h1 id\="title" class\="text-2xl md:text-4xl font-bold text-gray-900 leading-tight"\></h1>  
  <p id\="desc" class\="mt-3 text-gray-600 text-base md:text-lg leading-relaxed"\></p>  
</div>

ポイント

  • max-w-3xl mx-auto → 横幅制限+中央寄せ

  • text-2xl md:text-4xl → レスポンシブな見出しサイズ

  • mt-3 → タイトルと説明の間に余白

  • text-gray-600 → 説明文を少し薄くして読みやすく

  • leading-relaxed → 行間を広げて可読性アップ


もう少し強調したい場合(オプション)

カードっぽくするなら👇

HTML

<div class\="max-w-3xl mx-auto px-4 py-6 bg-white shadow rounded-2xl"\>  
  <h1 id\="title" class\="text-2xl md:text-4xl font-bold text-gray-900"\></h1>  
  <p id\="desc" class\="mt-3 text-gray-600 text-base md:text-lg"\></p>  
</div>

デザインの方向(ミニマル・ブログ風・LP風など)があれば、それに合わせてもっと寄せたスタイルにもできるよ。

You:

tailwindcss grid cardを使って表示するには import * as React from "react"; const Amazons = ({ book = }) => { const books = React.useMemo(() => { if (!Array.isArray(book)) return ; const normalized = book.map*1.values() ); }, [book]); if (books.length === 0) { return

表示する商品がありません
; } return (
{books.map((e) => { const releaseDate = e.ReleaseDate ? ${e.ReleaseDate.slice(0, 4)}/${e.ReleaseDate.slice(4, 6)}/${e.ReleaseDate.slice(6, 8)} : "不明"; return ( ); })}
); }; export default Amazons;

ChatGPT:

今の構造を活かしつつ、TailwindのGrid+カードUIにするとこんな感じになります👇
(レスポンシブでいい感じに並ぶやつ)


✅ Tailwind Grid + Card版

JavaScript

import \* as React from "react";  
  
const Amazons \= ({ book \= \[\] }) => {  
  const books \= React.useMemo(() => {  
    if (!Array.isArray(book)) return \[\];  
  
    const normalized \= book.map((b) => {  
      const image\_m \=  
        b.ImageURL\_m || b.ImageURL?.replace(/\_SL500\_/, "\_SL160\_") || "";  
      const image\_s \=  
        b.ImageURL\_s || b.ImageURL?.replace(/\_SL500\_/, "\_SL75\_") || "";  
  
      return { ...b, image\_m, image\_s };  
    });  
  
    return Array.from(  
      new Map(normalized.map((b) => \[b.Asin, b\])).values()  
    );  
  }, \[book\]);  
  
  if (books.length \=== 0) {  
    return <div id\="book"\>表示する商品がありません</div>;  
  }  
  
  return (  
    <div  
      id\="book"  
      className\="max-w-6xl mx-auto px-4 py-6 grid gap-6  
                 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5"  
    \>  
      {books.map((e) => {  
        const releaseDate \= e.ReleaseDate  
          ? \`${e.ReleaseDate.slice(0, 4)}/${e.ReleaseDate.slice(4, 6)}/${e.ReleaseDate.slice(6, 8)}\`  
          : "不明";  
  
        return (  
          <a  
            key\={e.Asin || e.Title}  
            href\={e.URL}  
            target\="\_blank"  
            rel\="noopener noreferrer"  
            className\="bg-white rounded-2xl shadow hover:shadow-lg transition overflow-hidden flex flex-col"  
          \>  
            {/\* 画像 \*/}  
            {e.image\_s && (  
              <div className\="aspect-\[3/4\] bg-gray-100"\>  
                <img  
                  srcSet\={\`${e.image\_s} 320w, ${e.image\_m} 640w, ${e.ImageURL} 1024w\`}  
                  src\={e.image\_s}  
                  sizes\="(max-width:1280px) 50vw, 1280px"  
                  loading\="lazy"  
                  alt\={e.Title}  
                  className\="w-full h-full object-cover"  
                />  
              </div>  
            )}  
  
            {/\* テキスト \*/}  
            <div className\="p-3 flex flex-col gap-2"\>  
              <div className\="text-sm font-semibold text-gray-900 line-clamp-2"\>  
                {e.Title}  
              </div>  
  
              <div className\="text-xs text-gray-600 line-clamp-3"\>  
                価格:{Number(e.Price) || "なし"}円<br />  
                ポイント:{e.Points ?? 0}<br />  
                {e.Contributor}<br />  
                出版社:{e.Publisher}<br />  
                発売日:{releaseDate}  
              </div>  
            </div>  
          </a>  
        );  
      })}  
    </div>  
  );  
};  
  
export default Amazons;

🎯 見た目のポイント

■ グリッド

CSS

grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5

→ 画面サイズごとに列数が増える


■ カード

  • rounded-2xl → 角丸

  • shadow → カード感

  • hover:shadow-lg → ホバーで浮く

  • overflow-hidden → 画像はみ出し防止


■ 画像

CSS

aspect-\[3/4\] object-cover

→ Kindle本っぽい比率で固定


■ タイトル省略

CSS

line-clamp-2

※使うにはプラグイン必要👇

Bash

npm install @tailwindcss/line-clamp

🔥 よくある改善ポイント

もしさらに良くするなら:

① ホバーで拡大

CSS

hover:scale-\[1.02\]

② クリック感

CSS

transition-transform duration-200

③ 価格を強調

JavaScript

<span className\="text-red-500 font-bold"\>

👍 まとめ

  • gridで一覧表示

  • card UIで見やすく

  • aspect-ratioで画像崩れ防止

  • line-clampでレイアウト安定


「横長カード(リスト型)」や「Amazon風ランキング表示」にもできるので、やりたければそこも作れる 👍

You:

文字情報が省略されすぎるので表示部分を大きくしたい { "Asin": "4411014939", "URL": "https://www.amazon.co.jp/dp/4411014939?tag=99hatena-22&linkCode=ogi&th=1&psc=1", "ImageURL": "https://m.media-amazon.com/images/I/41usKllGN3L._SL500_.jpg", "ImageURL_s": "https://m.media-amazon.com/images/I/41usKllGN3L._SL75_.jpg", "ImageURL_m": "https://m.media-amazon.com/images/I/41usKllGN3L._SL160_.jpg", "Publisher": "駿河台出版社", "Contributor": "阿南 婦美代(著)", "Booktype": "単行本", "Title": "新サンプルマン", "Price": "2860", "Points": 0, "Category": "語学・辞事典・年鑑,Amazon Student ポイント還元(和書),Amazonアプリキャンペーン対象商品(和書)", "ReleaseDate": "20260201" }

ChatGPT:

それは今のレイアウトだと「画像に対してテキスト領域が小さすぎる」のが原因です。
カードを縦長から“テキスト重視型”に変えると一気に改善します。


✅ 改善版(テキスト領域を拡大)

ポイント:

  • 画像を小さく(上部固定)

  • テキストをしっかり表示(省略を減らす)

  • カードを少し横広に

JavaScript

<div  
  id\="book"  
  className\="max-w-6xl mx-auto px-4 py-6 grid gap-6  
             grid-cols-1 sm:grid-cols-2 md:grid-cols-3"  
\>  
  {books.map((e) => {  
    const releaseDate \= e.ReleaseDate  
      ? \`${e.ReleaseDate.slice(0, 4)}/${e.ReleaseDate.slice(4, 6)}/${e.ReleaseDate.slice(6, 8)}\`  
      : "不明";  
  
    return (  
      <a  
        key\={e.Asin || e.Title}  
        href\={e.URL}  
        target\="\_blank"  
        rel\="noopener noreferrer"  
        className\="bg-white rounded-2xl shadow hover:shadow-lg transition p-4 flex gap-4"  
      \>  
        {/\* 画像(小さくする) \*/}  
        {e.image\_s && (  
          <img  
            src\={e.image\_s}  
            alt\={e.Title}  
            className\="w-20 h-28 object-cover rounded"  
          />  
        )}  
  
        {/\* テキスト(広くする) \*/}  
        <div className\="flex-1 min-w-0"\>  
          <div className\="text-base font-bold text-gray-900 break-words"\>  
            {e.Title}  
          </div>  
  
          <div className\="mt-1 text-sm text-gray-600"\>  
            {e.Contributor}  
          </div>  
  
          <div className\="mt-2 text-sm text-gray-700"\>  
            <span className\="text-red-500 font-semibold"\>  
              {Number(e.Price) || "なし"}円  
            </span>  
            / ポイント:{e.Points ?? 0}  
          </div>  
  
          <div className\="mt-1 text-xs text-gray-500"\>  
            出版社:{e.Publisher}  
          </div>  
  
          <div className\="text-xs text-gray-500"\>  
            発売日:{releaseDate}  
          </div>  
  
          <div className\="mt-1 text-xs text-gray-400 break-words"\>  
            {e.Category?.split(",").slice(0, 2).join(" / ")}  
          </div>  
        </div>  
      </a>  
    );  
  })}  
</div>

🎯 何が改善されるか

❌ Before(問題)

  • 画像が大きすぎる

  • line-clampで情報が消える

  • 縦カードで情報密度が低い


✅ After(改善)

  • 横カード化 → テキスト領域が倍以上

  • タイトルがほぼフル表示

  • 情報が段階的に見やすい


🔥 さらに良くするなら

① タイトル完全表示(改行OK)

CSS

break-words

② カードをさらに読みやすく

CSS

hover:bg-gray-50

③ 画像を少し大きく

CSS

w-24 h-32

👍 結論

  • グリッドカード(縦型) → 見た目重視

  • 横カード(今回) → 情報量重視 ←おすすめ


もし
👉「縦カードのまま情報量増やしたい」
👉「Amazon風(画像左・右に価格大きく)」

とかも作れるので、方向あればそこに最適化する 👍

You:

スマホでの表示に対応させたい --- import Layout from '../components/Layout.astro'; import Seo from '../components/Seo.astro'; import DataProvider from '../components/DataProvider.jsx'; import "../styles/global.css"; const siteTitle = "今日の新刊一覧"; const siteUrl = "https://kyukyunyorituryo.github.io/new_pub"; ---

カレンダーの日付をクリックで発売日を変更

ChatGPT:

今のコード、PC基準で中央寄せ+余白広めなので、スマホだとちょっと窮屈&使いにくい状態です。
「縦積み・フル幅・タップしやすいUI」に調整するとかなり良くなります。


✅ スマホ対応の改善ポイント

❌ 現状の問題

  • items-center justify-center → スマホで中央寄せになって見づらい

  • 余白が大きい

  • UIが横並びじゃないので操作しにくい


✅ 改善版コード(そのまま置き換えOK)

astro

<body class="min-h-screen bg-gray-50">  
  <Layout title={siteTitle}>  
    <article class="max-w-5xl mx-auto px-3 sm:px-4 py-4 sm:py-6">  
  
      <header>  
        <div class="px-1">  
          <h1 id="title"  
            class="text-xl sm:text-2xl md:text-4xl font-bold text-gray-900 leading-tight">  
          </h1>  
          <p id="desc"  
            class="mt-2 text-sm sm:text-base md:text-lg text-gray-600 leading-relaxed">  
          </p>  
        </div>  
      </header>  
  
      <!-- 説明 -->  
      <p class="mt-4 text-sm text-gray-600">  
        カレンダーの日付をクリックで発売日を変更  
      </p>  
  
      <!-- カレンダー -->  
      <div class="mt-3">  
        <input  
          type="date"  
          id="calendar"  
          class="w-full sm:w-auto border border-gray-300 rounded-lg p-3 text-base  
                 focus:outline-none focus:ring-2 focus:ring-blue-500"  
        />  
      </div>  
  
      <!-- 検索UI -->  
      <div id="search" class="mt-4"></div>  
  
      <!-- 本一覧 -->  
      <div id="list" class="mt-6"></div>  
  
      <DataProvider client:load />  
  
      <p id="empty" class="hidden mt-6 text-center text-gray-500">  
        この日の新刊情報はありません。  
      </p>  
  
    </article>  
  </Layout>  
</body>

🎯 何が良くなったか

① 中央寄せをやめた

Diff

\- flex items-center justify-center  
\+ 通常フロー

👉 スマホで自然な縦スクロールに


② 横幅をスマホ最適化

CSS

px-3 sm:px-4

👉 スマホは余白少なめ、PCは余白あり


③ フォントサイズをレスポンシブ化

CSS

text-xl sm:text-2xl md:text-4xl

④ カレンダーをタップしやすく

CSS

w-full p-3 text-base

👉 指でも押しやすい


⑤ セクション間の余白整理

CSS

mt-4 / mt-6

👉 スマホで詰まりすぎない


🔥 さらに良くするなら(おすすめ)

① カレンダー+説明を横並び(PCだけ)

HTML

<div class\="mt-4 flex flex-col sm:flex-row sm:items-center sm:gap-4"\>

② 上部ナビを固定(スマホUX改善)

CSS

sticky top-0 bg-white z-10

③ 本一覧も余白調整

HTML

<div id\="list" class\="mt-6 space-y-4"\></div>

👍 まとめ

スマホ対応のコツはこれ👇

  • ❌ 中央寄せ → 縦スクロール前提に

  • ❌ 固定サイズ → レスポンシブ

  • ❌ 小さいタップ領域 → padding増やす

  • w-full を活用


もし次やるなら👇
👉「スマホで2列グリッドにしたい」
👉「固定ヘッダー+検索バー」
👉「アプリっぽいUI」

ここまでいくとかなり完成度高くなるので、そこも作れる 👍

You:

日付を日本時間で取りたい kyou = new Date().toISOString().slice(0,10).replaceAll("-", "");

ChatGPT:

それ、toISOString() は **UTC(世界標準時)**なので、日本だと日付がズレます(特に朝9時前)。

👉 日本時間で取りたいならローカル時間を使う必要があります。


✅ 一番シンプル(おすすめ)

JavaScript

const now \= new Date();  
const kyou \=  
  now.getFullYear().toString() +  
  String(now.getMonth() + 1).padStart(2, "0") +  
  String(now.getDate()).padStart(2, "0");

✅ もっとスマート(Intl使う)

JavaScript

const kyou \= new Intl.DateTimeFormat("ja-JP", {  
  timeZone: "Asia/Tokyo",  
  year: "numeric",  
  month: "2-digit",  
  day: "2-digit",  
})  
  .format(new Date())  
  .replaceAll("/", "");

❌ なぜ元コードはダメか

JavaScript

new Date().toISOString()

これは👇

  • UTCで日付を作る

  • 日本は +9時間なので

  • 朝9時までは前日になる


👍 結論

  • 簡単 → getFullYear()系(速い)

  • 厳密 → Intl.DateTimeFormat(タイムゾーン指定可)

👉 普通のWebなら 最初の方法でOK


必要なら「サーバー側(Astro)で日本時間に固定」する方法も出せる 👍

You:

astroでbuild時の出力先を絶対パスで指定できるか

ChatGPT:

結論から言うと👇
👉 Astroの build.outDir は絶対パス指定できます(普通にOK)


✅ 基本設定(astro.config.mjs)

JavaScript

import { defineConfig } from 'astro/config';  
  
export default defineConfig({  
  outDir: "C:/output/my-site", // ← 絶対パスOK  
});

✅ Nodeっぽく安全に書く(推奨)

環境によってはこっちの方が安定👇

JavaScript

import { defineConfig } from 'astro/config';  
import path from 'path';  
  
export default defineConfig({  
  outDir: path.resolve("C:/output/my-site"),  
});

⚠️ 注意点

① OSごとのパス

  • Windows → "C:/..." or "C:\\..."

  • Mac/Linux → "/Users/..."


② 相対との違い

JavaScript

outDir: "./dist"   // ← プロジェクト基準  
outDir: "/dist"    // ← ルート直下(Linuxだと危険)

③ GitHub Pages使ってる場合

今の構成だと:

https://kyukyunyorituryo.github.io/new\_pub/

なので👇も重要

JavaScript

export default defineConfig({  
  site: "https://kyukyunyorituryo.github.io",  
  base: "/new\_pub",  
});

※これズレるとCSSや画像が壊れる


🔥 よくある使い方

別プロジェクトに直接出力

JavaScript

outDir: "../public\_html/new\_pub"

ビルド結果をそのまま配信用フォルダへ

JavaScript

outDir: "C:/nginx/html/new\_pub"

👍 まとめ

  • ✅ 絶対パス指定 → 可能

  • path.resolve()安全

  • ⚠️ base設定 → GitHub Pagesでは必須


もし
👉「ビルド → gh-pages 自動デプロイ」
👉「複数サイトを1つにまとめる構成」

やってるなら、その構成に最適な outDir も一緒に設計できる 👍

You:

tsconfig.jsonで書かれているのとの違いは { "extends": "astro/tsconfigs/strict", "include": [ ".astro/types.d.ts", "**/*" ], "exclude": [ "dist" ], "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react" } }

ChatGPT:

結論から言うと👇
👉 astro.config.mjstsconfig.json は役割が完全に別物です


✅ 違いを一言で

  • astro.config.mjs → 「ビルド設定(出力先・URLなど)」

  • tsconfig.json → 「TypeScriptの型チェック設定」


✅ 今回のポイント(outDirとの関係)

❌ tsconfig.json

JSON

"exclude": \["dist"\]

👉 これは
「TypeScriptの解析対象から dist を除外する」だけ

  • ビルド先を決めているわけではない

  • 出力先は変えられない


✅ astro.config.mjs

JavaScript

export default {  
  outDir: "C:/output/my-site"  
}

👉 これは
「ビルド結果をどこに出すか」そのもの


🎯 もう少し具体的に

tsconfig.json がやってること

JSON

{  
  "extends": "astro/tsconfigs/strict",  
  "include": \["\*\*/\*"\],  
  "exclude": \["dist"\]  
}

👉 意味:

  • プロジェクト全体を型チェック

  • ただし dist は無視(ビルド済みだから不要)


もし outDir を変えたら?

JavaScript

outDir: "C:/output/my-site"

👉 この場合👇に変えたほうがいい

JSON

"exclude": \["C:/output/my-site"\]

または安全に👇

JSON

"exclude": \["dist", "C:/output"\]

⚠️ 混同しやすいポイント

❌ よくある誤解

  • tsconfigで出力先を変えられる → できない

  • excludeでビルドを制御できる → できない


✅ 正しい理解

設定 役割
astro.config.mjs ビルド・出力・URL
tsconfig.json 型チェック・補完

👍 まとめ

  • outDirastro.config.mjsで指定

  • exclude型チェック対象を除外するだけ

  • 両者は完全に別レイヤー


🔥 実務的アドバイス

今の構成なら👇でOK

JSON

"exclude": \["dist"\]

👉 outDir を変えたときだけ見直せばいい


もし
👉「Astro + React + TSで型エラー出る」
👉「.astro と .tsx の型連携」

あたり気になってたらそこも深掘りできる 👍

*1:b) => { const image_m = b.ImageURL_m || b.ImageURL?.replace(/_SL500_/, "_SL160_") || ""; const image_s = b.ImageURL_s || b.ImageURL?.replace(/_SL500_/, "_SL75_") || ""; return { ...b, image_m, image_s }; }); return Array.from( new Map(normalized.map((b) => [b.Asin, b]

3月26日から4月1日までキンドル新刊ピックアップ

3月26日発売の本

3月26日発売のそのほかの本 kyukyunyorituryo.github.io

3月27日発売の本

3月27日発売のそのほかの本 kyukyunyorituryo.github.io

3月28日発売の本

3月28日発売のそのほかの本 kyukyunyorituryo.github.io

3月29日発売の本

3月29日発売のそのほかの本 kyukyunyorituryo.github.io

3月30日発売の本

3月30日発売のそのほかの本 kyukyunyorituryo.github.io

3月31日発売の本

鬼門の村

鬼門の村

Amazon
フィールド

フィールド

  • 作者:水井快
  • 幻冬舎メディアコンサルティング
Amazon

3月31日発売のそのほかの本 kyukyunyorituryo.github.io

4月1日発売の本

4月1日発売のそのほかの本 kyukyunyorituryo.github.io

画像の情報を読み取るためにJimpやsharpを使い

画像の情報を読み取るためにJimpやsharpを使いasync awaitを入れると処理の順番が変わる。 awaitを入れないと画像情報はpromiseのママになる。

AozoraEpub3.jsからepubWriter.write、epub3Writer = new Epub3Writer(${jarPath}template/); Epub3Writer,writeはasync

src\writer\Epub3Writer.js でthis.imageInfosがからの配列だったのでthis.imageInfosを追加する部分を見ていく。 async getImageFilePath(srcImageFileName, lineNum, bookInfo)

src\converter\AozoraEpub3Converter.jsの該当部分ではPromise { }なのでawaitを入れる必要がある。 convertTextLineToEpub3内の const dstFileName = this.writer.getImageFilePath(srcFilePath, lineNum, this.bookInfo);

convertTextLineToEpub3にasyncを入れると

../image/0001.jpg file:///C:/Users/Owner/Documents/GitHub/AozoraEpub3JS/src/writer/Epub3Writer.js:1311 return this.imageInfoReader.getImageAlt(srcFilePath); ^

TypeError: Cannot read properties of null (reading 'getImageAlt') at Epub3Writer.getAlt (file:///C:/Users/Owner/Documents/GitHub/AozoraEpub3JS/src/writer/Epub3Writer.js:1311:37) at AozoraEpub3Converter.convertTextLineToEpub3 (file:///C:/Users/Owner/Documents/GitHub/AozoraEpub3JS/src/converter/AozoraEpub3Converter.js:2413:49)

You:

TypeError: imageInfo.getOutFileName is not a function at Epub3Writer.write (file:///C:/Users/Owner/Documents/GitHub/AozoraEpub3JS/src/writer/Epub3Writer.js:807:105) at async convertFile (file:///C:/Users/Owner/Documents/GitHub/AozoraEpub3JS/src/AozoraEpub3.js:543:5) at async main (file:///C:/Users/Owner/Documents/GitHub/AozoraEpub3JS/src/AozoraEpub3.js:471:7) エラーが発生しました : imageInfo.getOutFileName is not a function import fs from "fs"; import sharp from "sharp"; export default class ImageInfo { /** 画像の情報を生成 * @param {string} ext png jpg gif webp など */ constructor(ext, width, height) { this.id = null; // ファイルID this.outFileName = null; // 出力ファイル名 this.ext = ext ? ext.toLowerCase() : null; this.width = width ?? -1; this.height = height ?? -1; this.outWidth = -1; this.outHeight = -1; this.isCover = false; this.rotateAngle = 0; } // ========================= // static 生成メソッド // ========================= /** ファイルから画像情報を生成 */ static async getImageInfoFromFile(imageFile) { const buffer = await fs.promises.readFile(imageFile); return await ImageInfo.getImageInfo(buffer); } /** 画像ストリーム / Buffer から画像情報を生成 */ static async getImageInfo(input) { if (!input) return null; try { const metadata = await sharp(input).metadata(); if (!metadata || !metadata.format) return null; return new ImageInfo( metadata.format, // png jpeg webp gif avif など metadata.width, metadata.height ); } catch (e) { return null; } } /** 既に幅高さが分かっている場合 */ static getImageInfoFromRaw(ext, width, height) { return new ImageInfo(ext, width, height); } // ========================= // getter / setter // ========================= getId() { return this.id; } setId(id) { this.id = id; } getOutFileName() { return this.outFileName; } setOutFileName(file) { this.outFileName = file; } setExt(ext) { this.ext = ext?.toLowerCase(); } getExt() { return this.ext; } /** mime形式(image/png)を返す */ getFormat() { if (!this.ext) return null; return "image/" + (this.ext === "jpg" ? "jpeg" : this.ext); } getIsCover() { return this.isCover; } setIsCover(isCover) { this.isCover = isCover; } getWidth() { return this.width; } setWidth(width) { this.width = width; } getHeight() { return this.height; } setHeight(height) { this.height = height; } getOutWidth() { return this.outWidth; } setOutWidth(outWidth) { this.outWidth = outWidth; } getOutHeight() { return this.outHeight; } setOutHeight(outHeight) { this.outHeight = outHeight; } }

ChatGPT:

このエラー、原因はかなり典型的です👇

TypeError: imageInfo.getOutFileName is not a function

つまり imageInfoImageInfo インスタンスじゃない です。


🔍 よくある原因(今回かなりこれっぽい)

あなたの ImageInfo クラス自体は問題ありません。
getOutFileName() もちゃんと定義されています。

なので問題はここ👇

imageInfo に入っているものが違う

例えばこんな状態になっている可能性があります:

JavaScript

const imageInfo \= await ImageInfo.getImageInfo(...);

一見OKですが、実際には👇

パターン1:null が入ってる

JavaScript

if (!metadata || !metadata.format) return null;

→ つまり imageInfo === null

でもこの場合は普通

Cannot read properties of null

になるので可能性は低め。


パターン2(本命):プレーンオブジェクト化してる

どこかでこうなってませんか?

JavaScript

imageInfo \= { ...imageInfo };

または

JavaScript

JSON.parse(JSON.stringify(imageInfo))

これをやると👇

👉 クラスのメソッドが全部消える

結果:

JavaScript

imageInfo.getOutFileName // undefined

パターン3:配列や別型が混ざってる

JavaScript

imageInfos.push(someObject); // ← ImageInfoじゃない

✅ 確認してほしい(最短デバッグ)

エラーが出る直前にこれ入れてください👇

JavaScript

console.log(imageInfo);  
console.log(typeof imageInfo);  
console.log(imageInfo instanceof ImageInfo);  
console.log(imageInfo?.constructor?.name);

🎯 期待される結果

JavaScript

true  
"ImageInfo"

❌ もしこうなったら

ケース1

JavaScript

false  
"Object"

👉 spread / JSON 化してる


ケース2

JavaScript

false  
undefined

👉 null / undefined


💡 修正方法

✔ 方法1:クラスを壊さない

NG:

JavaScript

const copy \= { ...imageInfo };

OK:

JavaScript

const copy \= imageInfo; // 参照でOK

✔ 方法2:復元する

もしJSON経由してるなら👇

JavaScript

const raw \= JSON.parse(...);  
const imageInfo \= Object.assign(new ImageInfo(), raw);

✔ 方法3:生成元を統一

必ずここから作る:

JavaScript

await ImageInfo.getImageInfo(...)

🧠 補足(今回かなり重要)

あなたのプロジェクト(AozoraEpub3移植)だと👇

  • Java → JS移植

  • DTOっぽく扱ってる箇所あり

  • 非同期+構造変換あり

👉 どこかで「ただのデータ」に変換されてる可能性が高い


🚀 もしよければ

Epub3Writer.js の 807行目付近

貼ってくれれば
どこで壊れてるかピンポイントで特定できます 👍

You:

[ ImageInfo { id: '0001', outFileName: '0001.jpg', ext: 'jpeg', width: 1512, height: 2150, outWidth: -1, outHeight: -1, isCover: false, rotateAngle: 0 }, ImageInfo { id: '0002', outFileName: '0002.webp', ext: 'webp', width: 550, height: 404, outWidth: -1, outHeight: -1, isCover: false, rotateAngle: 0 }, ImageInfo { id: '0003', outFileName: '0003.jpg', ext: 'jpeg', width: 640, height: 400, outWidth: -1, outHeight: -1, isCover: false, rotateAngle: 0 } ]

ChatGPT:

このログ、かなり重要で👇

JavaScript

\[  
  ImageInfo { ... },  
  ImageInfo { ... },  
  ImageInfo { ... }  
\]

👉 中身は完全に正しい(ちゃんと ImageInfo インスタンス)

なので結論はこれです👇


🎯 原因

👉 imageInfo が「配列」になってる

エラーはこれ:

imageInfo.getOutFileName is not a function

でも実際の imageInfo は👇

JavaScript

imageInfo \= \[ ImageInfo, ImageInfo, ImageInfo \]

つまり

JavaScript

imageInfo.getOutFileName()

を呼んでるけど

👉 配列にはそんなメソッドない


🔥 典型的なやらかしパターン

たぶんどこかでこうなってる👇

JavaScript

const imageInfo \= imageInfos; // ← 配列をそのまま代入

または

JavaScript

for (...) {  
  imageInfo \= imageInfos; // ← 上書きミス  
}

✅ 正しい使い方

✔ パターン1:ループする

JavaScript

for (const imageInfo of imageInfos) {  
  console.log(imageInfo.getOutFileName());  
}

✔ パターン2:1つだけ取りたい

JavaScript

const imageInfo \= imageInfos\[0\];  
imageInfo.getOutFileName();

✔ パターン3:map

JavaScript

const names \= imageInfos.map(i => i.getOutFileName());

🧠 今回の本質

  • クラスは壊れてない ✅

  • メソッドも存在する ✅

  • 変数の中身が違うだけ ❌


🔍 追加で確認すると一発

エラー直前で👇

JavaScript

console.log(Array.isArray(imageInfo));

👉 true が出るはず


🚀 ほぼ確定ポイント

エラー箇所:

JavaScript

Epub3Writer.js:807

この行たぶん👇こういうコード

JavaScript

imageInfo.getOutFileName()

👉 ここを

JavaScript

imageInfos\[i\].getOutFileName()

とかに直す必要あり


👍 まとめ

今回のバグは珍しくなくて:

👉 「単体」と「配列」を取り違えた」だけ


もしよければ
Epub3Writer.js 800〜820行 貼ってくれれば
どの変数がズレてるかピンポイント修正出せます 👍

You:

Promise { ImageInfo { id: '0001', outFileName: '0001.jpg', ext: 'jpeg', width: 1512, height: 2150, outWidth: -1, outHeight: -1, isCover: false, rotateAngle: 0 } } object false Promise

ChatGPT:

いいログです、これで完全に特定できます 👍


🎯 原因(確定)

JavaScript

Promise {  
  ImageInfo { ... }  
}

👉 imageInfo は ImageInfo ではなく Promise

だから:

JavaScript

imageInfo.getOutFileName()

👉 ❌ 存在しない(Promiseにはない)


🔥 なぜこうなるか

あなたのコード👇

JavaScript

static async getImageInfo(input)

👉 async = 必ず Promise を返す


つまり👇

JavaScript

const imageInfo \= ImageInfo.getImageInfo(...)

は実際には

JavaScript

const imageInfo \= Promise<ImageInfo\>

✅ 解決方法

✔ 正解:await する

JavaScript

const imageInfo \= await ImageInfo.getImageInfo(input);

❌ NGパターン(今これ)

JavaScript

const imageInfo \= ImageInfo.getImageInfo(input);  
  
imageInfo.getOutFileName(); // ← 💥

🧠 あなたのログの意味

JavaScript

typeof imageInfo           // object(Promiseもobject)  
imageInfo instanceof ImageInfo // false  
imageInfo.constructor.name // "Promise"

👉 完全に Promise


🚀 修正ポイント

Epub3Writer 側

たぶんこうなってる👇

JavaScript

const imageInfo \= ImageInfo.getImageInfo(...);

👉 修正:

JavaScript

const imageInfo \= await ImageInfo.getImageInfo(...);

⚠️ 重要(ここハマりやすい)

呼び出し元も async 必須

JavaScript

async write(...) {  
  const imageInfo \= await ImageInfo.getImageInfo(...);  
}

❗ もし async にできない場合

JavaScript

ImageInfo.getImageInfo(...).then(imageInfo => {  
  imageInfo.getOutFileName();  
});

💡 今回のエラーの変遷まとめ

  1. 最初のエラー
    👉 「関数がない」

  2. 実態
    👉 Promiseに対して呼んでる

  3. 根本原因
    👉 await忘れ


🎯 一発チェック

これを見ればOK👇

JavaScript

console.log(imageInfo.constructor.name);
  • "ImageInfo" → OK

  • "Promise" → ❌ await忘れ


👍 まとめ

👉 今回の本質はこれ

「async関数の戻り値をそのまま使っている」


必要なら
「AozoraEpub3全体で async汚染を最小にする設計」も整理できるので言ってください 👍

You:

Formatの値がない getFormat() { if (!this.ext) return null; return "image/" + (this.ext === "jpg" ? "jpeg" : this.ext); }

ChatGPT:

いいところに気づいてます 👍
この問題はシンプルで、でもハマりやすいやつです。


🎯 結論

👉 this.ext"jpg" じゃなくて "jpeg" になってる


🔍 あなたの現在の実装

JavaScript

getFormat() {  
  if (!this.ext) return null;  
  return "image/" + (this.ext \=== "jpg" ? "jpeg" : this.ext);  
}

❗ 問題点

sharp().metadata()format は👇

JavaScript

"jpeg"

つまり:

JavaScript

this.ext \=== "jpg"  // false

結果👇

JavaScript

"image/" + "jpeg" \= "image/jpeg"

👉 一見OK


🤔 じゃあなぜ「Formatがない」?

考えられる原因は2つ👇


❌ パターン1:ext が undefined / null

JavaScript

if (!this.ext) return null;

👉 ここに入ってる


🔍 原因候補

① metadata.format が undefined

JavaScript

const metadata \= await sharp(input).metadata();

画像によっては:

JavaScript

metadata.format \=== undefined

👉 return null になってる


② 途中で ext を消してる

JavaScript

this.ext \= null;

または

JavaScript

imageInfo.ext \= undefined;

❌ パターン2:formatを使う側の問題

例えば:

JavaScript

const format \= imageInfo.getFormat();
JavaScript

if (!format) {  
  // Formatがない  
}

👉 null が返ってる


✅ 改善案(おすすめ)

✔ extの正規化を入れる

JavaScript

setExt(ext) {  
  if (!ext) {  
    this.ext \= null;  
    return;  
  }  
  
  ext \= ext.toLowerCase();  
  
  if (ext \=== "jpeg") ext \= "jpg";  
  
  this.ext \= ext;  
}

✔ getFormatを強化

JavaScript

getFormat() {  
  if (!this.ext) return null;  
  
  const ext \= this.ext.toLowerCase();  
  
  if (ext \=== "jpg" || ext \=== "jpeg") {  
    return "image/jpeg";  
  }  
  
  return "image/" + ext;  
}

💡 さらに安全にするなら

JavaScript

getFormat() {  
  if (!this.ext) return "image/jpeg"; // fallback  
  
  const ext \= this.ext.toLowerCase();  
  
  switch (ext) {  
    case "jpg":  
    case "jpeg":  
      return "image/jpeg";  
    case "png":  
    case "webp":  
    case "gif":  
    case "avif":  
      return "image/" + ext;  
    default:  
      return "image/" + ext;  
  }  
}

🔍 デバッグ用(ここ重要)

一度これ出してください👇

JavaScript

console.log("ext:", this.ext);

🎯 まとめ

今回の本質👇

  • sharp"jpeg" を返す

  • あなたのコードは "jpg" 前提

  • または ext 自体が undefined


🚀 次にやるといいこと

もし可能なら👇貼ってください

  • getImageInfo() の戻り値ログ

  • getFormat() 呼び出し箇所

👉 どこで ext が消えてるか特定できます

You:

取得したImageInfoにformatがない [ ImageInfo { id: '0001', outFileName: '0001.jpg', ext: 'jpeg', width: 1512, height: 2150, outWidth: -1, outHeight: -1, isCover: false, rotateAngle: 0 }, ImageInfo { id: '0002', outFileName: '0002.webp', ext: 'webp', width: 550, height: 404, outWidth: -1, outHeight: -1, isCover: false, rotateAngle: 0 }, ImageInfo { id: '0003', outFileName: '0003.jpg', ext: 'jpeg', width: 640, height: 400, outWidth: -1, outHeight: -1, isCover: false, rotateAngle: 0 } ]

ChatGPT:

これはバグじゃなくて 設計の勘違い です 👍


🎯 結論

👉 format というプロパティは ImageInfo に存在しない

あなたのクラス👇

JavaScript

this.ext \= ext ? ext.toLowerCase() : null;

👉 保存しているのは ext


🔍 つまり何が起きてるか

ログ👇

JavaScript

ImageInfo {  
  ext: 'jpeg'  
}

👉 正常

でもどこかで👇やってるはず

JavaScript

imageInfo.format

👉 ❌ undefined(そんなプロパティない)


🧠 元ネタ(混乱の原因)

JavaScript

const metadata \= await sharp(input).metadata();

ここでは👇

JavaScript

metadata.format // ← ある

でも👇

JavaScript

new ImageInfo(metadata.format, ...)

👉 formatext に入れてる


✅ 正しい使い方

✔ 方法1(そのまま)

JavaScript

imageInfo.getFormat()

✔ 方法2(ext直接)

JavaScript

imageInfo.getExt()

❌ NG

JavaScript

imageInfo.format // ← 存在しない

💡 もし format プロパティ欲しいなら

✔ getter追加

JavaScript

get format() {  
  return this.getFormat();  
}

これで👇

JavaScript

imageInfo.format

使えるようになる


👍 おすすめ修正(AozoraEpub3的)

Java版に寄せるなら👇

JavaScript

getFormat() {  
  if (!this.ext) return null;  
  
  if (this.ext \=== "jpg" || this.ext \=== "jpeg") {  
    return "image/jpeg";  
  }  
  
  return "image/" + this.ext;  
}

🔥 よくあるミスまとめ

今回の流れ:

  1. sharp.metadata().format は存在

  2. それを ext に保存

  3. でも later で format を参照

  4. 👉 undefined


🎯 まとめ

👉 ImageInfoには format は無い、extだけある


🚀 ワンポイント改善

もしコード全体で混乱してるなら👇

JavaScript

this.format \= ext;

に変えてもいい(Java寄せ)


必要なら
👉 Java版 ImageInfo と完全互換のJS版 作れます(AozoraEpub3用)

You:

修正版が欲しい import fs from "fs"; import sharp from "sharp"; export default class ImageInfo { /** 画像の情報を生成 * @param {string} ext png jpg gif webp など */ constructor(ext, width, height) { this.id = null; // ファイルID this.outFileName = null; // 出力ファイル名 this.ext = ext ? ext.toLowerCase() : null; this.width = width ?? -1; this.height = height ?? -1; this.outWidth = -1; this.outHeight = -1; this.isCover = false; this.rotateAngle = 0; } // ========================= // static 生成メソッド // ========================= /** ファイルから画像情報を生成 */ static async getImageInfoFromFile(imageFile) { const buffer = await fs.promises.readFile(imageFile); return await ImageInfo.getImageInfo(buffer); } /** 画像ストリーム / Buffer から画像情報を生成 */ static async getImageInfo(input) { if (!input) return null; try { const metadata = await sharp(input).metadata(); if (!metadata || !metadata.format) return null; return new ImageInfo( metadata.format, // png jpeg webp gif avif など metadata.width, metadata.height ); } catch (e) { return null; } } /** 既に幅高さが分かっている場合 */ static getImageInfoFromRaw(ext, width, height) { return new ImageInfo(ext, width, height); } // ========================= // getter / setter // ========================= getId() { return this.id; } setId(id) { this.id = id; } getOutFileName() { return this.outFileName; } setOutFileName(file) { this.outFileName = file; } setExt(ext) { this.ext = ext?.toLowerCase(); } getExt() { return this.ext; } /** mime形式(image/png)を返す */ getFormat() { if (!this.ext) return null; return "image/" + (this.ext === "jpg" ? "jpeg" : this.ext); } getIsCover() { return this.isCover; } setIsCover(isCover) { this.isCover = isCover; } getWidth() { return this.width; } setWidth(width) { this.width = width; } getHeight() { return this.height; } setHeight(height) { this.height = height; } getOutWidth() { return this.outWidth; } setOutWidth(outWidth) { this.outWidth = outWidth; } getOutHeight() { return this.outHeight; } setOutHeight(outHeight) { this.outHeight = outHeight; } }

ChatGPT:

了解 👍
AozoraEpub3互換寄り+今回の不具合(format / ext / 安定性)を全部潰した実用版を出します。


✅ 修正版ポイント

  • jpeg → jpg 正規化

  • getFormat() を完全対応(image/jpeg など)

  • format プロパティ互換(Java的に使える)

  • sharpformat 欠損対策

  • null耐性強化

  • デバッグしやすく


🚀 修正版 ImageInfo.js

JavaScript

import fs from "fs";  
import sharp from "sharp";  
  
export default class ImageInfo {  
  /\*\* 画像の情報を生成  
   \* @param {string} ext png jpg gif webp など  
   \*/  
  constructor(ext, width, height) {  
    this.id \= null;                // ファイルID  
    this.outFileName \= null;       // 出力ファイル名  
  
    this.setExt(ext);  
  
    this.width \= width ?? \-1;  
    this.height \= height ?? \-1;  
  
    this.outWidth \= \-1;  
    this.outHeight \= \-1;  
  
    this.isCover \= false;  
    this.rotateAngle \= 0;  
  }  
  
  // =========================  
  // static 生成メソッド  
  // =========================  
  
  /\*\* ファイルから画像情報を生成 \*/  
  static async getImageInfoFromFile(imageFile) {  
    const buffer \= await fs.promises.readFile(imageFile);  
    return await ImageInfo.getImageInfo(buffer);  
  }  
  
  /\*\* 画像ストリーム / Buffer から画像情報を生成 \*/  
  static async getImageInfo(input) {  
    if (!input) return null;  
  
    try {  
      const metadata \= await sharp(input).metadata();  
  
      if (!metadata) return null;  
  
      // formatが取れないケース対策  
      let format \= metadata.format;  
  
      if (!format) {  
        // fallback: jpeg扱い  
        format \= "jpeg";  
      }  
  
      return new ImageInfo(  
        format,  
        metadata.width ?? \-1,  
        metadata.height ?? \-1  
      );  
  
    } catch (e) {  
      return null;  
    }  
  }  
  
  /\*\* 既に幅高さが分かっている場合 \*/  
  static getImageInfoFromRaw(ext, width, height) {  
    return new ImageInfo(ext, width, height);  
  }  
  
  // =========================  
  // getter / setter  
  // =========================  
  
  getId() {  
    return this.id;  
  }  
  
  setId(id) {  
    this.id \= id;  
  }  
  
  getOutFileName() {  
    return this.outFileName;  
  }  
  
  setOutFileName(file) {  
    this.outFileName \= file;  
  }  
  
  /\*\* 拡張子を正規化してセット \*/  
  setExt(ext) {  
    if (!ext) {  
      this.ext \= null;  
      return;  
    }  
  
    ext \= ext.toLowerCase();  
  
    // 正規化(重要)  
    if (ext \=== "jpeg") ext \= "jpg";  
  
    this.ext \= ext;  
  }  
  
  getExt() {  
    return this.ext;  
  }  
  
  /\*\* MIME形式を返す(image/jpegなど) \*/  
  getFormat() {  
    if (!this.ext) return null;  
  
    switch (this.ext) {  
      case "jpg":  
      case "jpeg":  
        return "image/jpeg";  
      case "png":  
      case "webp":  
      case "gif":  
      case "avif":  
        return "image/" + this.ext;  
      default:  
        return "image/" + this.ext;  
    }  
  }  
  
  /\*\* Java互換:formatプロパティとしてもアクセス可能 \*/  
  get format() {  
    return this.getFormat();  
  }  
  
  getIsCover() {  
    return this.isCover;  
  }  
  
  setIsCover(isCover) {  
    this.isCover \= isCover;  
  }  
  
  getWidth() {  
    return this.width;  
  }  
  
  setWidth(width) {  
    this.width \= width;  
  }  
  
  getHeight() {  
    return this.height;  
  }  
  
  setHeight(height) {  
    this.height \= height;  
  }  
  
  getOutWidth() {  
    return this.outWidth;  
  }  
  
  setOutWidth(outWidth) {  
    this.outWidth \= outWidth;  
  }  
  
  getOutHeight() {  
    return this.outHeight;  
  }  
  
  setOutHeight(outHeight) {  
    this.outHeight \= outHeight;  
  }  
}

🎯 これで解決できる問題

✔ format が無い問題

JavaScript

imageInfo.format

👉 使えるようになる


✔ jpeg / jpg 問題

JavaScript

ext \= "jpeg" → "jpg"

👉 統一


✔ sharp の format undefined 問題

👉 fallback対応済み


✔ Javaコード互換

JavaScript

imageInfo.getFormat()  
imageInfo.format

👉 両対応


👍 補足(かなり重要)

AozoraEpub3移植では👇

👉 「ext」と「format」が混在するコードが多い

この修正版はそれを吸収する設計になってます


🚀 次にやると良い

もし今後👇出たら

  • Promise { ImageInfo }

  • 配列ミス

  • async汚染

👉 全部まとめて整理できるので言ってください 👍