Astroで素早く小説用の倉庫サイトをつくる

素早く小説用の倉庫サイトをつくりましょう。

カクヨムとかpixivとかがあるので、投稿先に困っていないなら、自サイトなんていまさら感がありますが、どうやら時代は自サイト回帰の近頃だとかなんとか。

ということで、この記事では、JavaScriptフレームワークであるAstroを使い、ブログサイトのテンプレートベースで、素早く小説用の倉庫サイトをつくりたいと思います。

最も大事にするコンセプトは「小説掲載を手早くできるようにする」です。
このコンセプトを実現するための苦労は惜しまないものとします。

つまり、**立ち上げは手間だけど、維持・保守は手軽**ということです。

大雑把なサイト構成は「Astroでつくり、Gitでソースコードを管理し、Cloudflare Pagesで公開する」なので、必要な部分だけを参考にすることもできるかもしれません。

# 前提

この記事は、プログラミングとかソフトウェア開発とか、そういうことに抵抗感がない方向けの記事です。

つまり、「小説を書くソフトウェアエンジニア」が読者モデルです。

詳細な手順――何をどうして、こうする――は示すつもりですが、カバーしきれないことや手順が変わってしまってそのままでは適用できないステップが出ることが予想されます。

その場合、臨機応変に対応できることが求められますので、ご承知おきください。

# この記事が説明すること・しないこと

## 説明すること

- JavaScriptフレームワークAstroで小説を掲載するサイトをつくる方法
- サイトをCloudflare Pagesで公開する方法
- サイトを更新する方法

## 説明しないこと

- 開発環境のセットアップ方法
Node.js環境が必要ですから、各位準備してください。
この記事でいまさらガイドしなくても、インターネット上に大量に記事が存在しますので、それらを参考にしてください。

- Gitの操作方法
Cloudflare Pagesとの連携はGitHubまたはGitLabが必要だと思います。
Gitの操作方法関連は説明しないので、各位で調べてください。

# 本文

## Astroのインストール

https://docs.astro.build/en/install-and-setup/

上記を参考にして、Astroのテンプレートサイトを立ち上げてください。

`pnpm create astro@latest`コマンドで、対話型のインストールが始まります。
途中のテンプレート選択では「`Use blog template`」を選択することにします。

以下の実行例を参考にしてください

```console
PS E:\subayaku> pnpm create astro@latest 
.../195fe68b3e6-4b4c                     |  +30 +++
.../195fe68b3e6-4b4c                     | Progress: resolved 30, reused 30, downloaded 0, added 30, done

 astro   Launch sequence initiated.

   dir   Where should we create your new project?
         .

  tmpl   How would you like to start your new project?
         Use blog template

  deps   Install dependencies?
         Yes

   git   Initialize a new git repository?
         Yes

      ✔  Project initialized!
         ■ Template copied
         ■ Dependencies installed
         ■ Git initialized

  next   Liftoff confirmed. Explore your project!
         Run pnpm dev to start the dev server. CTRL+C to stop.
         Add frameworks like react or tailwind using astro add.

         Stuck? Join us at https://astro.build/chat
```

セットアップが完了すると、`pnpm dev`で開発用サーバーを立ち上げます。

表示されたローカルサーバーのURLへアクセスすると、テンプレートサイトが見えると思います。

## これから作成するサイトページ構成

基本的なサイトのページ構成はそのまま利用しつつ、以下の様なコンテンツにします。

- インデックスページ（Home）
サイトのファーストビューです。
サイト閲覧にあたっての注意事項、更新履歴を表示することにします。

- 小説一覧ページ（Blog）
サイトの要である小説一覧を表示するページです。
テンプレートのブログ一覧を流用します。

- 小説本文ページ
小説本文を載せるページです。

- サイト説明ページ（About）
サイトを説明したり、筆者の紹介を載せたりするページです。

そのほか、必要に応じてページは増やせますが、この記事では割愛します。

## サイト基本情報の編集

`src/consts.ts`にサイト名等を記載できるので、編集します。

```ts:src/consts.ts
export const SITE_TITLE = 'Subayaku';
export const SITE_SUBTITLE = '小説倉庫サイト';
export const SITE_DESCRIPTION = `高尾島 空が趣味で書いた小説の倉庫サイトです';
```
テンプレートをカスタマイズして、サイト名にプラスして、サイトのサブタイトルのようなキャッチコピーのような文字列を表示することにします。

`SITE_DESCRIPTION`はHTMLのmetaタグのdescriptionに設定されます。

このファイルを編集すると、立ち上げっぱなしの開発サーバーでも更新され、サイトタイトル変更がリアルタイムで反映されることが確認できます。

## ヘッダーの編集

### サイトタイトル

ヘッダーを編集し、`SITE_SUBTITLE`も表示されるようにします。

```react:src/coomponents/Header.astro
---
import HeaderLink from './HeaderLink.astro';
import { SITE_TITLE, SITE_SUBTITLE } from '../consts';
---

<header>
	<nav>
		<h2><a href="/">{SITE_TITLE} | {SITE_SUBTITLE}</a></h2>
```

### ナビゲーションメニュー

各ページのURLは以下の様にすることとします。
さしあたっては、このリンクに変更した後は、対応するページが存在しないため、ページが表示されなくなります。

```react:src/components/Header.astro
<div class="internal-links">
    <HeaderLink href="/">Home</HeaderLink>
    <HeaderLink href="/works">Works</HeaderLink>
    <HeaderLink href="/about">About</HeaderLink>
</div>
```

パソコンモニター等広めのウィンドウでは右上にソーシャルリンクが表示されます。

今回は使用しないため、`<div class="social-links">`の部分を削除しておきます。

## フッターの編集

以下の部分を編集します。
必要ない場合は削除してください。

ソーシャルリンク部分もヘッダーと同様に削除します。

```react:src/components/Footer.astro
	&copy; {today.getFullYear()} Takaoshima Sora. All rights reserved.

```
## インデックスページの編集

インデックスページは`src/pages/index.astro`で編集できます。

### 言語とtitleタグの編集

言語を示すプロパティとtitleタグに表示される文字列を編集します。

`lang="ja"`にします。
titleタグにサイト名だけでなく、`SITE_SUBTITLE`も表示します

```react:src/pages/index.astro
import { SITE_TITLE, SITE_SUBTITLE, SITE_DESCRIPTION } from '../consts';
---
<!doctype html>
<html lang="ja">
	<head>
		<BaseHead title={SITE_TITLE + " | " + SITE_SUBTITLE} description={SITE_DESCRIPTION} />
	</head>
	<body>
```

### コンテンツの編集

インデックスページに表示する文章はHTMLで自由に記載できます。
適当に編集してください。

例では以下の様に書きました。

```react:src/pages/index.astro
---
import BaseHead from "../components/BaseHead.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import { SITE_TITLE, SITE_SUBTITLE, SITE_DESCRIPTION } from "../consts";
---
<!doctype html>
<html lang="ja">
	<head>
		<BaseHead
			title={SITE_TITLE + " | " + SITE_SUBTITLE}
			description={SITE_DESCRIPTION}
		/>
	</head>
	<body>
		<Header />
		<main>
			<h1>いらっしゃいませ。</h1>
			<p>
				このサイトは趣味で小説を書いている
				<ruby>高尾島<rt>たかおしま</rt></ruby>
				<ruby>空<rt>そら</rt></ruby>
				の書いた小説を放りこむサイトです。
			</p>
			<p>
				はじめてお越しの方は、サイトの注意事項を
				<a href="/about">こちら</a>からご確認ください。
			</p>
		</main>
		<Footer />
	</body>
</html>
```

## サイト説明ページの編集

### レイアウトの編集

インデックスページ、小説一覧ページ以外は`src/layouts/BlogPost.astro`で定義しているレイアウトを使用します。

こちらをまず編集するため、上記のastroファイルを開きます。

- 言語設定を日本語にするため、`<html lang="ja">`へ変更します

### コンテンツの編集

続いて、`src/pages/about.astro`を編集します。

ここは好きなように編集してください。

ページタイトル、metaタグのdescription、日付は以下で変更できます。

```react:src/pages/about.astro
<Layout
	title="ページタイトル"
	description="ページ説明文"
	pubDate={new Date('2025/4/1')}
>
```

今回の例では以下のようにしました。

```react:src/pages/about.astro
---
import Layout from '../layouts/BlogPost.astro';
---

<Layout
	title="このサイトについて"
	description="サイト「Subayaku」について"
	pubDate={new Date('2025/4/1')}
>
	<p>このサイトは趣味で小説を書いている高尾島 空の書いた小説を放りこむサイトです。</p>
	<h2>注意事項</h2>
	<ul>
		<li>当サイトのすべてのコンテンツに関して、無断転載禁止です</li>
		<li>なりすましも絶対にしないでください</li>
	</ul>
</Layout>
```

## 小説一覧ページの編集

### コンテンツコレクションの編集

今回は、ブログサイトテンプレートを利用しているため、文章コンテンツを現すスラッグがblogになっています。
そのままでも問題はないのですが、ひとまずblogをworksへ変更しておきます。

```ts:src/ccontent.config.ts
import { glob } from 'astro/loaders';
import { defineCollection, z } from 'astro:content';

const works = defineCollection({
	// Load Markdown and MDX files in the `src/content/blog/` directory.
	loader: glob({ base: './src/content/works', pattern: '**/*.{md,mdx}' }),
	// Type-check frontmatter using a schema
	schema: z.object({
		title: z.string(),
		description: z.string(),
		// Transform string to Date object
		pubDate: z.coerce.date(),
		updatedDate: z.coerce.date().optional(),
		heroImage: z.string().optional(),
	}),
});

export const collections = { works };
```

以上のように、blogになっている部分をworksへ置換しただけです。

続いて、ディレクトリ名も変更します。

`src/pages/blog`のディレクトリ名を`src/pages/works`へ変更してください。

### データ取得編集

上記でディレクトリ名を変えた関係で以下も変更します。

```react:src/pages/works/index.astro
---
import BaseHead from '../../components/BaseHead.astro';
import Header from '../../components/Header.astro';
import Footer from '../../components/Footer.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
import { getCollection } from 'astro:content';
import FormattedDate from '../../components/FormattedDate.astro';

const posts = (await getCollection('works')).sort(
	(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
```

また、`src/content/blog`のディレクトリ名もblogから変更し、`src/content/works`とします。

この段階で、ヘッダーのナビゲーションメニューから`http://localhost:4321/works`が表示されるようになると思います。

ただし、個別ページへ飛ぶリンクが死んでいるので修正します。

```react:src/pages/works/index.astro
<li>
    <a href={`/works/${post.id}/`}>
```

また、個別ページの表示も修正しておきます。

```react:src/pages/works/[...slug].astro
export async function getStaticPaths() {
	const posts = await getCollection(`works`);
	return posts.map((post) => ({
		params: { slug: post.id },
		props: post,
	}));
}
type Props = CollectionEntry<'works'>;
```





### コンテンツの準備

ここで、コンテンツである小説データの準備をしましょう。

`src/content/works`へ小説データをmd形式ファイルで準備します。

ファイル名は任意ですので、今回は`1.md`、`2.md`という形式にしておきます。

`1.md`は以下のようにしておきます。

```md:src/content/works/1.md
---
title: '草枕 夏目漱石'
description: '本文は青空文庫からコピペさせていただきました'
pubDate: 'April 01 2025'
---

本文は青空文庫からコピペさせていただきました

一

　山路やまみちを登りながら、こう考えた。
 ```

各位のスタイルによって、小説本文の改行や段落は異なると思います。

Astroで使用しているMarkdown形式では、各段落は一行開けて並べるスタイルです。

元々の本文の書き方によっては、Markdown形式へ適宜変換させる必要があると思いますので、いつの日か、その変換を行うサイトを作成したいと思います。

今は、それぞれいい感じに表示されるよう、テキストデータを準備してください。

`2.md`は以下のようにしています。

```md:src/content/works/2.md
---
title: '銀河鉄道の夜 宮沢賢治'
description: '本文は青空文庫からコピペさせていただきました'
pubDate: 'April 05 2025'
---

本文は青空文庫からコピペさせていただきました

# 一、午后の授業

「ではみなさんは、そういうふうに川だと云いわれたり、乳の流れたあとだと云われたりしていたこのぼんやりと白いものがほんとうは何かご承知ですか。」先生は、黒板に吊つるした大きな黒い星座の図の、上から下へ白くけぶった銀河帯のようなところを指さしながら、みんなに問といをかけました。

略

# 二、活版所

　ジョバンニが学校の門を出るとき、同じ組の七八人は家へ帰らずカムパネルラをまん中にして校庭の隅すみの桜さくらの木のところに集まっていました。それはこんやの星祭に青いあかりをこしらえて川へ流す烏瓜からすうりを取りに行く相談らしかったのです。

 略
```

Markdown形式については、別途調べてください。
（`src/content/works/markdown-style-guide.md`にスタイルガイドがあるのでご参考）

### レイアウトの編集

これまでと同様に、言語をjaへ変更します。

```react:src/pages/blog/index.astro
<!doctype html>
<html lang="ja">
```

ヒーロー画像が表示されますが、通常はいらないので、ヒーロー画像があるときのみ表示されるようにします。

```react:src/pages/works/index.astro
<li>
    <a href={`/works/${post.id}/`}>
        {post.data.heroImage && <img width={720} height={360} src={post.data.heroImage} alt="" />}
        <h4 class="title">{post.data.title}</h4>

```

## Git管理

以上でおおよその形は完成です。
細かいところは、個人の好みでCSSをいじる必要があるため割愛します。

続いて、これをGitで管理してください。
GitHubでもGitLabでもどちらでもかまいません。
今回サイトをデプロイしようとしているCloudflare Pagesはどちらも対応しているようです。

## 静的サイト生成の確認

これまでは、`pnpm run dev`で開発用サーバーを立ち上げていましたが、サイトをデプロイするために、静的ファイルを生成します。

`pnpm build`を実行して、以下のようなログが表示されること、`dist`ディレクトリが作成されたことを確認してください。

```
15:23:50 [@astrojs/sitemap] `sitemap-index.xml` created at `dist`
15:23:50 [build] 10 page(s) built in 1.73s
15:23:50 [build] Complete!
```

## Cloudflare Pagesでのデプロイ

Gitでの管理を開始し、静的サイト生成も確認できたので、次にCloudflare Pagesへデプロイします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/33042290-dccc-44ec-a90f-822371a696c5.png)

Cloudflareの管理画面から、「Workers & Pages」へアクセスし、「作成」から始めます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/66dea924-79bc-421b-a299-c4b55dc68ad0.png)

サイトのガイドにしたがって、GitHubまたはGitLabのアカウントを接続すると、リポジトリを選択できるようになるため、先ほど作成したサイトのリポジトリを選択、「セットアップの開始」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/24e07cf6-d5ea-43c2-bb08-09918f1dc1b4.png)

後は、「フレームワークプリセット」で「Astro」を選択肢、「保存してデプロイする」をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/6d91f8a2-63f5-4887-8610-27ceedf3c972.png)

これで、ビルドとデプロイが実行され、問題なければCloudflare Pagesが準備したデフォルトURLへ公開されます。
（現在は`{一意なリポジトリ名}.pages.dev`のような形式です）

デプロイ直後はまだ表示されないため、少し経ってからアクセス確認してください。

## 更新

ここまで出来上がれば、後はサイト更新はGitへ更新したい小説のmd形式ファイルを放りこめば、サイトが自動ビルドされ、デプロイされます。

楽ですね。

# まとめ

このような感じで、立ち上げは手間だが、更新は楽なサイトをつくることができました。

テンプレートをそのまま使っているだけなので、あらためて記事にするほどのことでもないと思いましたが、何かしら皆様の役に立ってくれればうれしいです。

細かいレイアウトは、CSS等で各自いい感じにしてください。
気が向けば、もうちょっと小説サイトっぽくなるようなカスタマイズについて、記事にするかもしれません。

また、記事本文中でも書きましたが、小説プレーンテキストをいい感じなmd形式にするツールも気が向いたらリリースするかもしれません。

ご期待せずにお待ちください。

ここまで読んでいただきありがとうございました。