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

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

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

6月4日発売の本

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

6月5日発売の本

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

6月6日発売の本

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

6月7日発売の本

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

6月8日発売の本

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

6月9日発売の本

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

6月10日発売の本

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

作ろうWeb小説サイト

astroを使えばWeb小説サイトを簡単に作れるのではないかと思ったので作ってみた。特徴としては次のようになる。

  • なろうバックアップデータを使ってすぐに作れる
  • TXTファイルでルビや傍点やその他青空文庫注記が使える
  • Web小説の連載形式で表示
  • TXTファイルを追加するだけでサイト全体を更新
  • github pagesやnetlifyに無料で公開できる
  • html出力なので国内の静的ホスティングサイトが使える
  • rssやsitemapやGoogle アナリティクスに対応
  • 縦書き横書き、文字の拡大縮小、白黒セピアに対応

AozoraJavaScriptParserの青空文庫をHTML変換して表示する機能をHTMLレンダーとして利用。そのままだと利用しづらかったので改造した。なので青空文庫の注記はだいたいは使える。ただし、青空文庫注記を入れ子構造にするとバグりそうな気がする。

なろうのバックアップデータを見ると公開日や改稿日のデータがないのでクロールしないと取得できない。カクヨムのバックアップデータを見ると日付データはあった。

星の竜宮
急急如律令

[#区切り線]

[#ここから2字下げ]
[#ここから2字上げ]
現代風の浦島太郎
[#ここで字上げ終わり]
[#ここで字下げ終わり]

[#区切り線]


[#改ページ]
[#中見出し]第一 海辺のノマドワーカー[#中見出し終わり]
[#ここから地から1字上げ]
[#ここから1段階小さな文字]
2025/11/01 00:14 公開
[#ここで小さな文字終わり]
[#ここで字上げ終わり]

浦島 太郎(うらしま たろう)、28歳。

astro向けに書誌情報を渡して、目次ページを作る。descriptionは改行も可能にしたい。OGP向けのデータとしても利用する。 novel.json

{
  "title": "星の竜宮",
  "author": "急急如律令",
  "description": "現代風の浦島太郎",
  "status": "serialization"
}

本文からフロントマッターに章や節を抽出して、本文には公開日は必要ないかも。

---
chapter: 
title: 第一 海辺のノマドワーカー
published: 2025/11/01 00:14
updated: 
---

[#ここから地から1字上げ]
[#ここから1段階小さな文字]
2025/11/01 00:14 公開
[#ここで小さな文字終わり]
[#ここで字上げ終わり]

浦島 太郎(うらしま たろう)、28歳。

docs.astro.build

完成イメージ kyukyunyorituryo.github.io

5月28日から6月3日までキンドル新刊ピックアップ

5月28日発売の本

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

5月29日発売の本

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

5月30日発売の本

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

5月31日発売の本

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

6月1日発売の本

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

6月2日発売の本

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

6月3日発売の本

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

小説家になろうのバックアップから小説サイトを構築する

小説ウェブのビュワーにあったらいいなと思う機能

  • 左右の端にページ送りボタン
  • マウスのスクロールでページ内をページ送り

小説家になろうバックアップデータ形式を見てみると、独自の注記で投稿日時や更新日時もないので、バックアップからサイトを再現するのが難しそう。

自前の青空文庫形式としてバックアップしたデータからサイトを構築する。

なろうバックアップデータ下のような形だった。

はじめは書誌情報で「エピソード1開始 」から


【ユーザ情報】
ユーザID:
ユーザ名:

【Nコード】

【タイトル】

【作者名】

------------------------- エピソード1開始 -------------------------
【第1章】

【エピソードタイトル】

【本文】

青空文庫形式のバックアップデータは各話を別々のファイルとして管理されていないので、章や節を大見出し、中見だしと分かれていて、投稿日時と更新日時がある。それをastroで管理しやすいフロントマッターをつけた各話形式にする。 ファイル管理ではまとめたファイルのほうが管理しやすいが、長編になってくると文字数が多くなるので分割したほうがエディターの動作が軽い。

なろうのバックアップデータからサイトを構築する。

あなた:

.txtのindex.astroを作って

ChatGPT:

青空文庫TXTを直接扱うなら、Content Collections を使わずに fs で読む方が簡単です。

例えば、

src/
src/
├── content/
│   └── novels/
│       ├── wagahai.txt
│       ├── sanshiro.txt
│       └── ...
└── pages/
    └── index.astro```

src/ src/ ├── content/ │ └── novels/ │ ├── wagahai.txt │ ├── sanshiro.txt │ └── ... └── pages/ └── index.astro```

とします。

src/pages/index.astro

---
---
import fs from "node:fs/promises";
import path from "node:path";

const novelsDir =
  path.join(
    process.cwd(),
    "src",
    "content",
    "novels"
  );

const files =
  (await fs.readdir(novelsDir))
    .filter(file =>
      file.endsWith(".txt")
    );

const novels = files.map(file => ({
  slug: file.replace(/\.txt$/, ""),
  filename: file,
}));
---

<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>小説一覧</title>
</head>

<body>

<h1>小説一覧</h1>

<ul>
  {
    novels.map(novel => (
      <li>
        <a href={`/books/${novel.slug}`}>
          {novel.slug}
        </a>
      </li>
    ))
  }
</ul>

</body>
</html>```


import fs from "node:fs/promises"; import path from "node:path";

const novelsDir = path.join( process.cwd(), "src", "content", "novels" );

const files = (await fs.readdir(novelsDir)) .filter(file => file.endsWith(".txt") );

const novels = files.map(file => ({ slug: file.replace(/.txt$/, ""), filename: file,

}));

小説一覧

小説一覧

```

ファイル名が

[下村湖人] 西行の眼.txt
[下村湖人] 西行の眼.txt
[夏目漱石] 吾輩は猫である.txt```

[下村湖人] 西行の眼.txt [下村湖人] 西行の眼.txt [夏目漱石] 吾輩は猫である.txt```

なら

---
---
import fs from "node:fs/promises";
import path from "node:path";

const novelsDir =
  path.join(
    process.cwd(),
    "src",
    "content",
    "novels"
  );

const files =
  (await fs.readdir(novelsDir))
    .filter(file =>
      file.endsWith(".txt")
    );

const novels = files.map(file => {

  const match =
    file.match(
      /^\[(.*?)\]\s*(.*?)\.txt$/
    );

  return {
    slug:
      encodeURIComponent(
        file.replace(/\.txt$/, "")
      ),

    author:
      match?.[1] ?? "",

    title:
      match?.[2] ??
      file.replace(/\.txt$/, ""),
  };
});
---```


import fs from "node:fs/promises"; import path from "node:path";

const novelsDir = path.join( process.cwd(), "src", "content", "novels" );

const files = (await fs.readdir(novelsDir)) .filter(file => file.endsWith(".txt") );

const novels = files.map(file => {

const match = file.match( /^[(.?)]\s(.*?).txt$/ );

return { slug: encodeURIComponent( file.replace(/.txt$/, "") ),

author:
  match?.[1] ?? "",

title:
  match?.[2] ??
  file.replace(/\.txt$/, ""),

}; }); ---```

表示

<div class="novel-list">
<div class="novel-list">
  {
    novels.map(novel => (
      <article class="card">

        <h2>
          <a href={`/books/${novel.slug}`}>
            {novel.title}
          </a>
        </h2>

        <p>{novel.author}</p>

      </article>
    ))
  }
</div>```

{ novels.map(novel => (

    <h2>
      <a href={`/books/${novel.slug}`}>
        {novel.title}
      </a>
    </h2>

    <p>{novel.author}</p>

  </article>
))

}

```

<style>
<style>
body {
  max-width: 900px;
  margin: auto;
  padding: 1rem;
}

.novel-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.card {
  border: 1px solid #ddd;
  padding: 1rem;
}

.card h2 {
  margin: 0;
}

.card a {
  text-decoration: none;
}
</style>```

```

const novels = await Promise.all(
const novels = await Promise.all(
  files.map(async file => {

    const stat =
      await fs.stat(
        path.join(novelsDir, file)
      );

    return {
      file,
      mtime: stat.mtime,
    };
  })
);

novels.sort(
  (a, b) =>
    b.mtime.getTime() -
    a.mtime.getTime()
);```

const novels = await Promise.all( const novels = await Promise.all( files.map(async file => {

const stat =
  await fs.stat(
    path.join(novelsDir, file)
  );

return {
  file,
  mtime: stat.mtime,
};

}) );

novels.sort( (a, b) => b.mtime.getTime() - a.mtime.getTime() );```

もし

---

title: author: published:

updated:

本文```


title: author: published:

updated:

本文```

のような frontmatter をTXTの先頭に付けるなら、

npm
npm install gray-matter```

npm npm install gray-matter```

して

import
import matter from "gray-matter";

const raw =
  await fs.readFile(filePath, "utf-8");

const { data, content } =
  matter(raw);```

import import matter from "gray-matter";

const raw = await fs.readFile(filePath, "utf-8");

const { data, content } = matter(raw);```

で扱えます。

青空文庫パーサーと組み合わせるなら、この方式がかなり便利です。

ChatGPT:

毎回 meta.json を手動で書き換えるのは面倒なので、通常は次のどちらかにします。

meta.json には作品情報だけを持たせる。

{
{
  "title": "異世界転移",
  "author": "作者名",
  "description": "あらすじ"
}```

{ { "title": "異世界転移", "author": "作者名", "description": "あらすじ" }```

話数情報は持たない。

Astro側で

const
const files = await fs.readdir(
  "src/content/novels/isekai"
);

const episodes = files
  .filter(f => f.endsWith(".txt"))
  .sort();```

const const files = await fs.readdir( "src/content/novels/isekai" );

const episodes = files .filter(f => f.endsWith(".txt")) .sort();```

として自動取得する。

新しく

0005.txt
0005.txt```

0005.txt 0005.txt```

を追加しても、

meta.json
meta.json```

meta.json meta.json```

は変更不要です。

ZIP作成時に

{
{
  "chapters": [
    {
      "title": "異世界転移",
      "episodes": [
        {
          "id": 1,
          "title": "登場人物紹介"
        },
        {
          "id": 2,
          "title": "始まりそして転移"
        }
      ]
    }
  ]
}```

{ { "chapters": [ { "title": "異世界転移", "episodes": [ { "id": 1, "title": "登場人物紹介" }, { "id": 2, "title": "始まりそして転移" } ] } ] }```

を生成する。

人間は編集しない。

TXTを増やしたら分割ツールを再実行して作り直す。

各話に Frontmatter を持たせる。

0001.txt

---

chapter: 異世界転移 title: 登場人物紹介

published: 2020-03-02

本文```


chapter: 異世界転移 title: 登場人物紹介

published: 2020-03-02

本文```

0002.txt

---

chapter: 異世界転移 title: 始まりそして転移

published: 2020-03-03

本文```


chapter: 異世界転移 title: 始まりそして転移

published: 2020-03-03

本文```

Astro側で

import
import matter from "gray-matter";

const raw = await fs.readFile(file);
const { data } = matter(raw);```

import import matter from "gray-matter";

const raw = await fs.readFile(file); const { data } = matter(raw);```

して目次を組み立てる。

私なら

novels/
novels/
└─ isekai/
   ├─ novel.json
   ├─ 0001.txt
   ├─ 0002.txt
   └─ 0003.txt```

novels/ novels/ └─ isekai/ ├─ novel.json ├─ 0001.txt ├─ 0002.txt └─ 0003.txt```

にします。

novel.json

{
{
  "title": "異世界転移",
  "author": "作者名",
  "description": "あらすじ"
}```

{ { "title": "異世界転移", "author": "作者名", "description": "あらすじ" }```

各話

---

chapter: 異世界転移 title: 登場人物紹介

published: 2020-03-02

本文```


chapter: 異世界転移 title: 登場人物紹介

published: 2020-03-02

本文```

こうすると、

  • 作品情報 → novel.json
  • 話情報 → 各TXTのFrontmatter

作品情報 → novel.json

話情報 → 各TXTのFrontmatter

で分離できるので、

0004.txt
0004.txt
0005.txt```

0004.txt 0004.txt 0005.txt```

を追加しても novel.json は変更不要です。

青空文庫形式のWeb小説バックアップをAstroで読む用途なら、この構成が一番管理しやすいと思います。

あなた:

原稿の管理方法として青空文庫形式で一つのファイルを書いて変換させるのか、txtファイルを増やすのかどちらがいいのか

ChatGPT:

これは運用方針によりますが、Web小説バックアップや自作小説管理なら、それぞれに向いている用途があります。

無職転生.txt
無職転生.txt

[#改ページ]
[#大見出し]第一章[#大見出し終わり]

[#中見出し]第一話[#中見出し終わり]
本文

[#改ページ]
[#中見出し]第二話[#中見出し終わり]
本文```

無職転生.txt 無職転生.txt

[#改ページ] [#大見出し]第一章[#大見出し終わり]

[#中見出し]第一話[#中見出し終わり] 本文

[#改ページ] [#中見出し]第二話[#中見出し終わり] 本文```

  • 原稿が1ファイルで管理できる
  • 検索しやすい
  • Git管理しやすい
  • EPUB化しやすい
  • 青空文庫形式との相性が良い
  • バックアップが簡単

原稿が1ファイルで管理できる

検索しやすい

Git管理しやすい

EPUB化しやすい

青空文庫形式との相性が良い

バックアップが簡単

  • 10万~100万文字になると重い
  • 話単位の編集が少し面倒
  • 差分が大きくなる

10万~100万文字になると重い

話単位の編集が少し面倒

差分が大きくなる

無職転生/
無職転生/
├─ novel.json
├─ 0001.txt
├─ 0002.txt
├─ 0003.txt
└─ ...```

無職転生/ 無職転生/ ├─ novel.json ├─ 0001.txt ├─ 0002.txt ├─ 0003.txt └─ ...```

  • 投稿サイトに近い
  • 話単位で編集できる
  • 更新管理しやすい
  • Astroで扱いやすい
  • 前話・次話生成が簡単

投稿サイトに近い

話単位で編集できる

更新管理しやすい

Astroで扱いやすい

前話・次話生成が簡単

  • ファイル数が増える
  • 全文検索が面倒
  • 章移動時に整理が必要

ファイル数が増える

全文検索が面倒

章移動時に整理が必要

私は

原本
原本
↓
1ファイル```

原本 原本 ↓ 1ファイル```

をおすすめします。

例えば

src/source/
src/source/
└─ mushoku.txt```

src/source/ src/source/ └─ mushoku.txt```

を編集する。

そしてビルド時や変換時に

mushoku.txt
mushoku.txt
↓
分割
↓
dist/
 ├─ 0001.txt
 ├─ 0002.txt
 └─ ...```

mushoku.txt mushoku.txt ↓ 分割 ↓ dist/ ├─ 0001.txt ├─ 0002.txt └─ ...```

を生成する。

Gitなら

mushoku.txt
mushoku.txt```

mushoku.txt mushoku.txt```

1つの方が管理しやすいです。

章構成変更も

第一章
第一章
↓
プロローグ```

第一章 第一章 ↓ プロローグ```

だけで済みます。

公開用は

0001.txt
0001.txt
0002.txt
0003.txt```

0001.txt 0001.txt 0002.txt 0003.txt```

の方が便利です。

URL

/books/mushoku/1
/books/mushoku/1
/books/mushoku/2
/books/mushoku/3```

/books/mushoku/1 /books/mushoku/1 /books/mushoku/2 /books/mushoku/3```

を作りやすいからです。

編集用

source/
source/
└─ 無職転生.txt```

source/ source/ └─ 無職転生.txt```

公開用

content/
content/
└─ 無職転生/
    ├─ 0001.txt
    ├─ 0002.txt
    └─ ...```

content/ content/ └─ 無職転生/ ├─ 0001.txt ├─ 0002.txt └─ ...```

という二段構成にします。

青空文庫形式原稿
青空文庫形式原稿
↓
分割ツール
↓
Astro用データ
↓
サイト生成```

青空文庫形式原稿 青空文庫形式原稿 ↓ 分割ツール ↓ Astro用データ ↓ サイト生成```

です。

あなたはすでに AozoraText2Html() や青空文庫記法を扱うコードを持っているので、原稿は1ファイルで管理し、サイト生成時に話ごとのTXTへ自動分割する方式 が最も保守しやすく、EPUB生成やバックアップにも流用しやすいと思います。

あなた:

小説個別のページにタイトルやあらすじと各話へのリンクを作るには

ChatGPT:

その場合は、作品ごとに

src/content/novels/
src/content/novels/
└── isekai/
    ├── novel.json
    ├── 0001.txt
    ├── 0002.txt
    └── 0003.txt```

src/content/novels/ src/content/novels/ └── isekai/ ├── novel.json ├── 0001.txt ├── 0002.txt └── 0003.txt```

のような構成にして、

novel.json に作品情報を持たせると管理しやすいです。

{
{
  "title": "異世界転移",
  "author": "山田太郎",
  "description": "平凡な高校生が異世界へ転移する物語。",
  "status": "completed"
}```

{ { "title": "異世界転移", "author": "山田太郎", "description": "平凡な高校生が異世界へ転移する物語。", "status": "completed" }```

src/pages/books/[novel]/index.astro
src/pages/books/[novel]/index.astro```

src/pages/books/[novel]/index.astro src/pages/books/[novel]/index.astro```

/books/isekai/
/books/isekai/```

/books/isekai/ /books/isekai/```

---

import fs from "node:fs/promises"; import path from "node:path";

const { novel } = Astro.params;

const novelDir = path.join( process.cwd(), "src", "content", "novels", novel );

const meta = JSON.parse( await fs.readFile( path.join(novelDir, "novel.json"), "utf-8" ) );

const files = ( await fs.readdir(novelDir) ) .filter(file => /^\d+.txt$/.test(file)) .sort();

const episodes = files.map((file, index) => ({ no: index + 1, file

}));

<html lang="ja"> <head> <meta charset="UTF-8" /> <title>{meta.title}</title> </head>

<body>

<h1>{meta.title}</h1>

<p>作者:{meta.author}</p>

<p>{meta.description}</p>

<p> 状態: {meta.status === "completed" ? "完結" : "連載中"} </p>

<hr>

<h2>目次</h2>

<ol> { episodes.map(ep => ( <li> <a href={/books/${novel}/${ep.no}}> 第{ep.no}話 </a> </li> )) } </ol>

</body> </html>```


import fs from "node:fs/promises"; import path from "node:path";

const { novel } = Astro.params;

const novelDir = path.join( process.cwd(), "src", "content", "novels", novel );

const meta = JSON.parse( await fs.readFile( path.join(novelDir, "novel.json"), "utf-8" ) );

const files = ( await fs.readdir(novelDir) ) .filter(file => /^\d+.txt$/.test(file)) .sort();

const episodes = files.map*1;

{meta.title}

{meta.title}

作者:{meta.author}

{meta.description}

状態: {meta.status === "completed" ? "完結" : "連載中"}


目次

    { episodes.map(ep => (
  1. 第{ep.no}話
  2. )) }

```

さらに各話の Frontmatter を読むと、

0001.txt

---

title: 登場人物紹介 chapter: 異世界転移

published: 2020-03-02

本文```


title: 登場人物紹介 chapter: 異世界転移

published: 2020-03-02

本文```

からタイトルを取得できます。

---

import matter from "gray-matter";

const episodes = await Promise.all( files.map(async (file) => {

const raw = await fs.readFile(
  path.join(novelDir, file),
  &#34;utf-8&#34;
);

const { data } = matter(raw);

return {
  no: Number(
    file.replace(&#34;.txt&#34;, &#34;&#34;)
  ),
  title: data.title,
  chapter: data.chapter
};

}) ); ---```


import matter from "gray-matter";

const episodes = await Promise.all( files.map(async (file) => {

const raw = await fs.readFile(
  path.join(novelDir, file),
  "utf-8"
);

const { data } = matter(raw);

return {
  no: Number(
    file.replace(".txt", "")
  ),
  title: data.title,
  chapter: data.chapter
};

}) ); ---```

表示

<ol>
<ol>
{
  episodes.map(ep => (
    <li>
      <a href={`/books/${novel}/${ep.no}`}>
        {ep.title}
      </a>
    </li>
  ))
}
</ol>```

      { episodes.map(ep => (
    1. {ep.title}
    2. )) }
    ```

    もし分割時に

    ---

    chapter: 異世界転移 title: 登場人物紹介 ---```


    chapter: 異世界転移 title: 登場人物紹介 ---```

    を保存しているなら、

    const chapters = {};
    const chapters = {};
    
    for (const ep of episodes) {
      if (!chapters[ep.chapter]) {
        chapters[ep.chapter] = [];
      }
    
      chapters[ep.chapter].push(ep);
    }```
    

    const chapters = {}; const chapters = {};

    for (const ep of episodes) { if (!chapters[ep.chapter]) { chapters[ep.chapter] = []; }

    chapters[ep.chapter].push(ep); }```

    表示

    {
    {
      Object.entries(chapters).map(
        ([chapter, eps]) => (
          <>
            <h2>{chapter}</h2>
    
            <ol>
              {
                eps.map(ep => (
                  <li>
                    <a href={`/books/${novel}/${ep.no}`}>
                      {ep.title}
                    </a>
                  </li>
                ))
              }
            </ol>
          </>
        )
      )
    }```
    

    { { Object.entries(chapters).map( ([chapter, eps]) => ( <>

    {chapter}

        <ol>
          {
            eps.map(ep => (
              <li>
                <a href={`/books/${novel}/${ep.no}`}>
                  {ep.title}
                </a>
              </li>
            ))
          }
        </ol>
      </>
    )
    

    ) }```

    結果は

    異世界転移
    異世界転移
    
      登場人物紹介
      始まりそして転移
    
    魔王との戦い
    
      王都へ
      勇者召喚```
    

    異世界転移 異世界転移

    登場人物紹介 始まりそして転移

    魔王との戦い

    王都へ 勇者召喚```

    のような小説投稿サイト風の目次になります。

    青空文庫形式の1ファイルから分割するなら、分割時に各話の Frontmatter に

    chapter:
    chapter:
    title:
    published:
    updated:```
    

    chapter: chapter: title: published: updated:```

    を書き込んでおくと、Astro側はそれを読むだけで作品ページと目次ページを生成できます。

    *1:file, index) => ({ no: index + 1, file

    }

5月21日から5月27日までキンドル新刊ピックアップ

5月21日発売の本

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

5月22日発売の本

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

5月23日発売の本

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

5月24日発売の本

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

5月25日発売の本

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

5月26日発売の本

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

5月27日発売の本

湾

Amazon

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