@tenjuu99/blog 0.2.56 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/spec.md ADDED
@@ -0,0 +1,849 @@
1
+ # @tenjuu99/blog — 技術仕様書
2
+
3
+ このドキュメントは `@tenjuu99/blog` の技術仕様を定義します。
4
+
5
+ ## プロジェクト概要
6
+
7
+ `@tenjuu99/blog` は、Markdownファイルから静的HTMLサイトを生成するNode.js製の軽量静的サイトジェネレーターです。
8
+
9
+ ### 主な特徴
10
+
11
+ - Markdownファイルベースのコンテンツ管理
12
+ - フロントマターによるメタデータ定義
13
+ - テンプレートエンジン機能(変数展開、条件分岐、スクリプト実行)
14
+ - ホットリロード対応の開発サーバー
15
+ - CSSの自動結合・minify・キャッシュバスト
16
+ - ヘルパー関数による拡張性
17
+ - パッケージシステムによる機能拡張
18
+
19
+ ### 動作環境
20
+
21
+ - Node.js >= 21.7
22
+ - 依存パッケージ:
23
+ - `marked` ^13.x (Markdown → HTML変換)
24
+ - `chokidar` ^4.0.x (ファイル監視)
25
+
26
+ ## CLIコマンド
27
+
28
+ ```bash
29
+ npx create-blog # 新規プロジェクト作成
30
+ npx server # 開発サーバー起動(ホットリロード対応)
31
+ npx generate # 静的サイト生成
32
+ ```
33
+
34
+ ## ディレクトリ構成
35
+
36
+ ### ユーザープロジェクト構成
37
+
38
+ ```
39
+ project/
40
+ ├── blog.json # 設定ファイル
41
+ ├── src/ # ソースディレクトリ(デフォルト)
42
+ │ ├── pages/ # Markdownコンテンツ
43
+ │ ├── template/ # HTMLテンプレート
44
+ │ ├── css/ # スタイルシート
45
+ │ ├── image/ # 画像ファイル
46
+ │ ├── helper/ # ヘルパー関数
47
+ │ └── packages/ # カスタムパッケージ
48
+ ├── dist/ # ビルド出力(デフォルト)
49
+ └── .cache/ # ビルド時キャッシュ(自動生成)
50
+ ```
51
+
52
+ ### ライブラリ内部構成
53
+
54
+ ```
55
+ @tenjuu99/blog/
56
+ ├── lib/ # コアライブラリ
57
+ ├── bin/ # CLIコマンド
58
+ ├── src-sample/ # サンプルプロジェクト
59
+ ├── packages/ # コアパッケージ
60
+ │ ├── breadcrumbs # パンくずリスト
61
+ │ ├── editor # エディター機能
62
+ │ └── turbolink # Turbolink機能
63
+ └── index.js # エントリーポイント
64
+ ```
65
+
66
+ ## 設定ファイル (blog.json)
67
+
68
+ プロジェクトルートに `blog.json` を配置します。
69
+
70
+ ```json
71
+ {
72
+ "site_name": "サイト名",
73
+ "url_base": "http://localhost:8000",
74
+ "src_dir": "src",
75
+ "dist_dir": "dist",
76
+ "distribute_raw": "image,js",
77
+ "helper": "index.js",
78
+ "packages": "breadcrumbs,turbolink",
79
+ "relative_path": "",
80
+ "allowedSrcExt": "md|html|txt"
81
+ }
82
+ ```
83
+
84
+ ### 設定項目
85
+
86
+ | 項目 | デフォルト | 説明 |
87
+ |------|-----------|------|
88
+ | `site_name` | `"default"` | サイト名 |
89
+ | `url_base` | `"http://localhost:8000"` | ベースURL |
90
+ | `src_dir` | `"src"` | ソースディレクトリ |
91
+ | `dist_dir` | `"dist"` | 出力ディレクトリ |
92
+ | `distribute_raw` | `"image"` | そのままコピーするディレクトリ(カンマ区切り) |
93
+ | `helper` | `""` | ヘルパーファイル(カンマ区切り) |
94
+ | `packages` | `""` | 使用するパッケージ(カンマ区切り) |
95
+ | `relative_path` | `""` | 相対パス(サブディレクトリ配置時) |
96
+ | `allowedSrcExt` | `"md\|html\|txt"` | 処理対象の拡張子(正規表現) |
97
+
98
+ ## フロントマター仕様
99
+
100
+ Markdownファイルの冒頭で `---` または `<!--` で囲んだ領域にメタデータを記述します。
101
+
102
+ ### Markdown形式
103
+
104
+ ```markdown
105
+ ---
106
+ title: ページタイトル
107
+ url: /custom-url
108
+ published: 2024-03-18
109
+ template: default.html
110
+ description: ページの説明
111
+ ---
112
+
113
+ # コンテンツ
114
+ ```
115
+
116
+ ### HTML形式
117
+
118
+ ```html
119
+ <!--
120
+ title: ページタイトル
121
+ url: /custom-url
122
+ -->
123
+ <h1>コンテンツ</h1>
124
+ ```
125
+
126
+ ### 組み込み変数
127
+
128
+ | 変数名 | デフォルト値 | 説明 |
129
+ |--------|-------------|------|
130
+ | `name` | ファイル名(拡張子なし) | ページの内部名 |
131
+ | `title` | `name` の値 | ページタイトル |
132
+ | `url` | `/${name}` | URLパス |
133
+ | `description` | 本文先頭150文字 | ページ説明 |
134
+ | `og_description` | `description` と同じ | OGP説明文 |
135
+ | `published` | `"1970-01-01"` | 公開日 |
136
+ | `preview` | `false` | プレビューモード(`true` で `/preview` 配下) |
137
+ | `index` | `true` | インデックスに含めるか |
138
+ | `noindex` | `false` | `noindex` メタタグ出力 |
139
+ | `lang` | `"ja"` | 言語コード |
140
+ | `distribute` | `true` | 配布対象とするか |
141
+ | `template` | `"default.html"` | 使用テンプレート |
142
+ | `ext` | `"html"` | 出力ファイル拡張子 |
143
+ | `site_name` | `config.site_name` | サイト名 |
144
+ | `url_base` | `config.url_base` | ベースURL |
145
+ | `relative_path` | `config.relative_path` | 相対パス |
146
+ | `markdown` | 解析後のHTML | Markdown変換後のHTML |
147
+ | `markdown_not_parsed` | フロントマター除去後 | 変換前のMarkdown |
148
+ | `full_url` | 自動生成 | 完全なURL |
149
+ | `__output` | 自動生成 | 出力ファイルパス |
150
+ | `__filetype` | 拡張子 | 元ファイルの拡張子 |
151
+
152
+ ### データ型の記述
153
+
154
+ フロントマター内のデータ型:
155
+
156
+ ```yaml
157
+ # 文字列
158
+ title: ページタイトル
159
+
160
+ # 数値
161
+ price: 1000
162
+
163
+ # 真偽値
164
+ published: true
165
+
166
+ # 配列(JSON形式)
167
+ tags: ["tag1", "tag2", "tag3"]
168
+
169
+ # オブジェクト(JSON形式)
170
+ metadata: {"author": "名前", "year": 2024}
171
+
172
+ # 複数行文字列(ダブルクォートで開始・終了)
173
+ description: "これは
174
+ 複数行にわたる
175
+ 説明文です"
176
+
177
+ # config参照
178
+ url_base: config.url_base
179
+ ```
180
+
181
+ **重要**: 配列・オブジェクトは `JSON.parse()` で解析されるため、有効なJSON形式である必要があります。
182
+
183
+ ## テンプレート記法
184
+
185
+ `src/template/` 以下にHTMLテンプレートを配置します。
186
+
187
+ ### 変数展開
188
+
189
+ ```html
190
+ {{ 変数名 }}
191
+ ```
192
+
193
+ **重要**: 変数名は強制的に小文字化されます(`{{TITLE}}` も `{{title}}` として解釈)。
194
+
195
+ ### エスケープ
196
+
197
+ ```html
198
+ \{{ 変数名 }} <!-- {{ 変数名 }} と出力される -->
199
+ ```
200
+
201
+ ### includeディレクティブ
202
+
203
+ テンプレートやCSSファイルを読み込みます。
204
+
205
+ ```html
206
+ {include('template/header.html')}
207
+ {include('css/reset.css')}
208
+ ```
209
+
210
+ - 再帰的にincludeを解決します
211
+ - キャッシュされるため同じファイルは1度だけ読み込まれます
212
+
213
+ ### 条件分岐 (if)
214
+
215
+ ```html
216
+ {if 変数名}
217
+ 表示される内容
218
+ {/if}
219
+
220
+ {if 変数名}
221
+ trueの場合
222
+ {else}
223
+ falseの場合
224
+ {/if}
225
+ ```
226
+
227
+ #### 比較演算子
228
+
229
+ ```html
230
+ {if 変数A == 変数B} ... {/if}
231
+ {if 変数A != 変数B} ... {/if}
232
+ {if 変数A == "文字列"} ... {/if}
233
+ {if 変数A == 100} ... {/if}
234
+ ```
235
+
236
+ #### ヘルパー関数による条件
237
+
238
+ ```html
239
+ {if ヘルパー関数名(引数)} ... {/if}
240
+ ```
241
+
242
+ ### スクリプト実行 (SSG)
243
+
244
+ ビルド時にJavaScriptを実行してHTML生成します。
245
+
246
+ ```html
247
+ <script type="ssg">
248
+ return (new Date()).toString()
249
+ </script>
250
+
251
+ <!-- 短縮記法 -->
252
+ {script}
253
+ return variables.title.toUpperCase()
254
+ {/script}
255
+ ```
256
+
257
+ #### 利用可能なオブジェクト
258
+
259
+ - `variables`: 現在のページデータ(フロントマター + 組み込み変数)
260
+ - `helper`: ヘルパー関数オブジェクト
261
+
262
+ #### 注意点
263
+
264
+ - `return` で返した値がHTMLに展開されます
265
+ - `Promise` を返すこともできます(`async/await` 対応)
266
+ - `undefined` / `null` は空文字列として扱われます
267
+
268
+ ### CSSジェネレーター記法
269
+
270
+ 複数のCSSファイルを結合・minify・キャッシュバストします。
271
+
272
+ ```html
273
+ <link rel="stylesheet" href="${/css/bundle.css<<reset.css,layout.css,page.css}">
274
+ ```
275
+
276
+ ↓ビルド時に次のように変換されます:
277
+
278
+ ```html
279
+ <link rel="stylesheet" href="/css/bundle.css?t=a1b2c3d4e5f6...">
280
+ ```
281
+
282
+ - `${出力先<<元ファイル1,元ファイル2,...}` の形式
283
+ - MD5ハッシュによるキャッシュバスト
284
+ - `dist/css/` に結合・minifyされたCSSが出力されます
285
+
286
+ ## ヘルパー関数
287
+
288
+ `src/helper/` 以下にJavaScriptファイルを配置し、`blog.json` の `helper` で指定します。
289
+
290
+ ### 定義方法
291
+
292
+ ```javascript
293
+ // src/helper/index.js
294
+ import { allData, config } from '@tenjuu99/blog'
295
+
296
+ export function dateFormat(dateString) {
297
+ const date = new Date(dateString)
298
+ return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
299
+ }
300
+
301
+ export function readIndex(filter = null) {
302
+ const data = Object.entries(allData)
303
+ .sort((a, b) => new Date(b[1].published) - new Date(a[1].published))
304
+ return filter
305
+ ? data.filter(v => v[0].indexOf(filter) === 0).map(v => v[1])
306
+ : data.map(v => v[1])
307
+ }
308
+ ```
309
+
310
+ ### 使用方法
311
+
312
+ #### テンプレート内
313
+
314
+ ```html
315
+ {{ dateFormat(published) }}
316
+ {{ readIndex('post') }}
317
+ ```
318
+
319
+ #### スクリプト内
320
+
321
+ ```html
322
+ <script type="ssg">
323
+ const posts = helper.readIndex('post')
324
+ return posts.map(p => `<li>${p.title}</li>`).join('')
325
+ </script>
326
+ ```
327
+
328
+ ### `allData` オブジェクト
329
+
330
+ 全ページデータを保持するオブジェクトです。
331
+
332
+ **キー形式**: 先頭スラッシュなしのパス(`pages/sample.md` → `sample`)
333
+
334
+ ```javascript
335
+ {
336
+ "sample": { title: "...", url: "/sample", ... },
337
+ "post/1": { title: "...", url: "/post/1", ... },
338
+ "post/2": { title: "...", url: "/post/2", ... }
339
+ }
340
+ ```
341
+
342
+ **注意**: `readIndex()` などでフィルタリングする際は先頭スラッシュを付けません。
343
+
344
+ ```javascript
345
+ // 正しい
346
+ helper.readIndex('post')
347
+
348
+ // 間違い(マッチしない)
349
+ helper.readIndex('/post')
350
+ ```
351
+
352
+ ## パッケージシステム
353
+
354
+ コアパッケージまたはカスタムパッケージを有効化できます。
355
+
356
+ ### コアパッケージ
357
+
358
+ `@tenjuu99/blog/packages/` に含まれるパッケージ:
359
+
360
+ - `breadcrumbs` - パンくずリスト機能
361
+ - `editor` - エディター機能
362
+ - `turbolink` - Turbolink機能
363
+ - `category` - カテゴリー機能(階層型カテゴリーページ自動生成)
364
+
365
+ #### category パッケージ詳細
366
+
367
+ 階層型カテゴリーの自動ページ生成機能を提供します。
368
+
369
+ **基本的な使い方:**
370
+
371
+ 1. ページのフロントマターでカテゴリーを指定:
372
+ ```yaml
373
+ ---
374
+ title: React入門
375
+ category: ["Tech", "Frontend", "React"]
376
+ ---
377
+ ```
378
+
379
+ 2. `blog.json` で設定:
380
+ ```json
381
+ {
382
+ "packages": "category",
383
+ "hooks": {
384
+ "afterIndexing": "categoryIndexer.js"
385
+ },
386
+ "category": {
387
+ "template": "category.html",
388
+ "auto_generate": true,
389
+ "max_depth": 3,
390
+ "url_case": "lower",
391
+ "url_separator": "-",
392
+ "url_prefix": ""
393
+ }
394
+ }
395
+ ```
396
+
397
+ 3. ビルド時に以下のページが自動生成されます:
398
+ - `/tech/index.html`
399
+ - `/tech/frontend/index.html`
400
+ - `/tech/frontend/react/index.html`
401
+
402
+ **スペース変換とURLプレフィックスの使用例:**
403
+
404
+ ```json
405
+ {
406
+ "category": {
407
+ "url_case": "lower",
408
+ "url_separator": "-",
409
+ "url_prefix": "/books/category"
410
+ }
411
+ }
412
+ ```
413
+
414
+ カテゴリー指定: `category: ["Contemporary Art", "Abstract Painting"]`
415
+
416
+ 生成されるURL:
417
+ - `/books/category/contemporary-art/index.html`
418
+ - `/books/category/contemporary-art/abstract-painting/index.html`
419
+
420
+ **設定オプション:**
421
+
422
+ | オプション | 説明 | デフォルト |
423
+ |-----------|------|-----------|
424
+ | `name` | カテゴリーシステムの識別子(`categories` 配列使用時のみ) | - |
425
+ | `path_filter` | 対象とするページのパスプレフィックス(`categories` 配列使用時に推奨) | `""` (全ページ) |
426
+ | `template` | カテゴリーページのテンプレート | `category.html` |
427
+ | `auto_generate` | 自動生成の有効/無効 | `true` |
428
+ | `max_depth` | カテゴリーの最大階層数 | `3` |
429
+ | `url_case` | URLの大文字小文字変換 (`lower`/`original`) | `lower` |
430
+ | `url_separator` | スペースの置き換え文字 | `"-"` |
431
+ | `url_prefix` | カテゴリーURLのプレフィックス | `""` |
432
+
433
+ **設定オプションの詳細:**
434
+
435
+ - **`name`**: カテゴリーシステムの識別子。`categories` 配列で複数のカテゴリーシステムを定義する際に使用します。
436
+
437
+ - **`path_filter`**: 対象とするページのパスプレフィックス。
438
+ - 例: `path_filter: "book/"` → `book/` 配下のページのみがこのカテゴリーシステムに含まれる
439
+ - 空文字列の場合は全ページが対象
440
+ - 複数カテゴリーシステムを使用する場合は、各システムで `path_filter` を設定することを推奨
441
+
442
+ - **`url_separator`**: カテゴリー名にスペースが含まれる場合の置き換え文字を指定します。
443
+ - 例: `category: ["Contemporary Art"]` + `url_separator: "-"` → `/contemporary-art/`
444
+ - 例: `category: ["Contemporary Art"]` + `url_separator: "_"` → `/contemporary_art/`
445
+
446
+ - **`url_prefix`**: すべてのカテゴリーURLに共通のプレフィックスを追加します。
447
+ - 例: `url_prefix: "/blog/categories"` → カテゴリーページは `/blog/categories/tech/`、`/blog/categories/art/` など
448
+ - 空文字列の場合はルート直下に生成されます
449
+
450
+ **自動生成されるページのメタデータ:**
451
+
452
+ ```javascript
453
+ {
454
+ name: 'tech/frontend/index',
455
+ url: '/tech/frontend',
456
+ __output: '/tech/frontend/index.html',
457
+ title: 'Frontend', // カテゴリー名
458
+ template: 'category.html',
459
+ category_path: ['Tech', 'Frontend'], // カテゴリーパス
460
+ category_pages: ['tech/frontend/react/tutorial', ...], // このカテゴリーのページ
461
+ category_children: ['/tech/frontend/react', ...], // サブカテゴリー
462
+ __is_auto_category: true,
463
+ distribute: true
464
+ }
465
+ ```
466
+
467
+ **テンプレート内で利用可能な変数:**
468
+
469
+ - `{{title}}` - カテゴリー名
470
+ - `{{category_path}}` - カテゴリーパス配列
471
+ - `{{category_pages}}` - このカテゴリーに属するページ名の配列
472
+ - `{{category_children}}` - サブカテゴリーのURL配列
473
+
474
+ **複数カテゴリーシステムの使用:**
475
+
476
+ `categories` 配列を使用することで、複数の独立したカテゴリーシステムを定義できます。
477
+
478
+ ```json
479
+ {
480
+ "packages": "category",
481
+ "hooks": {
482
+ "afterIndexing": "categoryIndexer.js"
483
+ },
484
+ "categories": [
485
+ {
486
+ "name": "books",
487
+ "url_prefix": "/book-list",
488
+ "path_filter": "book/",
489
+ "template": "category.html",
490
+ "auto_generate": true,
491
+ "max_depth": 3,
492
+ "url_case": "lower",
493
+ "url_separator": "-"
494
+ },
495
+ {
496
+ "name": "articles",
497
+ "url_prefix": "/article-list",
498
+ "path_filter": "article/",
499
+ "template": "article-category.html",
500
+ "auto_generate": true,
501
+ "max_depth": 2
502
+ }
503
+ ]
504
+ }
505
+ ```
506
+
507
+ **動作例:**
508
+ - `book/after-rain.md` (category: ["Art", "Painting"]) → `/book-list/art/painting/index.html`
509
+ - `article/news/200910.md` (category: ["News"]) → `/article-list/news/index.html`
510
+ - `/book-list/news/index.html` は生成**されない**(意図しないページ生成を防止)
511
+ - `/article-list/art/index.html` は生成**されない**(各システムは独立)
512
+
513
+ **後方互換性:**
514
+
515
+ 既存の `category`(単数形)設定も引き続き動作します。`categories` 配列と `category` の両方が定義されている場合、`categories` が優先されます。
516
+
517
+ ```json
518
+ {
519
+ "category": {
520
+ "template": "category.html",
521
+ "auto_generate": true
522
+ }
523
+ }
524
+ ```
525
+
526
+ **手動ページによる上書き:**
527
+
528
+ `src/pages/tech/index.md` が存在する場合、自動生成はスキップされます。
529
+
530
+ **ヘルパー関数:**
531
+
532
+ ```javascript
533
+ // カテゴリーツリーを取得
534
+ const tree = helper.getCategoryTree()
535
+
536
+ // 特定カテゴリーのページを取得(配列で指定)
537
+ const pages = helper.getCategoryPages(['Tech', 'Frontend'])
538
+
539
+ // サブカテゴリーを含む全ページを取得(配列で指定)
540
+ const allPages = helper.getCategoryPagesRecursive(['Tech'])
541
+ ```
542
+
543
+ ### 有効化方法
544
+
545
+ ```json
546
+ {
547
+ "packages": "breadcrumbs,turbolink"
548
+ }
549
+ ```
550
+
551
+ ### パッケージの構成
552
+
553
+ ```
554
+ packages/breadcrumbs/
555
+ ├── breadcrumbs.js # ヘルパー関数(自動読み込み)
556
+ ├── template/ # テンプレートファイル
557
+ └── css/ # スタイルシート
558
+ ```
559
+
560
+ パッケージを有効化すると:
561
+ 1. パッケージディレクトリが `.cache/` にコピーされる
562
+ 2. `{パッケージ名}.js` が自動的に `helper` に追加される
563
+
564
+ ## Hook機構
565
+
566
+ ビルドプロセスの特定のタイミングで、カスタムロジックを実行できるフック機構を提供しています。
567
+
568
+ ### Hook設定
569
+
570
+ `blog.json` で設定:
571
+
572
+ ```json
573
+ {
574
+ "hooks": {
575
+ "afterIndexing": "categoryIndexer.js"
576
+ }
577
+ }
578
+ ```
579
+
580
+ - **複数フック対応**: 配列で複数ファイル指定可能
581
+ ```json
582
+ {
583
+ "hooks": {
584
+ "afterIndexing": ["hook1.js", "hook2.js"]
585
+ }
586
+ }
587
+ ```
588
+
589
+ ### 利用可能なフックポイント
590
+
591
+ #### `afterIndexing`
592
+
593
+ 全ページのインデックス化完了後、レンダリング開始前に実行されます。
594
+
595
+ **実行タイミング:**
596
+ ```
597
+ indexing() → afterIndexing Hook → distribute()
598
+ ```
599
+
600
+ **関数シグネチャ:**
601
+ ```javascript
602
+ export async function afterIndexing(allData, config) {
603
+ // allData: 全ページデータ(参照渡し、変更可能)
604
+ // config: blog.json の設定内容
605
+ }
606
+ ```
607
+
608
+ **使用例:**
609
+ ```javascript
610
+ // src/helper/categoryIndexer.js
611
+ export async function afterIndexing(allData, config) {
612
+ // カテゴリーページを自動生成
613
+ allData['tech/index'] = {
614
+ name: 'tech/index',
615
+ title: 'Tech',
616
+ template: 'category.html',
617
+ distribute: true,
618
+ // ... その他のメタデータ
619
+ }
620
+ }
621
+ ```
622
+
623
+ ### Hook関数の配置
624
+
625
+ - **配置場所**: `src/helper/` ディレクトリ
626
+ - **形式**: ES Module(`export` 必須)
627
+ - **実行順序**: 配列で指定した順に実行
628
+
629
+ ### 注意事項
630
+
631
+ - フック関数は非同期(`async`)に対応
632
+ - `allData` は参照渡しのため、直接変更可能
633
+ - エラー発生時はビルドが中断される
634
+ - フックファイルが存在しない場合はスキップ(エラーにならない)
635
+
636
+ ## ビルドプロセス
637
+
638
+ ### 開発サーバー (`npx server`)
639
+
640
+ 1. `.cache/` にソースをコピー(パッケージ含む)
641
+ 2. テンプレートを事前読み込み(warmUp)
642
+ 3. 全ページをインデックス化
643
+ 4. 静的サイト生成
644
+ 5. HTTPサーバー起動(ポート8000)
645
+ 6. ファイル監視を開始
646
+
647
+ ファイル変更時:
648
+ - 該当ページのみ再生成(高速)
649
+ - テンプレート変更時は全ページ再生成
650
+
651
+ ### 静的サイト生成 (`npx generate`)
652
+
653
+ 1. `.cache/` にソースをコピー(パッケージ含む)
654
+ 2. テンプレートを事前読み込み(warmUp)
655
+ 3. 全ページをインデックス化(`lib/indexer.js`)
656
+ 4. 各ページをレンダリング(`lib/render.js`)
657
+ - テンプレート読み込み
658
+ - フィルター処理(include, if, script)
659
+ - Markdown → HTML変換
660
+ - 変数展開
661
+ - HTML minify
662
+ 5. `dist/` に出力
663
+ 6. `distribute_raw` で指定したディレクトリをコピー
664
+ 7. 削除されたページのファイルをクリーンアップ
665
+
666
+ ## レンダリングパイプライン
667
+
668
+ 各ページは以下の順序で処理されます:
669
+
670
+ ### 1. ページデータ解析 (`lib/pageData.js`)
671
+
672
+ - フロントマターを抽出(`---...---` または `<!--...-->`)
673
+ - メタデータをパース(JSON対応)
674
+ - デフォルト値とマージ
675
+
676
+ ### 2. テンプレート適用 (`lib/applyTemplate.js`)
677
+
678
+ - テンプレートファイル読み込み
679
+ - `include()` ディレクティブ解決
680
+ - CSSジェネレーター処理
681
+
682
+ ### 3. フィルター処理(テンプレート) (`lib/filter.js`)
683
+
684
+ - `{if}` 条件分岐処理
685
+ - `{script}` / `<script type="ssg">` 実行
686
+
687
+ ### 4. フィルター処理(Markdown) (`lib/render.js`)
688
+
689
+ - `include()` 解決
690
+ - `{if}` 処理
691
+ - `{script}` 実行
692
+ - 変数展開(`{{変数名}}`)
693
+ - Markdown → HTML変換(`.md` ファイルのみ)
694
+
695
+ ### 5. 変数展開(テンプレート) (`lib/replaceVariablesFilter.js`)
696
+
697
+ - `{{変数名}}` を実際の値に置換
698
+ - ヘルパー関数実行(`{{関数名(引数)}}`)
699
+
700
+ ### 6. 出力 (`lib/distribute.js`)
701
+
702
+ - HTML minify
703
+ - `dist/` に書き込み
704
+
705
+ ## ファイル操作とキャッシュ
706
+
707
+ ### `.cache/` ディレクトリの役割
708
+
709
+ #### 概要
710
+
711
+ `.cache/` はパッケージとユーザーコードを統合した「実効的なソースディレクトリ」として機能します。ビルドシステムはこのディレクトリを実際のソースとして扱います。
712
+
713
+ #### ファイル展開の仕組み
714
+
715
+ `lib/dir.js` の `cache()` 関数が以下の順序で実行します:
716
+
717
+ 1. **コアパッケージの展開**: `packages/*/` → `.cache/`(名前空間フラット化)
718
+ 2. **ユーザーパッケージの展開**: `src/packages/*/` → `.cache/`
719
+ 3. **ユーザーコードのコピー**: `src/` → `.cache/`(上書き)
720
+
721
+ **展開例**:
722
+ ```
723
+ packages/breadcrumbs/helper/breadcrumbs.js → .cache/helper/breadcrumbs.js
724
+ packages/breadcrumbs/css/breadcrumbs.css → .cache/css/breadcrumbs.css
725
+ packages/category/template/category.html → .cache/template/category.html
726
+ src/helper/custom.js → .cache/helper/custom.js(追加)
727
+ src/template/category.html → .cache/template/category.html(上書き)
728
+ ```
729
+
730
+ #### ファイル優先順位
731
+
732
+ 同名ファイルが存在する場合の優先順位:
733
+
734
+ 1. **`src/`** (最優先 - ユーザーカスタム)
735
+ 2. **`src/packages/`** (ユーザー定義パッケージ)
736
+ 3. **`packages/`** (コアパッケージ)
737
+
738
+ この仕組みにより、ユーザーはパッケージが提供する任意のファイルを上書きしてカスタマイズできます。
739
+
740
+ #### Helper の自動登録
741
+
742
+ `blog.json` の `packages` で指定されたパッケージは:
743
+ - `.cache/helper/<package>.js` が自動的に `config.helper` に追加される(`lib/dir.js:38-40, 45-47`)
744
+ - ユーザーが明示的に設定する必要はない
745
+
746
+ #### ディレクトリ構造
747
+
748
+ ビルド時に以下がコピーされます:
749
+
750
+ ```
751
+ .cache/
752
+ ├── pages/ # src/pages/ のコピー
753
+ ├── template/ # src/template/ + パッケージのテンプレート
754
+ ├── css/ # src/css/ + パッケージのCSS
755
+ ├── helper/ # src/helper/ + パッケージのヘルパー
756
+ ├── image/ # src/image/ のコピー
757
+ └── index.json # ページインデックス(差分検出用)
758
+ ```
759
+
760
+ ### テンプレートキャッシュ
761
+
762
+ `lib/applyTemplate.js` と `lib/files.js` により:
763
+ - 初回ビルド時に全テンプレート・CSSをメモリにロード
764
+ - 2回目以降はキャッシュから取得
765
+
766
+ ### CSSキャッシュバスト
767
+
768
+ `lib/cssGenerator.js` により:
769
+ - CSS内容のMD5ハッシュを生成
770
+ - クエリパラメータとして付与(`?t={hash}`)
771
+
772
+ ## URL生成ルール
773
+
774
+ ### 基本ルール
775
+
776
+ | ファイルパス | デフォルトURL |
777
+ |-------------|--------------|
778
+ | `pages/sample.md` | `/sample` |
779
+ | `pages/post/1.md` | `/post/1` |
780
+ | `pages/index.md` | `/` |
781
+
782
+ ### カスタムURL
783
+
784
+ フロントマターで `url` を指定:
785
+
786
+ ```yaml
787
+ ---
788
+ url: /custom-path
789
+ ---
790
+ ```
791
+
792
+ ### 拡張子の扱い
793
+
794
+ - `ext: "html"` (デフォルト): `/path` → `/path.html`
795
+ - `ext: "txt"`: `/path` → `/path.txt`
796
+ - `index.md`: `/index.html`
797
+
798
+ ### プレビューモード
799
+
800
+ ```yaml
801
+ ---
802
+ preview: true
803
+ url: /article
804
+ ---
805
+ ```
806
+
807
+ → `/preview/article` として出力され、`index: false`, `noindex: true` が自動設定されます。
808
+
809
+ ## 開発サーバーの挙動
810
+
811
+ ### ルーティング
812
+
813
+ 1. `/path/` → `/path/index.html`
814
+ 2. `/path` (拡張子なし) → `/path.html`
815
+ 3. `/path.txt` → そのまま
816
+ 4. 404の場合 → `/404.html` を返す
817
+ 5. エラーの場合 → `/500.html` を返す
818
+
819
+ ### MIMEタイプ判定
820
+
821
+ `lib/contentType.js` により拡張子からMIMEタイプを判定:
822
+
823
+ - `.html` → `text/html`
824
+ - `.css` → `text/css`
825
+ - `.js` → `text/javascript`
826
+ - `.jpg` → `image/jpeg`
827
+ - など
828
+
829
+ ## エクスポートAPI
830
+
831
+ `index.js` から以下をエクスポート:
832
+
833
+ ```javascript
834
+ import { allData, config, dir } from '@tenjuu99/blog'
835
+
836
+ // allData: 全ページデータ
837
+ // config: 設定オブジェクト
838
+ // dir: ディレクトリパス定義
839
+ ```
840
+
841
+ これによりヘルパー関数から直接アクセスできます。
842
+
843
+ ## 制約事項
844
+
845
+ - フロントマターは YAML 完全互換ではなく、独自パーサーを使用
846
+ - 配列・オブジェクトは JSON 形式で記述必須
847
+ - 変数名は強制的に小文字化される
848
+ - `include()` は同期処理のため、非同期ファイル読み込みは不可
849
+ - SSGスクリプト内で `import()` は使用不可(ヘルパー関数を使用)