シリーズ目次 前章: 05. AstroPaperで記事を書く:フロントマターと公開制御 次章: 07. AstroPaperのMarkdown表現とアセットの扱い
Table of contents
Open Table of contents
この章のねらい
記事が10本、50本と増えてくると、src/content/posts/ はファイルの山になります。ディレクトリとタグを使った整理の仕方と、それがURLにどう出るのかを見ていきます。URLは後から変えると外部リンクが切れます。早いうちに方針を決めてください。
ファイルパスから公開URLが決まる仕組み
基本は単純です。
| ファイルの場所 | 公開URL |
|---|---|
src/content/posts/hello.md | /posts/hello/ |
src/content/posts/tech/hello.md | /posts/tech/hello/ |
src/content/posts/tech/astro/hello.md | /posts/tech/astro/hello/ |
src/content/posts/ より下のディレクトリ構造が、そのままURLの階層になります。
変換を担当しているコード
この処理は src/utils/getPostPaths.ts にあります。
function getPostPathSegments(filePath: string | undefined): string[] {
return (
filePath
?.replace(BLOG_PATH, "")
.split("/")
.filter(path => path !== "")
.filter(path => !path.startsWith("_"))
.slice(0, -1)
.map(segment => slugifyStr(segment)) ?? []
);
}src/utils/getPostPaths.ts
上から順に、
src/content/postsの部分を取り除く/で分解する- 空の要素を捨てる
_で始まる要素を捨てる.slice(0, -1)で最後の要素(ファイル名)を切り離す- 残ったディレクトリ名をURL向けの表記へ変換する
という流れです。ファイル名そのものは、Astroが記事に割り当てたID(拡張子を除いたパス)の末尾から取り出して連結されます。
ディレクトリ名の変換規則
slugifyStr は、文字列に非ASCII文字が含まれるかどうかで処理を切り替えます。
export const slugifyStr = (str: string): string => {
if (hasNonLatin(str)) {
return kebabcase(str);
}
return slugify(str, { lower: true });
};src/utils/slugify.ts
実際の変換結果は次のようになります。
| 元の文字列 | 変換後 | 使われる処理 |
|---|---|---|
Astro | astro | slugify |
AstroPaper | astropaper | slugify |
E2E Testing | e2e-testing | slugify |
OpenID Connect | openid-connect | slugify |
初心者向け | 初心者向け | kebabcase |
Astro 入門 | astro-入門 | kebabcase |
日本語はそのまま残ります。URLに日本語が入ってもブラウザは扱えますが、共有するとパーセントエンコード(%E5%88%9D... のような表記)に化けて読めなくなります。ディレクトリ名とファイル名はASCIIの小文字とハイフンで統一してください。
ディレクトリの分け方
分けない(フラット)
src/content/posts/
├── hello-world.md
├── astro-basics.md
└── css-tips.md
記事数が少ないうちはこれで十分です。URLも /posts/hello-world/ と短くなります。
カテゴリで分ける
src/content/posts/
├── tech/
│ ├── astro-basics.md
│ └── css-tips.md
└── diary/
└── hello-world.md
URLは /posts/tech/astro-basics/ になります。URLを見ただけで内容の分類が分かるのが利点です。
年で分ける
src/content/posts/
├── 2025/
│ └── hello-world.md
└── 2026/
└── astro-basics.md
時系列で整理したいならこれです。ただしURLに年が入るので、記事を書き直したときに移したくなります。移すとURLが変わります。
シリーズで分ける
src/content/posts/
└── astro-paper/
├── 00-index.md
├── 01-what-is-astro-paper.md
└── 02-getting-started.md
連載向きです。ファイル名に番号を付けておくと、エディタ上で順番どおりに並びます。このシリーズもこの形です。
注意:番号順に並ぶのはファイル一覧だけで、サイト上の記事一覧は日付順です。連載の順序を読者に伝えたい場合は、この章のように目次ページと前後リンクを自分で用意します。
4つの分け方を、生成されるURLと向き不向きで並べると次のようになります。
素材を記事の近くに置く
記事専用の画像を、記事と同じディレクトリに置きたいことがあります。取り込み対象は .md と .mdx だけなので、画像やSVGは自由に置いて構いません。困るのはMarkdownファイルだけです。
src/content/posts/astro-paper/
├── 01-what-is-astro-paper.md
└── assets/
└── layers.svg
記事からは ./assets/layers.svg のような相対パスで参照します。書き方は次章: 07. AstroPaperのMarkdown表現とアセットの扱いで扱います。
補助的なMarkdownを置きたい場合は、ファイル名の先頭に _ を付けてください。取り込みパターン **/[^_]*.{md,mdx} が判定しているのはファイル名だけなので、ディレクトリ名を _drafts/ にしても中身は除外されません。
src/content/posts/astro-paper/
├── _drafts/
│ ├── memo.md ← 取り込まれる(/posts/memo/ として公開される)
│ └── _memo.md ← 取り込まれない
└── _memo.md ← 取り込まれない
_drafts/memo.md が /posts/drafts/memo/ ではなく /posts/memo/ になるのは、URL生成時に _ で始まるディレクトリが読み飛ばされるからです(src/utils/getPostPaths.ts)。隠したつもりが、かえって階層のないURLで公開される。これがいちばん危ない。
3つの置き方を並べると、効くのがファイル名だけであることがはっきりします。
タグの設計
タグはフロントマターの配列で指定します。
tags:
- "Astro"
- "初心者向け"
タグページの生成
タグを付けると、次の2種類のページが自動で作られます。
| URL | 内容 |
|---|---|
/tags/ | 使われているタグの一覧 |
/tags/<タグのスラグ>/ | そのタグが付いた記事の一覧 |
タグ一覧を作っているのは src/utils/getUniqueTags.ts です。
const tags: Tag[] = posts
.filter(postFilter)
.flatMap(post => post.data.tags)
.map(tag => ({ tag: slugifyStr(tag), tagName: tag }))
.filter(
(value, index, self) =>
self.findIndex(tag => tag.tag === value.tag) === index
)
.sort((tagA, tagB) => tagA.tag.localeCompare(tagB.tag));src/utils/getUniqueTags.ts
この処理には性質が2つあります。
- 公開されている記事のタグしか集まらない:
postFilterを通しているため、下書きだけに付けたタグはタグ一覧に出ません。 - 重複判定はスラグで行われる:
tag(URL用のスラグ)が同じものはひとつにまとめられます。
2つ目の性質により、Astro と astro は同じタグになります。表示名には先に見つかったほうの表記が使われるので、記事ごとに大文字小文字がばらついていると表示が安定しません。
表記はプロジェクト内で統一してください。
Astroと書くと決めたら、全記事でそろえます。
表記の違う3つのタグが、スラグ化を経て1つのタグページへ集約される様子が次の図です。
タグを付けるときの目安
- 数を絞る:1記事あたり2〜4個程度。すべての記事に別々のタグを付けると、タグ一覧が記事一覧と変わらなくなります。
- 粒度をそろえる:
プログラミング(広い)とAstroのルーティング(狭い)が混ざると使いにくくなります。 - ASCIIを優先する:URLの読みやすさを重視するなら、タグ名も英数字にしておくと安全です。
タグを省略した記事には自動的に others が付きます。
記事一覧とページ送り
/posts/ は、astro-paper.config.ts の posts.perPage 件ずつに分割されます。
posts: {
perPage: 10,
},astro-paper.config.ts
| URL | 内容 |
|---|---|
/posts/ | 1ページ目 |
/posts/2/ | 2ページ目 |
トップページに出る「Recent Posts」の件数は posts.perIndex で別に指定します。ここには featured: true の記事は含まれません。
並び順
一覧は更新日時(なければ公開日時)の新しい順です。カテゴリもディレクトリも並び順には効きません。
アーカイブページ
/archives/ は、記事を年から月でグループ化して並べるページです。使われるのは pubDatetime で、modDatetime ではありません。いつ書かれたかを軸にした一覧なので、記事を更新しても位置が動きません。
月名の表示には Intl.DateTimeFormat が使われ、サイトの lang 設定に応じた言語で表示されます。lang: "ja" なら「8月」と表示されます。
このページは features.showArchives を false にすると非表示になります。
URLを決めるときの心得
一度公開したURLを変えると、外部からのリンクや検索エンジンの評価が失われます。次の点を最初に決めておくと安心です。
| 決めること | おすすめ |
|---|---|
| ディレクトリで分けるか | 記事数が50を超える見込みなら分ける |
| ファイル名の付け方 | 英小文字とハイフン、内容が分かる短い語 |
| 日付をURLに含めるか | 含めないほうが記事を長く使える |
| タグの表記 | 大文字小文字を統一する |
どうしてもURLを変える必要が出た場合は、ホスティングサービスのリダイレクト機能で旧URLから新URLへ301(恒久的な移動)リダイレクトを設定してください。読者のブックマークも検索エンジンの評価も、これで新URLへ引き継がれます。
canonicalURL は代わりになりません。あれは重複した内容を持つ複数のURLのうちどれを代表とみなすかを検索エンジンへ伝えるだけで、読者を転送する働きはありません。そもそも静的サイトではURLを変えた時点で旧ページが消えるので、旧URL側に canonicalURL を書くことすらできません。
2つは似て見えますが、担う役割はまったく別です。
要点
src/content/posts/より下のディレクトリ構造が、そのまま公開URLの階層になる。- ディレクトリ名とタグ名はスラグ化されるが、日本語はそのまま残るため、ASCIIの小文字とハイフンに統一するのが無難である。
- 取り込み対象から外れるのは
_で始まるファイルだけで、_drafts/のようなディレクトリに入れても中のMarkdownは公開される。 - タグの重複判定はスラグで行われるため、大文字小文字の表記ゆれは1つのタグにまとまり、表示名が不安定になる。
- 記事一覧は更新日時順、アーカイブは公開日時基準の年月別であり、基準が異なる。
参考資料
- AstroPaper 記事追加ガイド — サブディレクトリとURLの関係
- テンプレート同梱ソース
src/utils/getPostPaths.ts— パスからURLを組み立てる処理 - テンプレート同梱ソース
src/utils/slugify.ts— スラグ化の分岐 - テンプレート同梱ソース
src/utils/getUniqueTags.ts— タグ一覧の生成と重複排除 - Astro: Routing — 動的ルートとページ送り
- MDN: Intl.DateTimeFormat — 月名のローカライズ
シリーズ目次 前章: 05. AstroPaperで記事を書く:フロントマターと公開制御 次章: 07. AstroPaperのMarkdown表現とアセットの扱い