@c-time/frelio-cli 0.1.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.
@@ -0,0 +1,1353 @@
1
+ /**
2
+ * 初期コンテンツ・テンプレート・レシピの生成
3
+ * frelio-demo を踏襲したデモサイト構造
4
+ */
5
+ import { writeFile } from './templates.js';
6
+ import path from 'node:path';
7
+ export function generateInitialContent(projectDir) {
8
+ const base = (...p) => path.join(projectDir, ...p);
9
+ // --- Content Type ---
10
+ writeFile(base('frelio-data/site/content_types/article.json'), json({
11
+ id: 'article',
12
+ fields: [
13
+ { key: 'slug', type: 'text', multiple: false, useAsUrl: true },
14
+ { key: 'title', type: 'text', multiple: false },
15
+ { key: 'excerpt', type: 'text', multiple: false },
16
+ { key: 'body', type: 'html', multiple: false },
17
+ { key: 'eyecatch', type: 'image', multiple: false },
18
+ { key: 'publishDate', type: 'date', multiple: false },
19
+ ],
20
+ }));
21
+ // --- Content Type UI ---
22
+ writeFile(base('frelio-data/admin/content_types/article.ui.json'), json({
23
+ id: 'article',
24
+ label: '記事',
25
+ fields: [
26
+ {
27
+ key: 'slug', label: 'スラッグ', required: true,
28
+ defaultValue: '', placeholder: 'my-article-slug',
29
+ description: 'URLに使用される識別子。英数字とハイフンのみ',
30
+ warnLength: null, columns: 12, break: false, hidden: false, readOnly: false,
31
+ pattern: '^[a-z0-9-]+$', patternMessage: '小文字英数字とハイフンのみ使用できます',
32
+ autoFormat: 'slug',
33
+ },
34
+ {
35
+ key: 'title', label: 'タイトル', required: true,
36
+ defaultValue: '', placeholder: '記事のタイトルを入力',
37
+ description: '検索結果・OGPに表示されます。推奨: 60文字以内',
38
+ warnLength: 60, columns: 12, break: false, hidden: false, readOnly: false,
39
+ pattern: null, patternMessage: null, autoFormat: null,
40
+ },
41
+ {
42
+ key: 'excerpt', label: '概要', required: false,
43
+ defaultValue: '', placeholder: '記事の概要を入力',
44
+ description: '一覧ページや検索結果に表示される短い説明文',
45
+ warnLength: 200, columns: 12, break: false, hidden: false, readOnly: false,
46
+ pattern: null, patternMessage: null, autoFormat: null,
47
+ },
48
+ {
49
+ key: 'body', label: '本文', required: true,
50
+ defaultValue: '', placeholder: 'HTML形式で本文を入力',
51
+ description: null, warnLength: null, columns: 12, break: false, hidden: false, readOnly: false,
52
+ pattern: null, patternMessage: null, autoFormat: null,
53
+ },
54
+ {
55
+ key: 'eyecatch', label: 'アイキャッチ画像', required: false,
56
+ defaultValue: null, placeholder: null,
57
+ description: '記事のサムネイル画像',
58
+ warnLength: null, columns: 6, break: true, hidden: false, readOnly: false,
59
+ aspectRatio: '16:9', pattern: null, patternMessage: null, autoFormat: null,
60
+ },
61
+ {
62
+ key: 'publishDate', label: '公開日', required: false,
63
+ defaultValue: '', placeholder: null,
64
+ description: '記事の公開日',
65
+ warnLength: null, columns: 4, break: false, hidden: false, readOnly: false,
66
+ pattern: null, patternMessage: null, autoFormat: null,
67
+ },
68
+ ],
69
+ groups: [],
70
+ }));
71
+ // --- Content Type Views ---
72
+ writeFile(base('frelio-data/admin/content_types/article.views.json'), json({
73
+ defaultViewId: 'default',
74
+ views: [
75
+ {
76
+ id: 'default',
77
+ label: 'デフォルト',
78
+ columns: [
79
+ { field: 'title', label: 'タイトル' },
80
+ { field: 'status', label: '状態', width: '100px' },
81
+ { field: 'publishDate', label: '公開日', width: '150px' },
82
+ { field: 'updatedAt', label: '更新日', width: '150px' },
83
+ ],
84
+ defaultSort: { field: 'updatedAt', direction: 'desc' },
85
+ },
86
+ ],
87
+ }));
88
+ // --- Content Types Metadata (override the empty one) ---
89
+ writeFile(base('frelio-data/admin/metadata/content_types.json'), json({
90
+ contentTypes: [
91
+ { id: 'article', label: '記事', icon: 'Article' },
92
+ ],
93
+ }));
94
+ // --- Dashboard Metadata ---
95
+ writeFile(base('frelio-data/admin/metadata/_dashboard.json'), json({
96
+ contentTypes: {
97
+ article: {
98
+ lastUpdatedAt: null,
99
+ lastUpdatedBy: null,
100
+ totalCount: 0,
101
+ draftCount: 0,
102
+ publishedCount: 0,
103
+ },
104
+ },
105
+ }));
106
+ // --- Build Recipe ---
107
+ writeFile(base('frelio-data/admin/recipes/build-data-recipe.json'), json({
108
+ version: '1.0',
109
+ contentTypes: {
110
+ article: {
111
+ details: [
112
+ {
113
+ type: 'detail',
114
+ outputPath: 'news/{$this.slug}/index.json',
115
+ templatePath: 'news/_detail/index.html',
116
+ },
117
+ ],
118
+ lists: [
119
+ {
120
+ type: 'list',
121
+ outputPath: 'news/index.json',
122
+ templatePath: 'news/index.html',
123
+ pagination: { perPage: 10, numberFormatCount: 1 },
124
+ },
125
+ ],
126
+ },
127
+ },
128
+ staticPages: [
129
+ {
130
+ type: 'static',
131
+ outputPath: 'index.json',
132
+ templatePath: 'index.html',
133
+ customFields: [
134
+ {
135
+ field: 'latestNews',
136
+ type: 'relationList',
137
+ contentTypeId: 'article',
138
+ limit: 5,
139
+ sort: { field: 'publishDate', direction: 'desc' },
140
+ listFields: ['slug', 'title', 'excerpt', 'eyecatch', 'publishDate'],
141
+ },
142
+ ],
143
+ },
144
+ { type: 'static', outputPath: 'about/index.json', templatePath: 'about/index.html' },
145
+ { type: 'static', outputPath: 'contact/index.json', templatePath: 'contact/index.html' },
146
+ ],
147
+ templateIncludes: {
148
+ '_parts/*.htm': ['*'],
149
+ },
150
+ }));
151
+ // --- HTML Templates ---
152
+ generateTemplates(projectDir);
153
+ // --- SCSS ---
154
+ generateScss(projectDir);
155
+ // --- TypeScript ---
156
+ generateTypeScript(projectDir);
157
+ // --- CLAUDE.md ---
158
+ generateClaudeMd(projectDir);
159
+ }
160
+ // ========== HTML Templates ==========
161
+ function generateTemplates(projectDir) {
162
+ const t = (...p) => path.join(projectDir, 'frelio-data/site/templates', ...p);
163
+ // _parts/head.htm
164
+ writeFile(t('_parts/head.htm'), `<meta charset="UTF-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
+ <title data-gen-text="meta.title">Frelio Demo</title>
167
+ <link rel="stylesheet" href="/assets/style.css">
168
+ <script type="module" src="/assets/index.js"></script>
169
+ `);
170
+ // _parts/header.htm
171
+ writeFile(t('_parts/header.htm'), `<header class="l-header">
172
+ <div class="l-header__inner">
173
+ <a href="/" class="c-logo">Frelio Demo</a>
174
+ <nav class="c-nav">
175
+ <button type="button" class="c-nav__toggle" aria-label="メニュー" aria-expanded="false">
176
+ <span class="c-nav__toggle__bar"></span>
177
+ <span class="c-nav__toggle__bar"></span>
178
+ <span class="c-nav__toggle__bar"></span>
179
+ </button>
180
+ <ul class="c-nav__list">
181
+ <li class="c-nav__item"><a href="/">ホーム</a></li>
182
+ <li class="c-nav__item"><a href="/news/">お知らせ</a></li>
183
+ <li class="c-nav__item"><a href="/about/">会社概要</a></li>
184
+ <li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
185
+ </ul>
186
+ </nav>
187
+ </div>
188
+ </header>
189
+ `);
190
+ // _parts/footer.htm
191
+ writeFile(t('_parts/footer.htm'), `<footer class="l-footer">
192
+ <div class="l-footer__inner">
193
+ <p class="l-footer__copyright">&copy; 2026 Frelio Demo. All rights reserved.</p>
194
+ </div>
195
+ </footer>
196
+ `);
197
+ // index.html
198
+ writeFile(t('index.html'), `<!DOCTYPE html>
199
+ <html lang="ja">
200
+ <head>
201
+ <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
202
+ <meta charset="UTF-8">
203
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
204
+ <title>Frelio Demo</title>
205
+ <link rel="stylesheet" href="/assets/style.css">
206
+ <script type="module" src="/assets/index.js"></script>
207
+ </head>
208
+ <body>
209
+ <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
210
+ <header class="l-header">
211
+ <div class="l-header__inner">
212
+ <a href="/" class="c-logo">Frelio Demo</a>
213
+ <nav class="c-nav">
214
+ <ul class="c-nav__list">
215
+ <li class="c-nav__item"><a href="/">ホーム</a></li>
216
+ <li class="c-nav__item"><a href="/news/">お知らせ</a></li>
217
+ <li class="c-nav__item"><a href="/about/">会社概要</a></li>
218
+ <li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
219
+ </ul>
220
+ </nav>
221
+ </div>
222
+ </header>
223
+
224
+ <main class="l-main">
225
+ <section class="p-hero">
226
+ <div class="p-hero__inner">
227
+ <h1 class="p-hero__title">Frelio Demo</h1>
228
+ <p class="p-hero__lead">GitHub + Cloudflare で動く、シンプルなヘッドレス CMS のデモサイトです。</p>
229
+ </div>
230
+ </section>
231
+
232
+ <section class="p-news-list">
233
+ <div class="p-news-list__inner">
234
+ <h2 class="e-heading">お知らせ</h2>
235
+ <ul class="p-news-list__items">
236
+ <template data-gen-scope="" data-gen-repeat="latestNews" data-gen-repeat-name="article">
237
+ <li class="p-news-list__item">
238
+ <a class="c-card--news" data-gen-attrs="href:article.href">
239
+ <div class="c-card__body">
240
+ <time class="c-card__date" data-gen-text="article.publishDate">2026-03-01</time>
241
+ <h3 class="c-card__title" data-gen-text="article.title">お知らせタイトル</h3>
242
+ <p class="c-card__excerpt" data-gen-text="article.excerpt">お知らせの概要がここに入ります。</p>
243
+ </div>
244
+ </a>
245
+ </li>
246
+ </template>
247
+ <li class="p-news-list__item" data-gen-comment="">
248
+ <a class="c-card--news" href="/news/sample/">
249
+ <div class="c-card__body">
250
+ <time class="c-card__date">2026-03-01</time>
251
+ <h3 class="c-card__title">サンプルお知らせ</h3>
252
+ <p class="c-card__excerpt">これはプレビュー用のダミーデータです。</p>
253
+ </div>
254
+ </a>
255
+ </li>
256
+ </ul>
257
+ <div class="p-news-list__more">
258
+ <a href="/news/" class="c-btn">お知らせ一覧へ</a>
259
+ </div>
260
+ </div>
261
+ </section>
262
+ </main>
263
+
264
+ <template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
265
+ <footer class="l-footer">
266
+ <div class="l-footer__inner">
267
+ <p class="l-footer__copyright">&copy; 2026 Frelio Demo. All rights reserved.</p>
268
+ </div>
269
+ </footer>
270
+ </body>
271
+ </html>
272
+ `);
273
+ // about.html
274
+ writeFile(t('about.html'), `<!DOCTYPE html>
275
+ <html lang="ja">
276
+ <head>
277
+ <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
278
+ <meta charset="UTF-8">
279
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
280
+ <title>会社概要 | Frelio Demo</title>
281
+ <link rel="stylesheet" href="/assets/style.css">
282
+ <script type="module" src="/assets/index.js"></script>
283
+ </head>
284
+ <body>
285
+ <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
286
+ <header class="l-header">
287
+ <div class="l-header__inner">
288
+ <a href="/" class="c-logo">Frelio Demo</a>
289
+ <nav class="c-nav">
290
+ <ul class="c-nav__list">
291
+ <li class="c-nav__item"><a href="/">ホーム</a></li>
292
+ <li class="c-nav__item"><a href="/news/">お知らせ</a></li>
293
+ <li class="c-nav__item"><a href="/about/">会社概要</a></li>
294
+ <li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
295
+ </ul>
296
+ </nav>
297
+ </div>
298
+ </header>
299
+
300
+ <main class="l-main">
301
+ <section class="p-about">
302
+ <div class="p-about__inner">
303
+ <h1 class="e-heading">会社概要</h1>
304
+ <div class="p-about__content">
305
+ <p>Frelio Demo は、ヘッドレス CMS「Frelio」の機能を紹介するためのデモサイトです。</p>
306
+ <p>Frelio は GitHub をデータストア、Cloudflare を配信基盤として活用し、サーバーレスで運用できる次世代の CMS です。</p>
307
+ </div>
308
+ <div class="p-about__table">
309
+ <table class="c-table">
310
+ <tbody class="c-table__body">
311
+ <tr class="c-table__row">
312
+ <th class="c-table__header">会社名</th>
313
+ <td class="c-table__cell">株式会社フレリオ(デモ)</td>
314
+ </tr>
315
+ <tr class="c-table__row">
316
+ <th class="c-table__header">所在地</th>
317
+ <td class="c-table__cell">東京都渋谷区</td>
318
+ </tr>
319
+ <tr class="c-table__row">
320
+ <th class="c-table__header">設立</th>
321
+ <td class="c-table__cell">2026年</td>
322
+ </tr>
323
+ <tr class="c-table__row">
324
+ <th class="c-table__header">事業内容</th>
325
+ <td class="c-table__cell">ヘッドレス CMS の開発・提供</td>
326
+ </tr>
327
+ </tbody>
328
+ </table>
329
+ </div>
330
+ </div>
331
+ </section>
332
+ </main>
333
+
334
+ <template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
335
+ <footer class="l-footer">
336
+ <div class="l-footer__inner">
337
+ <p class="l-footer__copyright">&copy; 2026 Frelio Demo. All rights reserved.</p>
338
+ </div>
339
+ </footer>
340
+ </body>
341
+ </html>
342
+ `);
343
+ // contact.html
344
+ writeFile(t('contact.html'), `<!DOCTYPE html>
345
+ <html lang="ja">
346
+ <head>
347
+ <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
348
+ <meta charset="UTF-8">
349
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
350
+ <title>お問い合わせ | Frelio Demo</title>
351
+ <link rel="stylesheet" href="/assets/style.css">
352
+ <script type="module" src="/assets/index.js"></script>
353
+ </head>
354
+ <body>
355
+ <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
356
+ <header class="l-header">
357
+ <div class="l-header__inner">
358
+ <a href="/" class="c-logo">Frelio Demo</a>
359
+ <nav class="c-nav">
360
+ <ul class="c-nav__list">
361
+ <li class="c-nav__item"><a href="/">ホーム</a></li>
362
+ <li class="c-nav__item"><a href="/news/">お知らせ</a></li>
363
+ <li class="c-nav__item"><a href="/about/">会社概要</a></li>
364
+ <li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
365
+ </ul>
366
+ </nav>
367
+ </div>
368
+ </header>
369
+
370
+ <main class="l-main">
371
+ <section class="p-contact">
372
+ <div class="p-contact__inner">
373
+ <h1 class="e-heading">お問い合わせ</h1>
374
+ <p class="p-contact__lead">お気軽にお問い合わせください。</p>
375
+ <form class="p-contact__form" action="#" method="post">
376
+ <div class="p-contact__field">
377
+ <label class="p-contact__label" for="contact-name">お名前</label>
378
+ <input class="p-contact__input" type="text" id="contact-name" name="name" required>
379
+ </div>
380
+ <div class="p-contact__field">
381
+ <label class="p-contact__label" for="contact-email">メールアドレス</label>
382
+ <input class="p-contact__input" type="email" id="contact-email" name="email" required>
383
+ </div>
384
+ <div class="p-contact__field">
385
+ <label class="p-contact__label" for="contact-message">メッセージ</label>
386
+ <textarea class="p-contact__textarea" id="contact-message" name="message" rows="6" required></textarea>
387
+ </div>
388
+ <div class="p-contact__actions">
389
+ <button type="submit" class="c-btn--submit">送信する</button>
390
+ </div>
391
+ </form>
392
+ </div>
393
+ </section>
394
+ </main>
395
+
396
+ <template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
397
+ <footer class="l-footer">
398
+ <div class="l-footer__inner">
399
+ <p class="l-footer__copyright">&copy; 2026 Frelio Demo. All rights reserved.</p>
400
+ </div>
401
+ </footer>
402
+ </body>
403
+ </html>
404
+ `);
405
+ // news/index.html
406
+ writeFile(t('news/index.html'), `<!DOCTYPE html>
407
+ <html lang="ja">
408
+ <head>
409
+ <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
410
+ <meta charset="UTF-8">
411
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
412
+ <title>お知らせ | Frelio Demo</title>
413
+ <link rel="stylesheet" href="/assets/style.css">
414
+ <script type="module" src="/assets/index.js"></script>
415
+ </head>
416
+ <body>
417
+ <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
418
+ <header class="l-header">
419
+ <div class="l-header__inner">
420
+ <a href="/" class="c-logo">Frelio Demo</a>
421
+ <nav class="c-nav">
422
+ <ul class="c-nav__list">
423
+ <li class="c-nav__item"><a href="/">ホーム</a></li>
424
+ <li class="c-nav__item"><a href="/news/">お知らせ</a></li>
425
+ <li class="c-nav__item"><a href="/about/">会社概要</a></li>
426
+ <li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
427
+ </ul>
428
+ </nav>
429
+ </div>
430
+ </header>
431
+
432
+ <main class="l-main">
433
+ <section class="p-news-list">
434
+ <div class="p-news-list__inner">
435
+ <h1 class="e-heading">お知らせ</h1>
436
+ <ul class="p-news-list__items">
437
+ <template data-gen-scope="" data-gen-repeat="pagedItems" data-gen-repeat-name="article">
438
+ <li class="p-news-list__item">
439
+ <a class="c-card--news" data-gen-attrs="href:article.href">
440
+ <div class="c-card__body">
441
+ <time class="c-card__date" data-gen-text="article.publishDate">2026-03-01</time>
442
+ <h3 class="c-card__title" data-gen-text="article.title">お知らせタイトル</h3>
443
+ <p class="c-card__excerpt" data-gen-text="article.excerpt">お知らせの概要がここに入ります。</p>
444
+ </div>
445
+ </a>
446
+ </li>
447
+ </template>
448
+ <li class="p-news-list__item" data-gen-comment="">
449
+ <a class="c-card--news" href="/news/sample/">
450
+ <div class="c-card__body">
451
+ <time class="c-card__date">2026-03-01</time>
452
+ <h3 class="c-card__title">サンプルお知らせ</h3>
453
+ <p class="c-card__excerpt">これはプレビュー用のダミーデータです。</p>
454
+ </div>
455
+ </a>
456
+ </li>
457
+ </ul>
458
+ </div>
459
+ </section>
460
+ </main>
461
+
462
+ <template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
463
+ <footer class="l-footer">
464
+ <div class="l-footer__inner">
465
+ <p class="l-footer__copyright">&copy; 2026 Frelio Demo. All rights reserved.</p>
466
+ </div>
467
+ </footer>
468
+ </body>
469
+ </html>
470
+ `);
471
+ // news/_detail/index.html
472
+ writeFile(t('news/_detail/index.html'), `<!DOCTYPE html>
473
+ <html lang="ja">
474
+ <head>
475
+ <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
476
+ <meta charset="UTF-8">
477
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
478
+ <title>記事タイトル | Frelio Demo</title>
479
+ <link rel="stylesheet" href="/assets/style.css">
480
+ <script type="module" src="/assets/index.js"></script>
481
+ </head>
482
+ <body>
483
+ <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
484
+ <header class="l-header">
485
+ <div class="l-header__inner">
486
+ <a href="/" class="c-logo">Frelio Demo</a>
487
+ <nav class="c-nav">
488
+ <ul class="c-nav__list">
489
+ <li class="c-nav__item"><a href="/">ホーム</a></li>
490
+ <li class="c-nav__item"><a href="/news/">お知らせ</a></li>
491
+ <li class="c-nav__item"><a href="/about/">会社概要</a></li>
492
+ <li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
493
+ </ul>
494
+ </nav>
495
+ </div>
496
+ </header>
497
+
498
+ <main class="l-main">
499
+ <article class="p-article">
500
+ <div class="p-article__inner">
501
+ <header class="p-article__header">
502
+ <time class="p-article__date" data-gen-text="publishDate">2026-03-01</time>
503
+ <h1 class="p-article__title" data-gen-text="title">記事タイトル</h1>
504
+ </header>
505
+ <template data-gen-scope="" data-gen-if="eyecatch">
506
+ <div class="p-article__eyecatch">
507
+ <img class="p-article__eyecatch__image" data-gen-attrs="src:eyecatch.url,alt:title" src="/images/placeholder.jpg" alt="記事タイトル">
508
+ </div>
509
+ </template>
510
+ <div class="p-article__eyecatch" data-gen-comment="">
511
+ <img class="p-article__eyecatch__image" src="/images/placeholder.jpg" alt="プレビュー画像">
512
+ </div>
513
+ <div class="p-article__body" data-gen-html="body">
514
+ <p>これはプレビュー用のダミー本文です。実際の記事コンテンツがここに表示されます。</p>
515
+ <h2>見出し</h2>
516
+ <p>記事の本文が続きます。HTMLリッチテキストで記述されます。</p>
517
+ </div>
518
+ <div class="p-article__footer">
519
+ <a href="/news/" class="c-btn">お知らせ一覧に戻る</a>
520
+ </div>
521
+ </div>
522
+ </article>
523
+ </main>
524
+
525
+ <template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
526
+ <footer class="l-footer">
527
+ <div class="l-footer__inner">
528
+ <p class="l-footer__copyright">&copy; 2026 Frelio Demo. All rights reserved.</p>
529
+ </div>
530
+ </footer>
531
+ </body>
532
+ </html>
533
+ `);
534
+ }
535
+ // ========== SCSS ==========
536
+ function generateScss(projectDir) {
537
+ const s = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/scss', ...p);
538
+ // style.scss (entry point)
539
+ writeFile(s('style.scss'), `// Foundation
540
+ @use 'foundation/variables' as *;
541
+ @use 'foundation/mixins' as *;
542
+ @use 'foundation/reset';
543
+
544
+ // Layout
545
+ @use 'layout/l-header';
546
+ @use 'layout/l-footer';
547
+ @use 'layout/l-main';
548
+
549
+ // Component
550
+ @use 'component/c-logo';
551
+ @use 'component/c-nav';
552
+ @use 'component/c-btn';
553
+ @use 'component/c-card';
554
+ @use 'component/c-table';
555
+
556
+ // Project
557
+ @use 'project/p-hero';
558
+ @use 'project/p-news-list';
559
+ @use 'project/p-article';
560
+ @use 'project/p-about';
561
+ @use 'project/p-contact';
562
+
563
+ // Element
564
+ @use 'element/e-heading';
565
+ `);
566
+ // Foundation
567
+ writeFile(s('foundation/_variables.scss'), `// Colors
568
+ $color-primary: #0070f3;
569
+ $color-primary-dark: #0060d0;
570
+ $color-text: #333;
571
+ $color-text-light: #666;
572
+ $color-border: #e0e0e0;
573
+ $color-bg: #fff;
574
+ $color-bg-light: #f8f9fa;
575
+
576
+ // Typography
577
+ $font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
578
+ "Hiragino Sans", "Noto Sans JP", sans-serif;
579
+ $line-height-base: 1.7;
580
+
581
+ // Layout
582
+ $max-width: 1100px;
583
+ $spacing-unit: 8px;
584
+
585
+ // Breakpoints
586
+ $bp-tablet: 768px;
587
+ $bp-desktop: 1024px;
588
+ `);
589
+ writeFile(s('foundation/_mixins.scss'), `@use 'variables' as *;
590
+
591
+ @mixin tablet {
592
+ @media (min-width: $bp-tablet) {
593
+ @content;
594
+ }
595
+ }
596
+
597
+ @mixin desktop {
598
+ @media (min-width: $bp-desktop) {
599
+ @content;
600
+ }
601
+ }
602
+ `);
603
+ writeFile(s('foundation/_reset.scss'), `@use 'variables' as *;
604
+
605
+ *,
606
+ *::before,
607
+ *::after {
608
+ box-sizing: border-box;
609
+ margin: 0;
610
+ padding: 0;
611
+ }
612
+
613
+ html {
614
+ font-size: 100%;
615
+ -webkit-text-size-adjust: 100%;
616
+ }
617
+
618
+ body {
619
+ font-family: $font-family-base;
620
+ line-height: $line-height-base;
621
+ color: $color-text;
622
+ background-color: $color-bg;
623
+ -webkit-font-smoothing: antialiased;
624
+ }
625
+
626
+ img {
627
+ max-width: 100%;
628
+ height: auto;
629
+ vertical-align: middle;
630
+ }
631
+
632
+ a {
633
+ color: $color-primary;
634
+ text-decoration: none;
635
+ }
636
+
637
+ ul,
638
+ ol {
639
+ list-style: none;
640
+ }
641
+
642
+ table {
643
+ border-collapse: collapse;
644
+ border-spacing: 0;
645
+ }
646
+
647
+ button,
648
+ input,
649
+ select,
650
+ textarea {
651
+ font: inherit;
652
+ color: inherit;
653
+ }
654
+
655
+ button {
656
+ cursor: pointer;
657
+ background: none;
658
+ border: none;
659
+ }
660
+ `);
661
+ // Layout
662
+ writeFile(s('layout/_l-header.scss'), `@use '../foundation/variables' as *;
663
+ @use '../foundation/mixins' as *;
664
+
665
+ .l-header {
666
+ border-bottom: 1px solid $color-border;
667
+ background-color: $color-bg;
668
+ position: sticky;
669
+ top: 0;
670
+ z-index: 100;
671
+ }
672
+
673
+ .l-header__inner {
674
+ max-width: $max-width;
675
+ margin: 0 auto;
676
+ padding: $spacing-unit * 2;
677
+ display: flex;
678
+ justify-content: space-between;
679
+ align-items: center;
680
+ }
681
+
682
+ @include tablet {
683
+ .l-header__inner {
684
+ padding: $spacing-unit * 2 $spacing-unit * 4;
685
+ }
686
+ }
687
+ `);
688
+ writeFile(s('layout/_l-footer.scss'), `@use '../foundation/variables' as *;
689
+
690
+ .l-footer {
691
+ margin-top: $spacing-unit * 8;
692
+ border-top: 1px solid $color-border;
693
+ background-color: $color-bg-light;
694
+ }
695
+
696
+ .l-footer__inner {
697
+ max-width: $max-width;
698
+ margin: 0 auto;
699
+ padding: $spacing-unit * 4 $spacing-unit * 2;
700
+ text-align: center;
701
+ }
702
+
703
+ .l-footer__copyright {
704
+ font-size: 0.875rem;
705
+ color: $color-text-light;
706
+ }
707
+ `);
708
+ writeFile(s('layout/_l-main.scss'), `.l-main {
709
+ min-height: 60vh;
710
+ }
711
+ `);
712
+ // Component
713
+ writeFile(s('component/_c-logo.scss'), `@use '../foundation/variables' as *;
714
+
715
+ .c-logo {
716
+ font-size: 1.25rem;
717
+ font-weight: bold;
718
+ color: $color-text;
719
+ text-decoration: none;
720
+ }
721
+ `);
722
+ writeFile(s('component/_c-nav.scss'), `@use '../foundation/variables' as *;
723
+ @use '../foundation/mixins' as *;
724
+
725
+ .c-nav {
726
+ display: flex;
727
+ align-items: center;
728
+ }
729
+
730
+ .c-nav__toggle {
731
+ display: flex;
732
+ flex-direction: column;
733
+ gap: 4px;
734
+ width: 28px;
735
+ padding: 4px 0;
736
+ }
737
+
738
+ .c-nav__toggle__bar {
739
+ display: block;
740
+ width: 100%;
741
+ height: 2px;
742
+ background-color: $color-text;
743
+ transition: transform 0.3s, opacity 0.3s;
744
+ }
745
+
746
+ .c-nav__list {
747
+ display: none;
748
+ position: absolute;
749
+ top: 100%;
750
+ left: 0;
751
+ right: 0;
752
+ background-color: $color-bg;
753
+ border-bottom: 1px solid $color-border;
754
+ padding: $spacing-unit * 2;
755
+ flex-direction: column;
756
+ gap: $spacing-unit;
757
+ }
758
+
759
+ .c-nav__list--open {
760
+ display: flex;
761
+ }
762
+
763
+ .c-nav__item a {
764
+ color: $color-text-light;
765
+ font-size: 0.9375rem;
766
+ }
767
+
768
+ .c-nav__item a:hover {
769
+ color: $color-primary;
770
+ }
771
+
772
+ @include tablet {
773
+ .c-nav__toggle {
774
+ display: none;
775
+ }
776
+
777
+ .c-nav__list {
778
+ display: flex;
779
+ position: static;
780
+ flex-direction: row;
781
+ gap: $spacing-unit * 3;
782
+ padding: 0;
783
+ border-bottom: none;
784
+ }
785
+ }
786
+ `);
787
+ writeFile(s('component/_c-btn.scss'), `@use '../foundation/variables' as *;
788
+
789
+ %c-btn {
790
+ display: inline-block;
791
+ padding: $spacing-unit * 1.5 $spacing-unit * 4;
792
+ border: 1px solid $color-primary;
793
+ border-radius: 4px;
794
+ font-size: 0.9375rem;
795
+ text-align: center;
796
+ transition: background-color 0.2s, color 0.2s;
797
+ }
798
+
799
+ .c-btn {
800
+ @extend %c-btn;
801
+ color: $color-primary;
802
+ background-color: transparent;
803
+ }
804
+
805
+ .c-btn:hover {
806
+ background-color: $color-primary;
807
+ color: $color-bg;
808
+ }
809
+
810
+ .c-btn--submit {
811
+ @extend %c-btn;
812
+ color: $color-bg;
813
+ background-color: $color-primary;
814
+ border-color: $color-primary;
815
+ }
816
+
817
+ .c-btn--submit:hover {
818
+ background-color: $color-primary-dark;
819
+ border-color: $color-primary-dark;
820
+ }
821
+ `);
822
+ writeFile(s('component/_c-card.scss'), `@use '../foundation/variables' as *;
823
+ @use '../foundation/mixins' as *;
824
+
825
+ %c-card {
826
+ display: block;
827
+ border: 1px solid $color-border;
828
+ border-radius: 8px;
829
+ overflow: hidden;
830
+ text-decoration: none;
831
+ color: $color-text;
832
+ transition: box-shadow 0.2s;
833
+ }
834
+
835
+ %c-card:hover {
836
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
837
+ }
838
+
839
+ .c-card--news {
840
+ @extend %c-card;
841
+ }
842
+
843
+ .c-card__body {
844
+ padding: $spacing-unit * 2;
845
+ }
846
+
847
+ .c-card__date {
848
+ display: block;
849
+ font-size: 0.8125rem;
850
+ color: $color-text-light;
851
+ margin-bottom: $spacing-unit;
852
+ }
853
+
854
+ .c-card__title {
855
+ font-size: 1rem;
856
+ font-weight: bold;
857
+ line-height: 1.5;
858
+ margin-bottom: $spacing-unit;
859
+ }
860
+
861
+ .c-card__excerpt {
862
+ font-size: 0.875rem;
863
+ color: $color-text-light;
864
+ line-height: 1.6;
865
+ }
866
+
867
+ @include tablet {
868
+ .c-card__body {
869
+ padding: $spacing-unit * 3;
870
+ }
871
+
872
+ .c-card__title {
873
+ font-size: 1.125rem;
874
+ }
875
+ }
876
+ `);
877
+ writeFile(s('component/_c-table.scss'), `@use '../foundation/variables' as *;
878
+ @use '../foundation/mixins' as *;
879
+
880
+ .c-table {
881
+ width: 100%;
882
+ }
883
+
884
+ .c-table__body {
885
+ display: block;
886
+ }
887
+
888
+ .c-table__row {
889
+ display: flex;
890
+ flex-direction: column;
891
+ border-bottom: 1px solid $color-border;
892
+ padding: $spacing-unit * 2 0;
893
+ }
894
+
895
+ .c-table__header {
896
+ font-weight: bold;
897
+ font-size: 0.875rem;
898
+ color: $color-text-light;
899
+ margin-bottom: $spacing-unit;
900
+ text-align: left;
901
+ }
902
+
903
+ .c-table__cell {
904
+ font-size: 1rem;
905
+ }
906
+
907
+ @include tablet {
908
+ .c-table__row {
909
+ flex-direction: row;
910
+ }
911
+
912
+ .c-table__header {
913
+ width: 200px;
914
+ flex-shrink: 0;
915
+ margin-bottom: 0;
916
+ padding-right: $spacing-unit * 2;
917
+ }
918
+ }
919
+ `);
920
+ // Element
921
+ writeFile(s('element/_e-heading.scss'), `@use '../foundation/variables' as *;
922
+ @use '../foundation/mixins' as *;
923
+
924
+ .e-heading {
925
+ font-size: 1.5rem;
926
+ font-weight: bold;
927
+ padding-bottom: $spacing-unit * 2;
928
+ border-bottom: 3px solid $color-primary;
929
+ display: inline-block;
930
+ }
931
+
932
+ @include tablet {
933
+ .e-heading {
934
+ font-size: 1.75rem;
935
+ }
936
+ }
937
+ `);
938
+ // Project
939
+ writeFile(s('project/_p-hero.scss'), `@use '../foundation/variables' as *;
940
+ @use '../foundation/mixins' as *;
941
+
942
+ .p-hero {
943
+ background-color: $color-bg-light;
944
+ padding: $spacing-unit * 6 $spacing-unit * 2;
945
+ text-align: center;
946
+ }
947
+
948
+ .p-hero__inner {
949
+ max-width: $max-width;
950
+ margin: 0 auto;
951
+ }
952
+
953
+ .p-hero__title {
954
+ font-size: 1.75rem;
955
+ font-weight: bold;
956
+ margin-bottom: $spacing-unit * 2;
957
+ }
958
+
959
+ .p-hero__lead {
960
+ font-size: 1rem;
961
+ color: $color-text-light;
962
+ line-height: 1.8;
963
+ }
964
+
965
+ @include tablet {
966
+ .p-hero {
967
+ padding: $spacing-unit * 10 $spacing-unit * 4;
968
+ }
969
+
970
+ .p-hero__title {
971
+ font-size: 2.25rem;
972
+ }
973
+
974
+ .p-hero__lead {
975
+ font-size: 1.125rem;
976
+ }
977
+ }
978
+ `);
979
+ writeFile(s('project/_p-news-list.scss'), `@use '../foundation/variables' as *;
980
+ @use '../foundation/mixins' as *;
981
+
982
+ .p-news-list {
983
+ padding: $spacing-unit * 6 $spacing-unit * 2;
984
+ }
985
+
986
+ .p-news-list__inner {
987
+ max-width: $max-width;
988
+ margin: 0 auto;
989
+ }
990
+
991
+ .p-news-list__items {
992
+ display: grid;
993
+ gap: $spacing-unit * 3;
994
+ margin-top: $spacing-unit * 4;
995
+ }
996
+
997
+ .p-news-list__item {
998
+ display: block;
999
+ }
1000
+
1001
+ .p-news-list__more {
1002
+ margin-top: $spacing-unit * 4;
1003
+ text-align: center;
1004
+ }
1005
+
1006
+ @include tablet {
1007
+ .p-news-list {
1008
+ padding: $spacing-unit * 8 $spacing-unit * 4;
1009
+ }
1010
+
1011
+ .p-news-list__items {
1012
+ grid-template-columns: repeat(2, 1fr);
1013
+ }
1014
+ }
1015
+
1016
+ @include desktop {
1017
+ .p-news-list__items {
1018
+ grid-template-columns: repeat(3, 1fr);
1019
+ }
1020
+ }
1021
+ `);
1022
+ writeFile(s('project/_p-article.scss'), `@use '../foundation/variables' as *;
1023
+ @use '../foundation/mixins' as *;
1024
+
1025
+ .p-article {
1026
+ padding: $spacing-unit * 4 $spacing-unit * 2;
1027
+ }
1028
+
1029
+ .p-article__inner {
1030
+ max-width: 780px;
1031
+ margin: 0 auto;
1032
+ }
1033
+
1034
+ .p-article__header {
1035
+ margin-bottom: $spacing-unit * 4;
1036
+ }
1037
+
1038
+ .p-article__date {
1039
+ display: block;
1040
+ font-size: 0.875rem;
1041
+ color: $color-text-light;
1042
+ margin-bottom: $spacing-unit;
1043
+ }
1044
+
1045
+ .p-article__title {
1046
+ font-size: 1.5rem;
1047
+ font-weight: bold;
1048
+ line-height: 1.4;
1049
+ }
1050
+
1051
+ .p-article__eyecatch {
1052
+ margin-bottom: $spacing-unit * 4;
1053
+ }
1054
+
1055
+ .p-article__eyecatch__image {
1056
+ width: 100%;
1057
+ aspect-ratio: 16 / 9;
1058
+ object-fit: cover;
1059
+ border-radius: 8px;
1060
+ }
1061
+
1062
+ .p-article__body {
1063
+ font-size: 1rem;
1064
+ line-height: 1.8;
1065
+ }
1066
+
1067
+ .p-article__body h2 {
1068
+ font-size: 1.375rem;
1069
+ font-weight: bold;
1070
+ margin-top: $spacing-unit * 5;
1071
+ margin-bottom: $spacing-unit * 2;
1072
+ padding-bottom: $spacing-unit;
1073
+ border-bottom: 2px solid $color-border;
1074
+ }
1075
+
1076
+ .p-article__body h3 {
1077
+ font-size: 1.125rem;
1078
+ font-weight: bold;
1079
+ margin-top: $spacing-unit * 4;
1080
+ margin-bottom: $spacing-unit * 2;
1081
+ }
1082
+
1083
+ .p-article__body p {
1084
+ margin-bottom: $spacing-unit * 2;
1085
+ }
1086
+
1087
+ .p-article__body ul,
1088
+ .p-article__body ol {
1089
+ margin-bottom: $spacing-unit * 2;
1090
+ padding-left: $spacing-unit * 3;
1091
+ list-style: disc;
1092
+ }
1093
+
1094
+ .p-article__body ol {
1095
+ list-style: decimal;
1096
+ }
1097
+
1098
+ .p-article__footer {
1099
+ margin-top: $spacing-unit * 6;
1100
+ padding-top: $spacing-unit * 4;
1101
+ border-top: 1px solid $color-border;
1102
+ }
1103
+
1104
+ @include tablet {
1105
+ .p-article {
1106
+ padding: $spacing-unit * 6 $spacing-unit * 4;
1107
+ }
1108
+
1109
+ .p-article__title {
1110
+ font-size: 2rem;
1111
+ }
1112
+ }
1113
+ `);
1114
+ writeFile(s('project/_p-about.scss'), `@use '../foundation/variables' as *;
1115
+ @use '../foundation/mixins' as *;
1116
+
1117
+ .p-about {
1118
+ padding: $spacing-unit * 6 $spacing-unit * 2;
1119
+ }
1120
+
1121
+ .p-about__inner {
1122
+ max-width: $max-width;
1123
+ margin: 0 auto;
1124
+ }
1125
+
1126
+ .p-about__content {
1127
+ margin-top: $spacing-unit * 4;
1128
+ line-height: 1.8;
1129
+ }
1130
+
1131
+ .p-about__content p {
1132
+ margin-bottom: $spacing-unit * 2;
1133
+ }
1134
+
1135
+ .p-about__table {
1136
+ margin-top: $spacing-unit * 6;
1137
+ }
1138
+
1139
+ @include tablet {
1140
+ .p-about {
1141
+ padding: $spacing-unit * 8 $spacing-unit * 4;
1142
+ }
1143
+ }
1144
+ `);
1145
+ writeFile(s('project/_p-contact.scss'), `@use '../foundation/variables' as *;
1146
+ @use '../foundation/mixins' as *;
1147
+
1148
+ .p-contact {
1149
+ padding: $spacing-unit * 6 $spacing-unit * 2;
1150
+ }
1151
+
1152
+ .p-contact__inner {
1153
+ max-width: 640px;
1154
+ margin: 0 auto;
1155
+ }
1156
+
1157
+ .p-contact__lead {
1158
+ margin-top: $spacing-unit * 2;
1159
+ color: $color-text-light;
1160
+ }
1161
+
1162
+ .p-contact__form {
1163
+ margin-top: $spacing-unit * 4;
1164
+ }
1165
+
1166
+ .p-contact__field {
1167
+ margin-bottom: $spacing-unit * 3;
1168
+ }
1169
+
1170
+ .p-contact__label {
1171
+ display: block;
1172
+ font-size: 0.875rem;
1173
+ font-weight: bold;
1174
+ margin-bottom: $spacing-unit;
1175
+ }
1176
+
1177
+ .p-contact__input {
1178
+ display: block;
1179
+ width: 100%;
1180
+ padding: $spacing-unit * 1.5;
1181
+ border: 1px solid $color-border;
1182
+ border-radius: 4px;
1183
+ font-size: 1rem;
1184
+ }
1185
+
1186
+ .p-contact__input:focus {
1187
+ outline: none;
1188
+ border-color: $color-primary;
1189
+ box-shadow: 0 0 0 2px rgba($color-primary, 0.2);
1190
+ }
1191
+
1192
+ .p-contact__textarea {
1193
+ display: block;
1194
+ width: 100%;
1195
+ padding: $spacing-unit * 1.5;
1196
+ border: 1px solid $color-border;
1197
+ border-radius: 4px;
1198
+ font-size: 1rem;
1199
+ resize: vertical;
1200
+ }
1201
+
1202
+ .p-contact__textarea:focus {
1203
+ outline: none;
1204
+ border-color: $color-primary;
1205
+ box-shadow: 0 0 0 2px rgba($color-primary, 0.2);
1206
+ }
1207
+
1208
+ .p-contact__actions {
1209
+ margin-top: $spacing-unit * 4;
1210
+ text-align: center;
1211
+ }
1212
+
1213
+ @include tablet {
1214
+ .p-contact {
1215
+ padding: $spacing-unit * 8 $spacing-unit * 4;
1216
+ }
1217
+ }
1218
+ `);
1219
+ }
1220
+ // ========== TypeScript ==========
1221
+ function generateTypeScript(projectDir) {
1222
+ const ts = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/ts', ...p);
1223
+ writeFile(ts('index.ts'), `import { initMobileNav } from './features/mobile-nav'
1224
+ import { initSmoothScroll } from './features/smooth-scroll'
1225
+
1226
+ document.addEventListener('DOMContentLoaded', () => {
1227
+ initMobileNav()
1228
+ initSmoothScroll()
1229
+ })
1230
+ `);
1231
+ writeFile(ts('features/mobile-nav.ts'), `export function initMobileNav(): void {
1232
+ const toggle = document.querySelector<HTMLButtonElement>('.c-nav__toggle')
1233
+ const list = document.querySelector<HTMLUListElement>('.c-nav__list')
1234
+
1235
+ if (!toggle || !list) return
1236
+
1237
+ toggle.addEventListener('click', () => {
1238
+ const isOpen = list.classList.contains('c-nav__list--open')
1239
+ list.classList.toggle('c-nav__list--open', !isOpen)
1240
+ toggle.setAttribute('aria-expanded', String(!isOpen))
1241
+ })
1242
+
1243
+ document.addEventListener('click', (e) => {
1244
+ const target = e.target as HTMLElement
1245
+ if (!target.closest('.c-nav')) {
1246
+ list.classList.remove('c-nav__list--open')
1247
+ toggle.setAttribute('aria-expanded', 'false')
1248
+ }
1249
+ })
1250
+ }
1251
+ `);
1252
+ writeFile(ts('features/smooth-scroll.ts'), `export function initSmoothScroll(): void {
1253
+ document.addEventListener('click', (e) => {
1254
+ const target = e.target as HTMLElement
1255
+ const anchor = target.closest<HTMLAnchorElement>('a[href^="#"]')
1256
+
1257
+ if (!anchor) return
1258
+
1259
+ const id = anchor.getAttribute('href')
1260
+ if (!id || id === '#') return
1261
+
1262
+ const element = document.querySelector(id)
1263
+ if (!element) return
1264
+
1265
+ e.preventDefault()
1266
+ element.scrollIntoView({ behavior: 'smooth' })
1267
+ })
1268
+ }
1269
+ `);
1270
+ }
1271
+ // ========== CLAUDE.md ==========
1272
+ function generateClaudeMd(projectDir) {
1273
+ writeFile(path.join(projectDir, 'CLAUDE.md'), `# CLAUDE.md
1274
+
1275
+ Frelio(ヘッドレス CMS)で構築されたサイトリポジトリ。
1276
+ お知らせブログ付きのシンプルなコーポレートサイト。
1277
+
1278
+ ## プロジェクト構成
1279
+
1280
+ - \`admin/\` — CMS 管理画面(ビルド済み、\`config.json\` で設定)
1281
+ - \`public/\` — 公開サイトルート(SSG 出力先)
1282
+ - \`functions/\` — Cloudflare Pages Functions
1283
+ - \`api/auth/\` — OAuth 認証
1284
+ - \`api/storage/\` — CMS ファイルアップロード API
1285
+ - \`storage/[[path]].ts\` — R2 ファイル配信(/storage/*)
1286
+ - \`frelio-data/\` — CMS データ(コンテンツタイプ、コンテンツ、テンプレート、レシピ)
1287
+ - \`site/templates/assets/scss/\` — SCSS ソース(FLOCSS 亜種)
1288
+ - \`site/templates/assets/ts/\` — TypeScript ソース
1289
+ - \`public/images/\` — 静的ファイル(画像等、git 追跡対象)
1290
+
1291
+ ## よく使うコマンド
1292
+
1293
+ \`\`\`bash
1294
+ npm run dev # Vite dev server(テンプレートプレビュー)
1295
+ npm run build # SCSS/TS ビルド
1296
+ npx @frelio/cli update # CMS Admin バンドル更新
1297
+ npx @frelio/cli add-staging # カスタムステージング追加
1298
+ \`\`\`
1299
+
1300
+ ## CSS 記法ルール(FLOCSS 亜種・厳格)
1301
+
1302
+ - **プレフィックス**: \`l-\`(layout)、\`c-\`(component)、\`p-\`(project)、\`e-\`(element)のみ
1303
+ - **1 class ルール**: HTML の class 属性には必ず 1 クラスのみ
1304
+ - **Utility は絶対使用禁止**
1305
+ - **SCSS の \`&\` でクラス名を接続することは禁止**(grep で追跡可能を維持)
1306
+ - **@extend**: 同ファイル内でのみ許可。\`%placeholder\` または実体クラスに \`--variant\` サフィックス
1307
+ - **子要素**: \`__\` で繋げる(深さ制限なし)
1308
+ - **バリアント**: \`--\` サフィックス(数に制限なし)
1309
+ - **メディアクエリ**: \`@mixin\` で定義し、1 ファイルに各 mixin を 1 回のみ \`@include\`
1310
+
1311
+ ### 例
1312
+
1313
+ \`\`\`scss
1314
+ %c-section__inner__item {} // ベースデザイン
1315
+ .c-section__inner__item--red { @extend %c-section__inner__item; color: red; }
1316
+ \`\`\`
1317
+
1318
+ ## TypeScript ルール
1319
+
1320
+ - \`frelio-data/site/templates/assets/ts/index.ts\` はオーケストレーション専用(import + init 呼び出しのみ)
1321
+ - 各機能は \`frelio-data/site/templates/assets/ts/features/\` にファイル分離
1322
+
1323
+ ## テンプレート規約
1324
+
1325
+ - テンプレートエンジン: gentl(\`data-gen-*\` 属性ベース)
1326
+ - テンプレートは valid HTML(そのままブラウザで開ける)
1327
+ - 共通パーツ: \`_parts/*.htm\`(head, header, footer)
1328
+ - ページテンプレート: \`*.html\`
1329
+
1330
+ ## Cloudflare Pages 構成
1331
+
1332
+ - \`_redirects\`: \`/admin/*\` → SPA、\`/*\` → \`/public/:splat\`
1333
+ - \`_routes.json\`: \`/api/*\`, \`/storage/*\` → Functions
1334
+ - \`wrangler.toml\`: R2 バケットバインディング
1335
+
1336
+ ## 型パッケージ活用方針
1337
+
1338
+ JSON ファイルの読み書きでは、以下の型・ガード・スキーマを使用する。
1339
+
1340
+ | ファイル | 型 | パッケージ |
1341
+ |---|---|---|
1342
+ | \`content_types/*.json\` | \`ContentType\` / \`validateContentType\` | \`@c-time/frelio-types\` |
1343
+ | \`*.ui.json\` | \`ContentTypeUi\` / \`validateContentTypeUi\` | 同上 |
1344
+ | \`*.views.json\` | \`ContentTypeViews\` / \`validateContentTypeViews\` | 同上 |
1345
+ | \`build-data-recipe.json\` | \`FrelioBuildDataRecipe\` / \`validateSiteRecipe\` | 同上 |
1346
+ | \`data-json/*.json\` | \`FrelioDataJson\` / \`isFrelioDataJson\` | \`@c-time/frelio-data-json\` |
1347
+ | \`contents/*/*.json\` | \`Content\` / \`isContent\` | \`@c-time/frelio-types\` |
1348
+ `);
1349
+ }
1350
+ // ========== Helpers ==========
1351
+ function json(obj) {
1352
+ return JSON.stringify(obj, null, 2);
1353
+ }