@c-time/frelio-cli 1.4.1 → 1.4.3
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/dist/core/file-generators.d.ts +3 -0
- package/dist/core/file-generators.js +12 -1
- package/dist/lib/templates.d.ts +1 -0
- package/dist/lib/templates.js +21 -0
- package/package.json +1 -1
- package/dist/core/workflows.d.ts +0 -11
- package/dist/core/workflows.js +0 -345
- package/dist/lib/github-release.d.ts +0 -15
- package/dist/lib/github-release.js +0 -41
- package/dist/lib/initial-content.d.ts +0 -5
- package/dist/lib/initial-content.js +0 -2215
|
@@ -1,2215 +0,0 @@
|
|
|
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}.json',
|
|
115
|
-
templatePath: 'news/detail.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
|
-
// --- Entry Points ---
|
|
158
|
-
generateEntries(projectDir);
|
|
159
|
-
// --- Build Scripts ---
|
|
160
|
-
generateBuildScripts(projectDir);
|
|
161
|
-
// --- CLAUDE.md ---
|
|
162
|
-
generateClaudeMd(projectDir);
|
|
163
|
-
}
|
|
164
|
-
// ========== HTML Templates ==========
|
|
165
|
-
function generateTemplates(projectDir) {
|
|
166
|
-
const t = (...p) => path.join(projectDir, 'frelio-data/site/templates', ...p);
|
|
167
|
-
// _parts/head.htm
|
|
168
|
-
writeFile(t('_parts/head.htm'), `<meta charset="UTF-8">
|
|
169
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
170
|
-
<title data-gen-text="meta.title">Frelio Demo</title>
|
|
171
|
-
<link rel="stylesheet" href="/common/styles/index.css">
|
|
172
|
-
<script type="module" src="/common/scripts/index.js"></script>
|
|
173
|
-
`);
|
|
174
|
-
// _parts/header.htm
|
|
175
|
-
writeFile(t('_parts/header.htm'), `<header class="l-header">
|
|
176
|
-
<div class="l-header__inner">
|
|
177
|
-
<a href="/" class="c-logo">Frelio Demo</a>
|
|
178
|
-
<nav class="c-nav">
|
|
179
|
-
<button type="button" class="c-nav__toggle" aria-label="メニュー" aria-expanded="false">
|
|
180
|
-
<span class="c-nav__toggle__bar"></span>
|
|
181
|
-
<span class="c-nav__toggle__bar"></span>
|
|
182
|
-
<span class="c-nav__toggle__bar"></span>
|
|
183
|
-
</button>
|
|
184
|
-
<ul class="c-nav__list">
|
|
185
|
-
<li class="c-nav__item"><a href="/">ホーム</a></li>
|
|
186
|
-
<li class="c-nav__item"><a href="/news/">お知らせ</a></li>
|
|
187
|
-
<li class="c-nav__item"><a href="/about/">会社概要</a></li>
|
|
188
|
-
<li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
|
|
189
|
-
</ul>
|
|
190
|
-
</nav>
|
|
191
|
-
</div>
|
|
192
|
-
</header>
|
|
193
|
-
`);
|
|
194
|
-
// _parts/footer.htm
|
|
195
|
-
writeFile(t('_parts/footer.htm'), `<footer class="l-footer">
|
|
196
|
-
<div class="l-footer__inner">
|
|
197
|
-
<p class="l-footer__copyright">© 2026 Frelio Demo. All rights reserved.</p>
|
|
198
|
-
</div>
|
|
199
|
-
</footer>
|
|
200
|
-
`);
|
|
201
|
-
// index.html
|
|
202
|
-
writeFile(t('index.html'), `<!DOCTYPE html>
|
|
203
|
-
<html lang="ja">
|
|
204
|
-
<head>
|
|
205
|
-
<template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
|
|
206
|
-
<meta charset="UTF-8">
|
|
207
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
208
|
-
<title>Frelio Demo</title>
|
|
209
|
-
<link rel="stylesheet" href="/styles/index.css">
|
|
210
|
-
<script type="module" src="/scripts/index.js"></script>
|
|
211
|
-
</head>
|
|
212
|
-
<body>
|
|
213
|
-
<template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
|
|
214
|
-
<header class="l-header">
|
|
215
|
-
<div class="l-header__inner">
|
|
216
|
-
<a href="/" class="c-logo">Frelio Demo</a>
|
|
217
|
-
<nav class="c-nav">
|
|
218
|
-
<ul class="c-nav__list">
|
|
219
|
-
<li class="c-nav__item"><a href="/">ホーム</a></li>
|
|
220
|
-
<li class="c-nav__item"><a href="/news/">お知らせ</a></li>
|
|
221
|
-
<li class="c-nav__item"><a href="/about/">会社概要</a></li>
|
|
222
|
-
<li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
|
|
223
|
-
</ul>
|
|
224
|
-
</nav>
|
|
225
|
-
</div>
|
|
226
|
-
</header>
|
|
227
|
-
|
|
228
|
-
<main class="l-main">
|
|
229
|
-
<section class="p-hero">
|
|
230
|
-
<div class="p-hero__inner">
|
|
231
|
-
<h1 class="p-hero__title">Frelio Demo</h1>
|
|
232
|
-
<p class="p-hero__lead">GitHub + Cloudflare で動く、シンプルなヘッドレス CMS のデモサイトです。</p>
|
|
233
|
-
</div>
|
|
234
|
-
</section>
|
|
235
|
-
|
|
236
|
-
<section class="p-news-list">
|
|
237
|
-
<div class="p-news-list__inner">
|
|
238
|
-
<h2 class="e-heading">お知らせ</h2>
|
|
239
|
-
<ul class="p-news-list__items">
|
|
240
|
-
<template data-gen-scope="" data-gen-repeat="latestNews" data-gen-repeat-name="article">
|
|
241
|
-
<li class="p-news-list__item">
|
|
242
|
-
<a class="c-card--news" data-gen-attrs="href:article.href">
|
|
243
|
-
<div class="c-card__body">
|
|
244
|
-
<time class="c-card__date" data-gen-text="article.publishDate">2026-03-01</time>
|
|
245
|
-
<h3 class="c-card__title" data-gen-text="article.title">お知らせタイトル</h3>
|
|
246
|
-
<p class="c-card__excerpt" data-gen-text="article.excerpt">お知らせの概要がここに入ります。</p>
|
|
247
|
-
</div>
|
|
248
|
-
</a>
|
|
249
|
-
</li>
|
|
250
|
-
</template>
|
|
251
|
-
<li class="p-news-list__item" data-gen-comment="">
|
|
252
|
-
<a class="c-card--news" href="/news/sample/">
|
|
253
|
-
<div class="c-card__body">
|
|
254
|
-
<time class="c-card__date">2026-03-01</time>
|
|
255
|
-
<h3 class="c-card__title">サンプルお知らせ</h3>
|
|
256
|
-
<p class="c-card__excerpt">これはプレビュー用のダミーデータです。</p>
|
|
257
|
-
</div>
|
|
258
|
-
</a>
|
|
259
|
-
</li>
|
|
260
|
-
</ul>
|
|
261
|
-
<div class="p-news-list__more">
|
|
262
|
-
<a href="/news/" class="c-btn">お知らせ一覧へ</a>
|
|
263
|
-
</div>
|
|
264
|
-
</div>
|
|
265
|
-
</section>
|
|
266
|
-
</main>
|
|
267
|
-
|
|
268
|
-
<template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
|
|
269
|
-
<footer class="l-footer">
|
|
270
|
-
<div class="l-footer__inner">
|
|
271
|
-
<p class="l-footer__copyright">© 2026 Frelio Demo. All rights reserved.</p>
|
|
272
|
-
</div>
|
|
273
|
-
</footer>
|
|
274
|
-
</body>
|
|
275
|
-
</html>
|
|
276
|
-
`);
|
|
277
|
-
// about/index.html
|
|
278
|
-
writeFile(t('about/index.html'), `<!DOCTYPE html>
|
|
279
|
-
<html lang="ja">
|
|
280
|
-
<head>
|
|
281
|
-
<template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
|
|
282
|
-
<meta charset="UTF-8">
|
|
283
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
284
|
-
<title>会社概要 | Frelio Demo</title>
|
|
285
|
-
<link rel="stylesheet" href="/about/styles/index.css">
|
|
286
|
-
<script type="module" src="/about/scripts/index.js"></script>
|
|
287
|
-
</head>
|
|
288
|
-
<body>
|
|
289
|
-
<template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
|
|
290
|
-
<header class="l-header">
|
|
291
|
-
<div class="l-header__inner">
|
|
292
|
-
<a href="/" class="c-logo">Frelio Demo</a>
|
|
293
|
-
<nav class="c-nav">
|
|
294
|
-
<ul class="c-nav__list">
|
|
295
|
-
<li class="c-nav__item"><a href="/">ホーム</a></li>
|
|
296
|
-
<li class="c-nav__item"><a href="/news/">お知らせ</a></li>
|
|
297
|
-
<li class="c-nav__item"><a href="/about/">会社概要</a></li>
|
|
298
|
-
<li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
|
|
299
|
-
</ul>
|
|
300
|
-
</nav>
|
|
301
|
-
</div>
|
|
302
|
-
</header>
|
|
303
|
-
|
|
304
|
-
<main class="l-main">
|
|
305
|
-
<section class="p-about">
|
|
306
|
-
<div class="p-about__inner">
|
|
307
|
-
<h1 class="e-heading">会社概要</h1>
|
|
308
|
-
<div class="p-about__content">
|
|
309
|
-
<p>Frelio Demo は、ヘッドレス CMS「Frelio」の機能を紹介するためのデモサイトです。</p>
|
|
310
|
-
<p>Frelio は GitHub をデータストア、Cloudflare を配信基盤として活用し、サーバーレスで運用できる次世代の CMS です。</p>
|
|
311
|
-
</div>
|
|
312
|
-
<div class="p-about__table">
|
|
313
|
-
<table class="c-table">
|
|
314
|
-
<tbody class="c-table__body">
|
|
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">東京都渋谷区</td>
|
|
322
|
-
</tr>
|
|
323
|
-
<tr class="c-table__row">
|
|
324
|
-
<th class="c-table__header">設立</th>
|
|
325
|
-
<td class="c-table__cell">2026年</td>
|
|
326
|
-
</tr>
|
|
327
|
-
<tr class="c-table__row">
|
|
328
|
-
<th class="c-table__header">事業内容</th>
|
|
329
|
-
<td class="c-table__cell">ヘッドレス CMS の開発・提供</td>
|
|
330
|
-
</tr>
|
|
331
|
-
</tbody>
|
|
332
|
-
</table>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
</section>
|
|
336
|
-
</main>
|
|
337
|
-
|
|
338
|
-
<template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
|
|
339
|
-
<footer class="l-footer">
|
|
340
|
-
<div class="l-footer__inner">
|
|
341
|
-
<p class="l-footer__copyright">© 2026 Frelio Demo. All rights reserved.</p>
|
|
342
|
-
</div>
|
|
343
|
-
</footer>
|
|
344
|
-
</body>
|
|
345
|
-
</html>
|
|
346
|
-
`);
|
|
347
|
-
// contact/index.html
|
|
348
|
-
writeFile(t('contact/index.html'), `<!DOCTYPE html>
|
|
349
|
-
<html lang="ja">
|
|
350
|
-
<head>
|
|
351
|
-
<template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
|
|
352
|
-
<meta charset="UTF-8">
|
|
353
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
354
|
-
<title>お問い合わせ | Frelio Demo</title>
|
|
355
|
-
<link rel="stylesheet" href="/contact/styles/index.css">
|
|
356
|
-
<script type="module" src="/contact/scripts/index.js"></script>
|
|
357
|
-
</head>
|
|
358
|
-
<body>
|
|
359
|
-
<template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
|
|
360
|
-
<header class="l-header">
|
|
361
|
-
<div class="l-header__inner">
|
|
362
|
-
<a href="/" class="c-logo">Frelio Demo</a>
|
|
363
|
-
<nav class="c-nav">
|
|
364
|
-
<ul class="c-nav__list">
|
|
365
|
-
<li class="c-nav__item"><a href="/">ホーム</a></li>
|
|
366
|
-
<li class="c-nav__item"><a href="/news/">お知らせ</a></li>
|
|
367
|
-
<li class="c-nav__item"><a href="/about/">会社概要</a></li>
|
|
368
|
-
<li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
|
|
369
|
-
</ul>
|
|
370
|
-
</nav>
|
|
371
|
-
</div>
|
|
372
|
-
</header>
|
|
373
|
-
|
|
374
|
-
<main class="l-main">
|
|
375
|
-
<section class="p-contact">
|
|
376
|
-
<div class="p-contact__inner">
|
|
377
|
-
<h1 class="e-heading">お問い合わせ</h1>
|
|
378
|
-
<p class="p-contact__lead">お気軽にお問い合わせください。</p>
|
|
379
|
-
<form class="p-contact__form" action="#" method="post">
|
|
380
|
-
<div class="p-contact__field">
|
|
381
|
-
<label class="p-contact__label" for="contact-name">お名前</label>
|
|
382
|
-
<input class="p-contact__input" type="text" id="contact-name" name="name" required>
|
|
383
|
-
</div>
|
|
384
|
-
<div class="p-contact__field">
|
|
385
|
-
<label class="p-contact__label" for="contact-email">メールアドレス</label>
|
|
386
|
-
<input class="p-contact__input" type="email" id="contact-email" name="email" required>
|
|
387
|
-
</div>
|
|
388
|
-
<div class="p-contact__field">
|
|
389
|
-
<label class="p-contact__label" for="contact-message">メッセージ</label>
|
|
390
|
-
<textarea class="p-contact__textarea" id="contact-message" name="message" rows="6" required></textarea>
|
|
391
|
-
</div>
|
|
392
|
-
<div class="p-contact__actions">
|
|
393
|
-
<button type="submit" class="c-btn--submit">送信する</button>
|
|
394
|
-
</div>
|
|
395
|
-
</form>
|
|
396
|
-
</div>
|
|
397
|
-
</section>
|
|
398
|
-
</main>
|
|
399
|
-
|
|
400
|
-
<template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
|
|
401
|
-
<footer class="l-footer">
|
|
402
|
-
<div class="l-footer__inner">
|
|
403
|
-
<p class="l-footer__copyright">© 2026 Frelio Demo. All rights reserved.</p>
|
|
404
|
-
</div>
|
|
405
|
-
</footer>
|
|
406
|
-
</body>
|
|
407
|
-
</html>
|
|
408
|
-
`);
|
|
409
|
-
// news/index.html
|
|
410
|
-
writeFile(t('news/index.html'), `<!DOCTYPE html>
|
|
411
|
-
<html lang="ja">
|
|
412
|
-
<head>
|
|
413
|
-
<template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
|
|
414
|
-
<meta charset="UTF-8">
|
|
415
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
416
|
-
<title>お知らせ | Frelio Demo</title>
|
|
417
|
-
<link rel="stylesheet" href="/news/styles/index.css">
|
|
418
|
-
<script type="module" src="/news/scripts/index.js"></script>
|
|
419
|
-
</head>
|
|
420
|
-
<body>
|
|
421
|
-
<template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
|
|
422
|
-
<header class="l-header">
|
|
423
|
-
<div class="l-header__inner">
|
|
424
|
-
<a href="/" class="c-logo">Frelio Demo</a>
|
|
425
|
-
<nav class="c-nav">
|
|
426
|
-
<ul class="c-nav__list">
|
|
427
|
-
<li class="c-nav__item"><a href="/">ホーム</a></li>
|
|
428
|
-
<li class="c-nav__item"><a href="/news/">お知らせ</a></li>
|
|
429
|
-
<li class="c-nav__item"><a href="/about/">会社概要</a></li>
|
|
430
|
-
<li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
|
|
431
|
-
</ul>
|
|
432
|
-
</nav>
|
|
433
|
-
</div>
|
|
434
|
-
</header>
|
|
435
|
-
|
|
436
|
-
<main class="l-main">
|
|
437
|
-
<section class="p-news-list">
|
|
438
|
-
<div class="p-news-list__inner">
|
|
439
|
-
<h1 class="e-heading">お知らせ</h1>
|
|
440
|
-
<ul class="p-news-list__items">
|
|
441
|
-
<template data-gen-scope="" data-gen-repeat="pagedItems" data-gen-repeat-name="article">
|
|
442
|
-
<li class="p-news-list__item">
|
|
443
|
-
<a class="c-card--news" data-gen-attrs="href:article.href">
|
|
444
|
-
<div class="c-card__body">
|
|
445
|
-
<time class="c-card__date" data-gen-text="article.publishDate">2026-03-01</time>
|
|
446
|
-
<h3 class="c-card__title" data-gen-text="article.title">お知らせタイトル</h3>
|
|
447
|
-
<p class="c-card__excerpt" data-gen-text="article.excerpt">お知らせの概要がここに入ります。</p>
|
|
448
|
-
</div>
|
|
449
|
-
</a>
|
|
450
|
-
</li>
|
|
451
|
-
</template>
|
|
452
|
-
<li class="p-news-list__item" data-gen-comment="">
|
|
453
|
-
<a class="c-card--news" href="/news/sample/">
|
|
454
|
-
<div class="c-card__body">
|
|
455
|
-
<time class="c-card__date">2026-03-01</time>
|
|
456
|
-
<h3 class="c-card__title">サンプルお知らせ</h3>
|
|
457
|
-
<p class="c-card__excerpt">これはプレビュー用のダミーデータです。</p>
|
|
458
|
-
</div>
|
|
459
|
-
</a>
|
|
460
|
-
</li>
|
|
461
|
-
</ul>
|
|
462
|
-
</div>
|
|
463
|
-
</section>
|
|
464
|
-
</main>
|
|
465
|
-
|
|
466
|
-
<template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
|
|
467
|
-
<footer class="l-footer">
|
|
468
|
-
<div class="l-footer__inner">
|
|
469
|
-
<p class="l-footer__copyright">© 2026 Frelio Demo. All rights reserved.</p>
|
|
470
|
-
</div>
|
|
471
|
-
</footer>
|
|
472
|
-
</body>
|
|
473
|
-
</html>
|
|
474
|
-
`);
|
|
475
|
-
// news/detail.html
|
|
476
|
-
writeFile(t('news/detail.html'), `<!DOCTYPE html>
|
|
477
|
-
<html lang="ja">
|
|
478
|
-
<head>
|
|
479
|
-
<template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
|
|
480
|
-
<meta charset="UTF-8">
|
|
481
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
482
|
-
<title>記事タイトル | Frelio Demo</title>
|
|
483
|
-
<link rel="stylesheet" href="/news/styles/index.css">
|
|
484
|
-
<script type="module" src="/news/scripts/index.js"></script>
|
|
485
|
-
</head>
|
|
486
|
-
<body>
|
|
487
|
-
<template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
|
|
488
|
-
<header class="l-header">
|
|
489
|
-
<div class="l-header__inner">
|
|
490
|
-
<a href="/" class="c-logo">Frelio Demo</a>
|
|
491
|
-
<nav class="c-nav">
|
|
492
|
-
<ul class="c-nav__list">
|
|
493
|
-
<li class="c-nav__item"><a href="/">ホーム</a></li>
|
|
494
|
-
<li class="c-nav__item"><a href="/news/">お知らせ</a></li>
|
|
495
|
-
<li class="c-nav__item"><a href="/about/">会社概要</a></li>
|
|
496
|
-
<li class="c-nav__item"><a href="/contact/">お問い合わせ</a></li>
|
|
497
|
-
</ul>
|
|
498
|
-
</nav>
|
|
499
|
-
</div>
|
|
500
|
-
</header>
|
|
501
|
-
|
|
502
|
-
<main class="l-main">
|
|
503
|
-
<article class="p-article">
|
|
504
|
-
<div class="p-article__inner">
|
|
505
|
-
<header class="p-article__header">
|
|
506
|
-
<time class="p-article__date" data-gen-text="publishDate">2026-03-01</time>
|
|
507
|
-
<h1 class="p-article__title" data-gen-text="title">記事タイトル</h1>
|
|
508
|
-
</header>
|
|
509
|
-
<template data-gen-scope="" data-gen-if="eyecatch">
|
|
510
|
-
<div class="p-article__eyecatch">
|
|
511
|
-
<img class="p-article__eyecatch__image" data-gen-attrs="src:eyecatch.url,alt:title" src="/images/placeholder.jpg" alt="記事タイトル">
|
|
512
|
-
</div>
|
|
513
|
-
</template>
|
|
514
|
-
<div class="p-article__eyecatch" data-gen-comment="">
|
|
515
|
-
<img class="p-article__eyecatch__image" src="/images/placeholder.jpg" alt="プレビュー画像">
|
|
516
|
-
</div>
|
|
517
|
-
<div class="p-article__body" data-gen-html="body">
|
|
518
|
-
<p>これはプレビュー用のダミー本文です。実際の記事コンテンツがここに表示されます。</p>
|
|
519
|
-
<h2>見出し</h2>
|
|
520
|
-
<p>記事の本文が続きます。HTMLリッチテキストで記述されます。</p>
|
|
521
|
-
</div>
|
|
522
|
-
<div class="p-article__footer">
|
|
523
|
-
<a href="/news/" class="c-btn">お知らせ一覧に戻る</a>
|
|
524
|
-
</div>
|
|
525
|
-
</div>
|
|
526
|
-
</article>
|
|
527
|
-
</main>
|
|
528
|
-
|
|
529
|
-
<template data-gen-scope="" data-gen-include="_parts/footer.htm"></template>
|
|
530
|
-
<footer class="l-footer">
|
|
531
|
-
<div class="l-footer__inner">
|
|
532
|
-
<p class="l-footer__copyright">© 2026 Frelio Demo. All rights reserved.</p>
|
|
533
|
-
</div>
|
|
534
|
-
</footer>
|
|
535
|
-
</body>
|
|
536
|
-
</html>
|
|
537
|
-
`);
|
|
538
|
-
// .gitkeep for images directories
|
|
539
|
-
const gitkeepDirs = [
|
|
540
|
-
'common/images',
|
|
541
|
-
'images',
|
|
542
|
-
'about/images',
|
|
543
|
-
'contact/images',
|
|
544
|
-
'news/images',
|
|
545
|
-
];
|
|
546
|
-
for (const dir of gitkeepDirs) {
|
|
547
|
-
writeFile(t(`${dir}/.gitkeep`), '');
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
// ========== SCSS ==========
|
|
551
|
-
function generateScss(projectDir) {
|
|
552
|
-
const s = (...p) => path.join(projectDir, 'frelio-data/site/templates/common/styles', ...p);
|
|
553
|
-
// Foundation
|
|
554
|
-
writeFile(s('foundation/_variables.scss'), `// Colors
|
|
555
|
-
$color-primary: #0070f3;
|
|
556
|
-
$color-primary-dark: #0060d0;
|
|
557
|
-
$color-text: #333;
|
|
558
|
-
$color-text-light: #666;
|
|
559
|
-
$color-border: #e0e0e0;
|
|
560
|
-
$color-bg: #fff;
|
|
561
|
-
$color-bg-light: #f8f9fa;
|
|
562
|
-
|
|
563
|
-
// Typography
|
|
564
|
-
$font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
565
|
-
"Hiragino Sans", "Noto Sans JP", sans-serif;
|
|
566
|
-
$line-height-base: 1.7;
|
|
567
|
-
|
|
568
|
-
// Layout
|
|
569
|
-
$max-width: 1100px;
|
|
570
|
-
$spacing-unit: 8px;
|
|
571
|
-
|
|
572
|
-
// Breakpoints
|
|
573
|
-
$bp-tablet: 768px;
|
|
574
|
-
$bp-desktop: 1024px;
|
|
575
|
-
`);
|
|
576
|
-
writeFile(s('foundation/_mixins.scss'), `@use 'variables' as *;
|
|
577
|
-
|
|
578
|
-
@mixin tablet {
|
|
579
|
-
@media (min-width: $bp-tablet) {
|
|
580
|
-
@content;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
@mixin desktop {
|
|
585
|
-
@media (min-width: $bp-desktop) {
|
|
586
|
-
@content;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
`);
|
|
590
|
-
writeFile(s('foundation/_reset.scss'), `@use 'variables' as *;
|
|
591
|
-
|
|
592
|
-
*,
|
|
593
|
-
*::before,
|
|
594
|
-
*::after {
|
|
595
|
-
box-sizing: border-box;
|
|
596
|
-
margin: 0;
|
|
597
|
-
padding: 0;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
html {
|
|
601
|
-
font-size: 100%;
|
|
602
|
-
-webkit-text-size-adjust: 100%;
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
body {
|
|
606
|
-
font-family: $font-family-base;
|
|
607
|
-
line-height: $line-height-base;
|
|
608
|
-
color: $color-text;
|
|
609
|
-
background-color: $color-bg;
|
|
610
|
-
-webkit-font-smoothing: antialiased;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
img {
|
|
614
|
-
max-width: 100%;
|
|
615
|
-
height: auto;
|
|
616
|
-
vertical-align: middle;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
a {
|
|
620
|
-
color: $color-primary;
|
|
621
|
-
text-decoration: none;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
ul,
|
|
625
|
-
ol {
|
|
626
|
-
list-style: none;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
table {
|
|
630
|
-
border-collapse: collapse;
|
|
631
|
-
border-spacing: 0;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
button,
|
|
635
|
-
input,
|
|
636
|
-
select,
|
|
637
|
-
textarea {
|
|
638
|
-
font: inherit;
|
|
639
|
-
color: inherit;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
button {
|
|
643
|
-
cursor: pointer;
|
|
644
|
-
background: none;
|
|
645
|
-
border: none;
|
|
646
|
-
}
|
|
647
|
-
`);
|
|
648
|
-
// Layout
|
|
649
|
-
writeFile(s('layout/_l-header.scss'), `@use '../foundation/variables' as *;
|
|
650
|
-
@use '../foundation/mixins' as *;
|
|
651
|
-
|
|
652
|
-
.l-header {
|
|
653
|
-
border-bottom: 1px solid $color-border;
|
|
654
|
-
background-color: $color-bg;
|
|
655
|
-
position: sticky;
|
|
656
|
-
top: 0;
|
|
657
|
-
z-index: 100;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
.l-header__inner {
|
|
661
|
-
max-width: $max-width;
|
|
662
|
-
margin: 0 auto;
|
|
663
|
-
padding: $spacing-unit * 2;
|
|
664
|
-
display: flex;
|
|
665
|
-
justify-content: space-between;
|
|
666
|
-
align-items: center;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
@include tablet {
|
|
670
|
-
.l-header__inner {
|
|
671
|
-
padding: $spacing-unit * 2 $spacing-unit * 4;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
`);
|
|
675
|
-
writeFile(s('layout/_l-footer.scss'), `@use '../foundation/variables' as *;
|
|
676
|
-
|
|
677
|
-
.l-footer {
|
|
678
|
-
margin-top: $spacing-unit * 8;
|
|
679
|
-
border-top: 1px solid $color-border;
|
|
680
|
-
background-color: $color-bg-light;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
.l-footer__inner {
|
|
684
|
-
max-width: $max-width;
|
|
685
|
-
margin: 0 auto;
|
|
686
|
-
padding: $spacing-unit * 4 $spacing-unit * 2;
|
|
687
|
-
text-align: center;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
.l-footer__copyright {
|
|
691
|
-
font-size: 0.875rem;
|
|
692
|
-
color: $color-text-light;
|
|
693
|
-
}
|
|
694
|
-
`);
|
|
695
|
-
writeFile(s('layout/_l-main.scss'), `.l-main {
|
|
696
|
-
min-height: 60vh;
|
|
697
|
-
}
|
|
698
|
-
`);
|
|
699
|
-
// Component
|
|
700
|
-
writeFile(s('component/_c-logo.scss'), `@use '../foundation/variables' as *;
|
|
701
|
-
|
|
702
|
-
.c-logo {
|
|
703
|
-
font-size: 1.25rem;
|
|
704
|
-
font-weight: bold;
|
|
705
|
-
color: $color-text;
|
|
706
|
-
text-decoration: none;
|
|
707
|
-
}
|
|
708
|
-
`);
|
|
709
|
-
writeFile(s('component/_c-nav.scss'), `@use '../foundation/variables' as *;
|
|
710
|
-
@use '../foundation/mixins' as *;
|
|
711
|
-
|
|
712
|
-
.c-nav {
|
|
713
|
-
display: flex;
|
|
714
|
-
align-items: center;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
.c-nav__toggle {
|
|
718
|
-
display: flex;
|
|
719
|
-
flex-direction: column;
|
|
720
|
-
gap: 4px;
|
|
721
|
-
width: 28px;
|
|
722
|
-
padding: 4px 0;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
.c-nav__toggle__bar {
|
|
726
|
-
display: block;
|
|
727
|
-
width: 100%;
|
|
728
|
-
height: 2px;
|
|
729
|
-
background-color: $color-text;
|
|
730
|
-
transition: transform 0.3s, opacity 0.3s;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
.c-nav__list {
|
|
734
|
-
display: none;
|
|
735
|
-
position: absolute;
|
|
736
|
-
top: 100%;
|
|
737
|
-
left: 0;
|
|
738
|
-
right: 0;
|
|
739
|
-
background-color: $color-bg;
|
|
740
|
-
border-bottom: 1px solid $color-border;
|
|
741
|
-
padding: $spacing-unit * 2;
|
|
742
|
-
flex-direction: column;
|
|
743
|
-
gap: $spacing-unit;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
.c-nav__list--open {
|
|
747
|
-
display: flex;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
.c-nav__item a {
|
|
751
|
-
color: $color-text-light;
|
|
752
|
-
font-size: 0.9375rem;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
.c-nav__item a:hover {
|
|
756
|
-
color: $color-primary;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
@include tablet {
|
|
760
|
-
.c-nav__toggle {
|
|
761
|
-
display: none;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
.c-nav__list {
|
|
765
|
-
display: flex;
|
|
766
|
-
position: static;
|
|
767
|
-
flex-direction: row;
|
|
768
|
-
gap: $spacing-unit * 3;
|
|
769
|
-
padding: 0;
|
|
770
|
-
border-bottom: none;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
`);
|
|
774
|
-
writeFile(s('component/_c-btn.scss'), `@use '../foundation/variables' as *;
|
|
775
|
-
|
|
776
|
-
%c-btn {
|
|
777
|
-
display: inline-block;
|
|
778
|
-
padding: $spacing-unit * 1.5 $spacing-unit * 4;
|
|
779
|
-
border: 1px solid $color-primary;
|
|
780
|
-
border-radius: 4px;
|
|
781
|
-
font-size: 0.9375rem;
|
|
782
|
-
text-align: center;
|
|
783
|
-
transition: background-color 0.2s, color 0.2s;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
.c-btn {
|
|
787
|
-
@extend %c-btn;
|
|
788
|
-
color: $color-primary;
|
|
789
|
-
background-color: transparent;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
.c-btn:hover {
|
|
793
|
-
background-color: $color-primary;
|
|
794
|
-
color: $color-bg;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
.c-btn--submit {
|
|
798
|
-
@extend %c-btn;
|
|
799
|
-
color: $color-bg;
|
|
800
|
-
background-color: $color-primary;
|
|
801
|
-
border-color: $color-primary;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
.c-btn--submit:hover {
|
|
805
|
-
background-color: $color-primary-dark;
|
|
806
|
-
border-color: $color-primary-dark;
|
|
807
|
-
}
|
|
808
|
-
`);
|
|
809
|
-
writeFile(s('component/_c-card.scss'), `@use '../foundation/variables' as *;
|
|
810
|
-
@use '../foundation/mixins' as *;
|
|
811
|
-
|
|
812
|
-
%c-card {
|
|
813
|
-
display: block;
|
|
814
|
-
border: 1px solid $color-border;
|
|
815
|
-
border-radius: 8px;
|
|
816
|
-
overflow: hidden;
|
|
817
|
-
text-decoration: none;
|
|
818
|
-
color: $color-text;
|
|
819
|
-
transition: box-shadow 0.2s;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
%c-card:hover {
|
|
823
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
.c-card--news {
|
|
827
|
-
@extend %c-card;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
.c-card__body {
|
|
831
|
-
padding: $spacing-unit * 2;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
.c-card__date {
|
|
835
|
-
display: block;
|
|
836
|
-
font-size: 0.8125rem;
|
|
837
|
-
color: $color-text-light;
|
|
838
|
-
margin-bottom: $spacing-unit;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
.c-card__title {
|
|
842
|
-
font-size: 1rem;
|
|
843
|
-
font-weight: bold;
|
|
844
|
-
line-height: 1.5;
|
|
845
|
-
margin-bottom: $spacing-unit;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
.c-card__excerpt {
|
|
849
|
-
font-size: 0.875rem;
|
|
850
|
-
color: $color-text-light;
|
|
851
|
-
line-height: 1.6;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
@include tablet {
|
|
855
|
-
.c-card__body {
|
|
856
|
-
padding: $spacing-unit * 3;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
.c-card__title {
|
|
860
|
-
font-size: 1.125rem;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
`);
|
|
864
|
-
writeFile(s('component/_c-table.scss'), `@use '../foundation/variables' as *;
|
|
865
|
-
@use '../foundation/mixins' as *;
|
|
866
|
-
|
|
867
|
-
.c-table {
|
|
868
|
-
width: 100%;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
.c-table__body {
|
|
872
|
-
display: block;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
.c-table__row {
|
|
876
|
-
display: flex;
|
|
877
|
-
flex-direction: column;
|
|
878
|
-
border-bottom: 1px solid $color-border;
|
|
879
|
-
padding: $spacing-unit * 2 0;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
.c-table__header {
|
|
883
|
-
font-weight: bold;
|
|
884
|
-
font-size: 0.875rem;
|
|
885
|
-
color: $color-text-light;
|
|
886
|
-
margin-bottom: $spacing-unit;
|
|
887
|
-
text-align: left;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
.c-table__cell {
|
|
891
|
-
font-size: 1rem;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
@include tablet {
|
|
895
|
-
.c-table__row {
|
|
896
|
-
flex-direction: row;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
.c-table__header {
|
|
900
|
-
width: 200px;
|
|
901
|
-
flex-shrink: 0;
|
|
902
|
-
margin-bottom: 0;
|
|
903
|
-
padding-right: $spacing-unit * 2;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
`);
|
|
907
|
-
// Element
|
|
908
|
-
writeFile(s('element/_e-heading.scss'), `@use '../foundation/variables' as *;
|
|
909
|
-
@use '../foundation/mixins' as *;
|
|
910
|
-
|
|
911
|
-
.e-heading {
|
|
912
|
-
font-size: 1.5rem;
|
|
913
|
-
font-weight: bold;
|
|
914
|
-
padding-bottom: $spacing-unit * 2;
|
|
915
|
-
border-bottom: 3px solid $color-primary;
|
|
916
|
-
display: inline-block;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
@include tablet {
|
|
920
|
-
.e-heading {
|
|
921
|
-
font-size: 1.75rem;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
`);
|
|
925
|
-
// Project
|
|
926
|
-
writeFile(s('project/_p-hero.scss'), `@use '../foundation/variables' as *;
|
|
927
|
-
@use '../foundation/mixins' as *;
|
|
928
|
-
|
|
929
|
-
.p-hero {
|
|
930
|
-
background-color: $color-bg-light;
|
|
931
|
-
padding: $spacing-unit * 6 $spacing-unit * 2;
|
|
932
|
-
text-align: center;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
.p-hero__inner {
|
|
936
|
-
max-width: $max-width;
|
|
937
|
-
margin: 0 auto;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
.p-hero__title {
|
|
941
|
-
font-size: 1.75rem;
|
|
942
|
-
font-weight: bold;
|
|
943
|
-
margin-bottom: $spacing-unit * 2;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
.p-hero__lead {
|
|
947
|
-
font-size: 1rem;
|
|
948
|
-
color: $color-text-light;
|
|
949
|
-
line-height: 1.8;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
@include tablet {
|
|
953
|
-
.p-hero {
|
|
954
|
-
padding: $spacing-unit * 10 $spacing-unit * 4;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
.p-hero__title {
|
|
958
|
-
font-size: 2.25rem;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
.p-hero__lead {
|
|
962
|
-
font-size: 1.125rem;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
`);
|
|
966
|
-
writeFile(s('project/_p-news-list.scss'), `@use '../foundation/variables' as *;
|
|
967
|
-
@use '../foundation/mixins' as *;
|
|
968
|
-
|
|
969
|
-
.p-news-list {
|
|
970
|
-
padding: $spacing-unit * 6 $spacing-unit * 2;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
.p-news-list__inner {
|
|
974
|
-
max-width: $max-width;
|
|
975
|
-
margin: 0 auto;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
.p-news-list__items {
|
|
979
|
-
display: grid;
|
|
980
|
-
gap: $spacing-unit * 3;
|
|
981
|
-
margin-top: $spacing-unit * 4;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
.p-news-list__item {
|
|
985
|
-
display: block;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
.p-news-list__more {
|
|
989
|
-
margin-top: $spacing-unit * 4;
|
|
990
|
-
text-align: center;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
@include tablet {
|
|
994
|
-
.p-news-list {
|
|
995
|
-
padding: $spacing-unit * 8 $spacing-unit * 4;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
.p-news-list__items {
|
|
999
|
-
grid-template-columns: repeat(2, 1fr);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
@include desktop {
|
|
1004
|
-
.p-news-list__items {
|
|
1005
|
-
grid-template-columns: repeat(3, 1fr);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
`);
|
|
1009
|
-
writeFile(s('project/_p-article.scss'), `@use '../foundation/variables' as *;
|
|
1010
|
-
@use '../foundation/mixins' as *;
|
|
1011
|
-
|
|
1012
|
-
.p-article {
|
|
1013
|
-
padding: $spacing-unit * 4 $spacing-unit * 2;
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
.p-article__inner {
|
|
1017
|
-
max-width: 780px;
|
|
1018
|
-
margin: 0 auto;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
.p-article__header {
|
|
1022
|
-
margin-bottom: $spacing-unit * 4;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
.p-article__date {
|
|
1026
|
-
display: block;
|
|
1027
|
-
font-size: 0.875rem;
|
|
1028
|
-
color: $color-text-light;
|
|
1029
|
-
margin-bottom: $spacing-unit;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
.p-article__title {
|
|
1033
|
-
font-size: 1.5rem;
|
|
1034
|
-
font-weight: bold;
|
|
1035
|
-
line-height: 1.4;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
.p-article__eyecatch {
|
|
1039
|
-
margin-bottom: $spacing-unit * 4;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
.p-article__eyecatch__image {
|
|
1043
|
-
width: 100%;
|
|
1044
|
-
aspect-ratio: 16 / 9;
|
|
1045
|
-
object-fit: cover;
|
|
1046
|
-
border-radius: 8px;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
.p-article__body {
|
|
1050
|
-
font-size: 1rem;
|
|
1051
|
-
line-height: 1.8;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
.p-article__body h2 {
|
|
1055
|
-
font-size: 1.375rem;
|
|
1056
|
-
font-weight: bold;
|
|
1057
|
-
margin-top: $spacing-unit * 5;
|
|
1058
|
-
margin-bottom: $spacing-unit * 2;
|
|
1059
|
-
padding-bottom: $spacing-unit;
|
|
1060
|
-
border-bottom: 2px solid $color-border;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
.p-article__body h3 {
|
|
1064
|
-
font-size: 1.125rem;
|
|
1065
|
-
font-weight: bold;
|
|
1066
|
-
margin-top: $spacing-unit * 4;
|
|
1067
|
-
margin-bottom: $spacing-unit * 2;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
.p-article__body p {
|
|
1071
|
-
margin-bottom: $spacing-unit * 2;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
.p-article__body ul,
|
|
1075
|
-
.p-article__body ol {
|
|
1076
|
-
margin-bottom: $spacing-unit * 2;
|
|
1077
|
-
padding-left: $spacing-unit * 3;
|
|
1078
|
-
list-style: disc;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
.p-article__body ol {
|
|
1082
|
-
list-style: decimal;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
.p-article__footer {
|
|
1086
|
-
margin-top: $spacing-unit * 6;
|
|
1087
|
-
padding-top: $spacing-unit * 4;
|
|
1088
|
-
border-top: 1px solid $color-border;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
@include tablet {
|
|
1092
|
-
.p-article {
|
|
1093
|
-
padding: $spacing-unit * 6 $spacing-unit * 4;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
.p-article__title {
|
|
1097
|
-
font-size: 2rem;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
`);
|
|
1101
|
-
writeFile(s('project/_p-about.scss'), `@use '../foundation/variables' as *;
|
|
1102
|
-
@use '../foundation/mixins' as *;
|
|
1103
|
-
|
|
1104
|
-
.p-about {
|
|
1105
|
-
padding: $spacing-unit * 6 $spacing-unit * 2;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
.p-about__inner {
|
|
1109
|
-
max-width: $max-width;
|
|
1110
|
-
margin: 0 auto;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
.p-about__content {
|
|
1114
|
-
margin-top: $spacing-unit * 4;
|
|
1115
|
-
line-height: 1.8;
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
.p-about__content p {
|
|
1119
|
-
margin-bottom: $spacing-unit * 2;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
.p-about__table {
|
|
1123
|
-
margin-top: $spacing-unit * 6;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
@include tablet {
|
|
1127
|
-
.p-about {
|
|
1128
|
-
padding: $spacing-unit * 8 $spacing-unit * 4;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
`);
|
|
1132
|
-
writeFile(s('project/_p-contact.scss'), `@use '../foundation/variables' as *;
|
|
1133
|
-
@use '../foundation/mixins' as *;
|
|
1134
|
-
|
|
1135
|
-
.p-contact {
|
|
1136
|
-
padding: $spacing-unit * 6 $spacing-unit * 2;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
.p-contact__inner {
|
|
1140
|
-
max-width: 640px;
|
|
1141
|
-
margin: 0 auto;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
.p-contact__lead {
|
|
1145
|
-
margin-top: $spacing-unit * 2;
|
|
1146
|
-
color: $color-text-light;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
.p-contact__form {
|
|
1150
|
-
margin-top: $spacing-unit * 4;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
.p-contact__field {
|
|
1154
|
-
margin-bottom: $spacing-unit * 3;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
.p-contact__label {
|
|
1158
|
-
display: block;
|
|
1159
|
-
font-size: 0.875rem;
|
|
1160
|
-
font-weight: bold;
|
|
1161
|
-
margin-bottom: $spacing-unit;
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
.p-contact__input {
|
|
1165
|
-
display: block;
|
|
1166
|
-
width: 100%;
|
|
1167
|
-
padding: $spacing-unit * 1.5;
|
|
1168
|
-
border: 1px solid $color-border;
|
|
1169
|
-
border-radius: 4px;
|
|
1170
|
-
font-size: 1rem;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
.p-contact__input:focus {
|
|
1174
|
-
outline: none;
|
|
1175
|
-
border-color: $color-primary;
|
|
1176
|
-
box-shadow: 0 0 0 2px rgba($color-primary, 0.2);
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
.p-contact__textarea {
|
|
1180
|
-
display: block;
|
|
1181
|
-
width: 100%;
|
|
1182
|
-
padding: $spacing-unit * 1.5;
|
|
1183
|
-
border: 1px solid $color-border;
|
|
1184
|
-
border-radius: 4px;
|
|
1185
|
-
font-size: 1rem;
|
|
1186
|
-
resize: vertical;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
.p-contact__textarea:focus {
|
|
1190
|
-
outline: none;
|
|
1191
|
-
border-color: $color-primary;
|
|
1192
|
-
box-shadow: 0 0 0 2px rgba($color-primary, 0.2);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
.p-contact__actions {
|
|
1196
|
-
margin-top: $spacing-unit * 4;
|
|
1197
|
-
text-align: center;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
@include tablet {
|
|
1201
|
-
.p-contact {
|
|
1202
|
-
padding: $spacing-unit * 8 $spacing-unit * 4;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
`);
|
|
1206
|
-
}
|
|
1207
|
-
// ========== TypeScript ==========
|
|
1208
|
-
function generateTypeScript(projectDir) {
|
|
1209
|
-
const ts = (...p) => path.join(projectDir, 'frelio-data/site/templates/common/scripts', ...p);
|
|
1210
|
-
writeFile(ts('features/mobile-nav.ts'), `export function initMobileNav(): void {
|
|
1211
|
-
const toggle = document.querySelector<HTMLButtonElement>('.c-nav__toggle')
|
|
1212
|
-
const list = document.querySelector<HTMLUListElement>('.c-nav__list')
|
|
1213
|
-
|
|
1214
|
-
if (!toggle || !list) return
|
|
1215
|
-
|
|
1216
|
-
toggle.addEventListener('click', () => {
|
|
1217
|
-
const isOpen = list.classList.contains('c-nav__list--open')
|
|
1218
|
-
list.classList.toggle('c-nav__list--open', !isOpen)
|
|
1219
|
-
toggle.setAttribute('aria-expanded', String(!isOpen))
|
|
1220
|
-
})
|
|
1221
|
-
|
|
1222
|
-
document.addEventListener('click', (e) => {
|
|
1223
|
-
const target = e.target as HTMLElement
|
|
1224
|
-
if (!target.closest('.c-nav')) {
|
|
1225
|
-
list.classList.remove('c-nav__list--open')
|
|
1226
|
-
toggle.setAttribute('aria-expanded', 'false')
|
|
1227
|
-
}
|
|
1228
|
-
})
|
|
1229
|
-
}
|
|
1230
|
-
`);
|
|
1231
|
-
writeFile(ts('features/smooth-scroll.ts'), `export function initSmoothScroll(): void {
|
|
1232
|
-
document.addEventListener('click', (e) => {
|
|
1233
|
-
const target = e.target as HTMLElement
|
|
1234
|
-
const anchor = target.closest<HTMLAnchorElement>('a[href^="#"]')
|
|
1235
|
-
|
|
1236
|
-
if (!anchor) return
|
|
1237
|
-
|
|
1238
|
-
const id = anchor.getAttribute('href')
|
|
1239
|
-
if (!id || id === '#') return
|
|
1240
|
-
|
|
1241
|
-
const element = document.querySelector(id)
|
|
1242
|
-
if (!element) return
|
|
1243
|
-
|
|
1244
|
-
e.preventDefault()
|
|
1245
|
-
element.scrollIntoView({ behavior: 'smooth' })
|
|
1246
|
-
})
|
|
1247
|
-
}
|
|
1248
|
-
`);
|
|
1249
|
-
}
|
|
1250
|
-
// ========== Entry Points ==========
|
|
1251
|
-
function generateEntries(projectDir) {
|
|
1252
|
-
const t = (...p) => path.join(projectDir, 'frelio-data/site/templates', ...p);
|
|
1253
|
-
// common
|
|
1254
|
-
writeFile(t('common/styles/index.scss'), `// Foundation
|
|
1255
|
-
@use 'foundation/variables' as *;
|
|
1256
|
-
@use 'foundation/mixins' as *;
|
|
1257
|
-
@use 'foundation/reset';
|
|
1258
|
-
|
|
1259
|
-
// Layout
|
|
1260
|
-
@use 'layout/l-header';
|
|
1261
|
-
@use 'layout/l-footer';
|
|
1262
|
-
@use 'layout/l-main';
|
|
1263
|
-
|
|
1264
|
-
// Component
|
|
1265
|
-
@use 'component/c-logo';
|
|
1266
|
-
@use 'component/c-nav';
|
|
1267
|
-
@use 'component/c-btn';
|
|
1268
|
-
@use 'component/c-card';
|
|
1269
|
-
@use 'component/c-table';
|
|
1270
|
-
|
|
1271
|
-
// Element
|
|
1272
|
-
@use 'element/e-heading';
|
|
1273
|
-
`);
|
|
1274
|
-
writeFile(t('common/scripts/index.ts'), `import '../styles/index.scss'
|
|
1275
|
-
import { initMobileNav } from './features/mobile-nav'
|
|
1276
|
-
import { initSmoothScroll } from './features/smooth-scroll'
|
|
1277
|
-
|
|
1278
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
1279
|
-
initMobileNav()
|
|
1280
|
-
initSmoothScroll()
|
|
1281
|
-
})
|
|
1282
|
-
`);
|
|
1283
|
-
// home (root level — / maps to templates root)
|
|
1284
|
-
writeFile(t('styles/index.scss'), `@use 'foundation/variables' as *;
|
|
1285
|
-
@use 'foundation/mixins' as *;
|
|
1286
|
-
|
|
1287
|
-
@use 'project/p-hero';
|
|
1288
|
-
@use 'project/p-news-list';
|
|
1289
|
-
`);
|
|
1290
|
-
writeFile(t('scripts/index.ts'), `import '../styles/index.scss'
|
|
1291
|
-
`);
|
|
1292
|
-
// about
|
|
1293
|
-
writeFile(t('about/styles/index.scss'), `@use 'foundation/variables' as *;
|
|
1294
|
-
@use 'foundation/mixins' as *;
|
|
1295
|
-
|
|
1296
|
-
@use 'project/p-about';
|
|
1297
|
-
`);
|
|
1298
|
-
writeFile(t('about/scripts/index.ts'), `import '../styles/index.scss'
|
|
1299
|
-
`);
|
|
1300
|
-
// contact
|
|
1301
|
-
writeFile(t('contact/styles/index.scss'), `@use 'foundation/variables' as *;
|
|
1302
|
-
@use 'foundation/mixins' as *;
|
|
1303
|
-
|
|
1304
|
-
@use 'project/p-contact';
|
|
1305
|
-
`);
|
|
1306
|
-
writeFile(t('contact/scripts/index.ts'), `import '../styles/index.scss'
|
|
1307
|
-
`);
|
|
1308
|
-
// news (list + detail share the same scripts/styles)
|
|
1309
|
-
writeFile(t('news/styles/index.scss'), `@use 'foundation/variables' as *;
|
|
1310
|
-
@use 'foundation/mixins' as *;
|
|
1311
|
-
|
|
1312
|
-
@use 'project/p-news-list';
|
|
1313
|
-
@use 'project/p-article';
|
|
1314
|
-
`);
|
|
1315
|
-
writeFile(t('news/scripts/index.ts'), `import '../styles/index.scss'
|
|
1316
|
-
`);
|
|
1317
|
-
}
|
|
1318
|
-
// ========== Build Scripts ==========
|
|
1319
|
-
function generateBuildScripts(projectDir) {
|
|
1320
|
-
const s = (...p) => path.join(projectDir, 'scripts', ...p);
|
|
1321
|
-
writeFile(s('generate-data-json.ts'), `/**
|
|
1322
|
-
* FrelioDataJson 生成スクリプト
|
|
1323
|
-
*
|
|
1324
|
-
* FrelioBuildDataRecipe に従って FrelioDataJson を生成し、
|
|
1325
|
-
* frelio-data/site/data/data-json/ に出力する。
|
|
1326
|
-
*
|
|
1327
|
-
* @example
|
|
1328
|
-
* # 差分ビルド(デフォルト)
|
|
1329
|
-
* npx tsx scripts/generate-data-json.ts
|
|
1330
|
-
*
|
|
1331
|
-
* # フルリビルド
|
|
1332
|
-
* npx tsx scripts/generate-data-json.ts --full-rebuild
|
|
1333
|
-
*
|
|
1334
|
-
* # ドライラン
|
|
1335
|
-
* npx tsx scripts/generate-data-json.ts --dry-run
|
|
1336
|
-
*/
|
|
1337
|
-
|
|
1338
|
-
import {
|
|
1339
|
-
generateDataJson,
|
|
1340
|
-
NodeFileSystem,
|
|
1341
|
-
getGitDiff,
|
|
1342
|
-
type GenerateDataJsonOptions,
|
|
1343
|
-
} from '@c-time/frelio-data-json-generator'
|
|
1344
|
-
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from 'fs'
|
|
1345
|
-
import { dirname, join } from 'path'
|
|
1346
|
-
import { parseArgs } from 'util'
|
|
1347
|
-
|
|
1348
|
-
const { values } = parseArgs({
|
|
1349
|
-
options: {
|
|
1350
|
-
'diff-range': { type: 'string', default: 'origin/main...HEAD' },
|
|
1351
|
-
'full-rebuild': { type: 'boolean', default: false },
|
|
1352
|
-
'dry-run': { type: 'boolean', default: false },
|
|
1353
|
-
'log-level': { type: 'string', default: 'info' },
|
|
1354
|
-
},
|
|
1355
|
-
})
|
|
1356
|
-
|
|
1357
|
-
const options: GenerateDataJsonOptions = {
|
|
1358
|
-
fullRebuild: values['full-rebuild'],
|
|
1359
|
-
dryRun: values['dry-run'],
|
|
1360
|
-
logLevel: values['log-level'] as 'debug' | 'info' | 'quiet',
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
const CONTENT_ROOT = 'frelio-data/site'
|
|
1364
|
-
const OUTPUT_ROOT = 'frelio-data/site/data/data-json'
|
|
1365
|
-
const REPORT_PATH = 'frelio-data/site/data/report.json'
|
|
1366
|
-
const RECIPE_PATH = 'frelio-data/admin/recipes/build-data-recipe.json'
|
|
1367
|
-
const DEPENDENCY_MAP_PATH = 'frelio-data/site/data/_dependency-map.json'
|
|
1368
|
-
|
|
1369
|
-
async function main(): Promise<void> {
|
|
1370
|
-
if (!existsSync(RECIPE_PATH)) {
|
|
1371
|
-
console.error(\`Recipe not found: \${RECIPE_PATH}\`)
|
|
1372
|
-
process.exit(1)
|
|
1373
|
-
}
|
|
1374
|
-
const recipe = JSON.parse(readFileSync(RECIPE_PATH, 'utf-8'))
|
|
1375
|
-
|
|
1376
|
-
if (!existsSync(DEPENDENCY_MAP_PATH)) {
|
|
1377
|
-
console.error(\`Dependency map not found: \${DEPENDENCY_MAP_PATH}\`)
|
|
1378
|
-
process.exit(1)
|
|
1379
|
-
}
|
|
1380
|
-
const dependencyMap = JSON.parse(readFileSync(DEPENDENCY_MAP_PATH, 'utf-8'))
|
|
1381
|
-
|
|
1382
|
-
const gitDiff = options.fullRebuild
|
|
1383
|
-
? { added: [], modified: [], deleted: [] }
|
|
1384
|
-
: await getGitDiff(values['diff-range']!)
|
|
1385
|
-
|
|
1386
|
-
if (options.logLevel !== 'quiet') {
|
|
1387
|
-
console.log(\`Mode: \${options.fullRebuild ? 'full-rebuild' : 'incremental'}\`)
|
|
1388
|
-
console.log(\`Dry run: \${options.dryRun}\`)
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
const result = await generateDataJson({
|
|
1392
|
-
recipe,
|
|
1393
|
-
dependencyMap,
|
|
1394
|
-
gitDiff,
|
|
1395
|
-
fileSystem: new NodeFileSystem(),
|
|
1396
|
-
contentRootPath: CONTENT_ROOT,
|
|
1397
|
-
outputRootPath: OUTPUT_ROOT,
|
|
1398
|
-
options,
|
|
1399
|
-
})
|
|
1400
|
-
|
|
1401
|
-
if (!options.dryRun) {
|
|
1402
|
-
mkdirSync(OUTPUT_ROOT, { recursive: true })
|
|
1403
|
-
for (const output of result.outputs) {
|
|
1404
|
-
if (output.content.type === 'delete') {
|
|
1405
|
-
const fullPath = join(OUTPUT_ROOT, output.path)
|
|
1406
|
-
if (existsSync(fullPath)) unlinkSync(fullPath)
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
for (const output of result.outputs) {
|
|
1410
|
-
if (output.content.type !== 'delete') {
|
|
1411
|
-
const fullPath = join(OUTPUT_ROOT, output.path)
|
|
1412
|
-
mkdirSync(dirname(fullPath), { recursive: true })
|
|
1413
|
-
writeFileSync(fullPath, JSON.stringify(output.content, null, 2))
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
mkdirSync(dirname(REPORT_PATH), { recursive: true })
|
|
1419
|
-
writeFileSync(REPORT_PATH, JSON.stringify(result.report, null, 2))
|
|
1420
|
-
|
|
1421
|
-
const { stats } = result.report
|
|
1422
|
-
console.log(\`\\n=== Summary ===\`)
|
|
1423
|
-
console.log(\`Updated: \${stats.updated}\`)
|
|
1424
|
-
console.log(\`Deleted: \${stats.deleted}\`)
|
|
1425
|
-
console.log(\`Skipped: \${stats.skipped}\`)
|
|
1426
|
-
console.log(\`Errors: \${stats.errors}\`)
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
main().catch((error) => {
|
|
1430
|
-
console.error('Fatal error:', error)
|
|
1431
|
-
process.exit(1)
|
|
1432
|
-
})
|
|
1433
|
-
`);
|
|
1434
|
-
writeFile(s('generate-html.ts'), `/**
|
|
1435
|
-
* HTML 生成スクリプト
|
|
1436
|
-
*
|
|
1437
|
-
* FrelioDataJson → HTML の変換(ビルドパイプライン Phase 2)
|
|
1438
|
-
*
|
|
1439
|
-
* @example
|
|
1440
|
-
* npx tsx scripts/generate-html.ts
|
|
1441
|
-
* npx tsx scripts/generate-html.ts --dry-run
|
|
1442
|
-
*/
|
|
1443
|
-
|
|
1444
|
-
import {
|
|
1445
|
-
generateHtml,
|
|
1446
|
-
NodeFileSystem,
|
|
1447
|
-
type GenerateHtmlOptions,
|
|
1448
|
-
} from '@c-time/frelio-gentl'
|
|
1449
|
-
import type { FrelioDataJson } from '@c-time/frelio-data-json'
|
|
1450
|
-
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, readdirSync } from 'fs'
|
|
1451
|
-
import { dirname, join, extname } from 'path'
|
|
1452
|
-
import { parseArgs } from 'util'
|
|
1453
|
-
|
|
1454
|
-
const { values } = parseArgs({
|
|
1455
|
-
options: {
|
|
1456
|
-
'dry-run': { type: 'boolean', default: false },
|
|
1457
|
-
'log-level': { type: 'string', default: 'info' },
|
|
1458
|
-
},
|
|
1459
|
-
})
|
|
1460
|
-
|
|
1461
|
-
const options: GenerateHtmlOptions = {
|
|
1462
|
-
logLevel: values['log-level'] as 'debug' | 'info' | 'quiet',
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
const DATA_JSON_ROOT = 'frelio-data/site/data/data-json'
|
|
1466
|
-
const TEMPLATE_ROOT = 'frelio-data/site/templates'
|
|
1467
|
-
const OUTPUT_ROOT = 'public'
|
|
1468
|
-
const REPORT_PATH = 'frelio-data/site/data/html-report.json'
|
|
1469
|
-
|
|
1470
|
-
function collectJsonFiles(dir: string): string[] {
|
|
1471
|
-
if (!existsSync(dir)) return []
|
|
1472
|
-
const files: string[] = []
|
|
1473
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1474
|
-
const fullPath = join(dir, entry.name)
|
|
1475
|
-
if (entry.isDirectory()) {
|
|
1476
|
-
files.push(...collectJsonFiles(fullPath))
|
|
1477
|
-
} else if (extname(entry.name) === '.json') {
|
|
1478
|
-
files.push(fullPath)
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
return files
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
async function main(): Promise<void> {
|
|
1485
|
-
if (!existsSync(DATA_JSON_ROOT)) {
|
|
1486
|
-
console.error(\`Data JSON directory not found: \${DATA_JSON_ROOT}\`)
|
|
1487
|
-
process.exit(1)
|
|
1488
|
-
}
|
|
1489
|
-
if (!existsSync(TEMPLATE_ROOT)) {
|
|
1490
|
-
console.error(\`Template directory not found: \${TEMPLATE_ROOT}\`)
|
|
1491
|
-
process.exit(1)
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
const jsonFiles = collectJsonFiles(DATA_JSON_ROOT)
|
|
1495
|
-
if (jsonFiles.length === 0) {
|
|
1496
|
-
console.log('No data JSON files found. Nothing to generate.')
|
|
1497
|
-
return
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const dataJsons: FrelioDataJson[] = jsonFiles.map(filePath => {
|
|
1501
|
-
return JSON.parse(readFileSync(filePath, 'utf-8'))
|
|
1502
|
-
})
|
|
1503
|
-
|
|
1504
|
-
console.log(\`Found \${dataJsons.length} data JSON files\`)
|
|
1505
|
-
|
|
1506
|
-
const result = await generateHtml({
|
|
1507
|
-
dataJsons,
|
|
1508
|
-
templateRootPath: TEMPLATE_ROOT,
|
|
1509
|
-
fileSystem: new NodeFileSystem(),
|
|
1510
|
-
options,
|
|
1511
|
-
})
|
|
1512
|
-
|
|
1513
|
-
if (!values['dry-run']) {
|
|
1514
|
-
mkdirSync(OUTPUT_ROOT, { recursive: true })
|
|
1515
|
-
for (const output of result.outputs) {
|
|
1516
|
-
if (output.status === 'deleted') {
|
|
1517
|
-
const fullPath = join(OUTPUT_ROOT, output.outputPath)
|
|
1518
|
-
if (existsSync(fullPath)) unlinkSync(fullPath)
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
for (const output of result.outputs) {
|
|
1522
|
-
if (output.status === 'created') {
|
|
1523
|
-
const fullPath = join(OUTPUT_ROOT, output.outputPath)
|
|
1524
|
-
mkdirSync(dirname(fullPath), { recursive: true })
|
|
1525
|
-
writeFileSync(fullPath, output.html)
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
mkdirSync(dirname(REPORT_PATH), { recursive: true })
|
|
1531
|
-
writeFileSync(REPORT_PATH, JSON.stringify(result.report, null, 2))
|
|
1532
|
-
|
|
1533
|
-
const { stats } = result.report
|
|
1534
|
-
console.log(\`\\n=== Summary ===\`)
|
|
1535
|
-
console.log(\`Created: \${stats.created}\`)
|
|
1536
|
-
console.log(\`Deleted: \${stats.deleted}\`)
|
|
1537
|
-
console.log(\`Errors: \${stats.errors}\`)
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
main().catch((error) => {
|
|
1541
|
-
console.error('Fatal error:', error)
|
|
1542
|
-
process.exit(1)
|
|
1543
|
-
})
|
|
1544
|
-
`);
|
|
1545
|
-
writeFile(s('generate-sitemap.ts'), `/**
|
|
1546
|
-
* sitemap.xml 生成スクリプト
|
|
1547
|
-
*
|
|
1548
|
-
* public/ 配下の HTML ファイルを走査して sitemap.xml を生成する。
|
|
1549
|
-
*
|
|
1550
|
-
* @example
|
|
1551
|
-
* npx tsx scripts/generate-sitemap.ts --base-url https://example.com
|
|
1552
|
-
* npx tsx scripts/generate-sitemap.ts --full-rebuild --base-url https://example.com
|
|
1553
|
-
*/
|
|
1554
|
-
|
|
1555
|
-
import { execSync } from 'child_process'
|
|
1556
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs'
|
|
1557
|
-
import { dirname, join } from 'path'
|
|
1558
|
-
import { parseArgs } from 'util'
|
|
1559
|
-
|
|
1560
|
-
const { values } = parseArgs({
|
|
1561
|
-
options: {
|
|
1562
|
-
'base-url': { type: 'string' },
|
|
1563
|
-
'diff-range': { type: 'string', default: 'origin/main...HEAD' },
|
|
1564
|
-
'full-rebuild': { type: 'boolean', default: false },
|
|
1565
|
-
'dry-run': { type: 'boolean', default: false },
|
|
1566
|
-
'log-level': { type: 'string', default: 'info' },
|
|
1567
|
-
},
|
|
1568
|
-
})
|
|
1569
|
-
|
|
1570
|
-
const baseUrl = (values['base-url'] || process.env.SITE_BASE_URL || '').replace(/\\/$/, '')
|
|
1571
|
-
if (!baseUrl) {
|
|
1572
|
-
console.error('Error: --base-url or SITE_BASE_URL is required.')
|
|
1573
|
-
process.exit(1)
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const logLevel = values['log-level'] as 'debug' | 'info' | 'quiet'
|
|
1577
|
-
|
|
1578
|
-
const HTML_ROOT = 'public'
|
|
1579
|
-
const OUTPUT_PATH = 'public/sitemap.xml'
|
|
1580
|
-
|
|
1581
|
-
interface UrlEntry {
|
|
1582
|
-
loc: string
|
|
1583
|
-
lastmod: string | null
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
function htmlPathToUrlPath(htmlPath: string): string {
|
|
1587
|
-
const relative = htmlPath
|
|
1588
|
-
.replace(/\\\\\\\\/g, '/')
|
|
1589
|
-
.replace(/^public\\//, '')
|
|
1590
|
-
.replace(/\\/index\\.html$/, '/')
|
|
1591
|
-
return relative === 'index.html' ? '/' : '/' + relative
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
function hasNoindex(htmlPath: string): boolean {
|
|
1595
|
-
const html = readFileSync(htmlPath, 'utf-8')
|
|
1596
|
-
return /<meta\\s[^>]*name\\s*=\\s*["']robots["'][^>]*content\\s*=\\s*["'][^"']*noindex[^"']*["'][^>]*>/i.test(html)
|
|
1597
|
-
|| /<meta\\s[^>]*content\\s*=\\s*["'][^"']*noindex[^"']*["'][^>]*name\\s*=\\s*["']robots["'][^>]*>/i.test(html)
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
function getLastmod(filePath: string): string | null {
|
|
1601
|
-
try {
|
|
1602
|
-
const result = execSync(\`git log -1 --format=%aI -- "\${filePath}"\`, {
|
|
1603
|
-
encoding: 'utf-8',
|
|
1604
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1605
|
-
}).trim()
|
|
1606
|
-
return result || null
|
|
1607
|
-
} catch {
|
|
1608
|
-
return null
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
function collectHtmlFiles(dir: string): string[] {
|
|
1613
|
-
if (!existsSync(dir)) return []
|
|
1614
|
-
const files: string[] = []
|
|
1615
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1616
|
-
const fullPath = join(dir, entry.name)
|
|
1617
|
-
if (entry.isDirectory()) {
|
|
1618
|
-
files.push(...collectHtmlFiles(fullPath))
|
|
1619
|
-
} else if (entry.name === 'index.html') {
|
|
1620
|
-
files.push(fullPath)
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
return files
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
function parseSitemap(xml: string): Map<string, UrlEntry> {
|
|
1627
|
-
const entries = new Map<string, UrlEntry>()
|
|
1628
|
-
const urlBlockRe = /<url>([\\s\\S]*?)<\\/url>/g
|
|
1629
|
-
const locRe = /<loc>([\\s\\S]*?)<\\/loc>/
|
|
1630
|
-
const lastmodRe = /<lastmod>([\\s\\S]*?)<\\/lastmod>/
|
|
1631
|
-
let match: RegExpExecArray | null
|
|
1632
|
-
while ((match = urlBlockRe.exec(xml)) !== null) {
|
|
1633
|
-
const block = match[1]
|
|
1634
|
-
const locMatch = locRe.exec(block)
|
|
1635
|
-
if (!locMatch) continue
|
|
1636
|
-
const loc = locMatch[1].trim()
|
|
1637
|
-
const lastmodMatch = lastmodRe.exec(block)
|
|
1638
|
-
entries.set(loc, { loc, lastmod: lastmodMatch ? lastmodMatch[1].trim() : null })
|
|
1639
|
-
}
|
|
1640
|
-
return entries
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
function buildSitemapXml(entries: Map<string, UrlEntry>): string {
|
|
1644
|
-
const sorted = [...entries.values()].sort((a, b) => a.loc.localeCompare(b.loc))
|
|
1645
|
-
const urlElements = sorted.map((entry) => {
|
|
1646
|
-
const lastmodLine = entry.lastmod ? \`\\n <lastmod>\${entry.lastmod}</lastmod>\` : ''
|
|
1647
|
-
return \` <url>\\n <loc>\${entry.loc}</loc>\${lastmodLine}\\n </url>\`
|
|
1648
|
-
})
|
|
1649
|
-
return [
|
|
1650
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1651
|
-
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
1652
|
-
...urlElements,
|
|
1653
|
-
'</urlset>',
|
|
1654
|
-
'',
|
|
1655
|
-
].join('\\n')
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
function createEntry(htmlPath: string): UrlEntry | null {
|
|
1659
|
-
if (hasNoindex(htmlPath)) return null
|
|
1660
|
-
return { loc: baseUrl + htmlPathToUrlPath(htmlPath), lastmod: getLastmod(htmlPath) }
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
function main(): void {
|
|
1664
|
-
if (logLevel !== 'quiet') {
|
|
1665
|
-
console.log(\`Mode: \${values['full-rebuild'] ? 'full-rebuild' : 'incremental'}\`)
|
|
1666
|
-
console.log(\`Dry run: \${values['dry-run']}\`)
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
const entries = new Map<string, UrlEntry>()
|
|
1670
|
-
const htmlFiles = collectHtmlFiles(HTML_ROOT)
|
|
1671
|
-
for (const htmlPath of htmlFiles) {
|
|
1672
|
-
const entry = createEntry(htmlPath)
|
|
1673
|
-
if (entry) entries.set(entry.loc, entry)
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
const xml = buildSitemapXml(entries)
|
|
1677
|
-
|
|
1678
|
-
if (!values['dry-run']) {
|
|
1679
|
-
mkdirSync(dirname(OUTPUT_PATH), { recursive: true })
|
|
1680
|
-
writeFileSync(OUTPUT_PATH, xml)
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
if (logLevel !== 'quiet') {
|
|
1684
|
-
console.log(\`Total URLs: \${entries.size}\`)
|
|
1685
|
-
console.log(\`Output: \${OUTPUT_PATH}\`)
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
main()
|
|
1690
|
-
`);
|
|
1691
|
-
writeFile(s('generate-dependency-map.ts'), `/**
|
|
1692
|
-
* 依存マップ生成スクリプト
|
|
1693
|
-
*
|
|
1694
|
-
* FrelioBuildDataRecipe から FrelioDependencyMap を生成する。
|
|
1695
|
-
*
|
|
1696
|
-
* @example
|
|
1697
|
-
* npx tsx scripts/generate-dependency-map.ts
|
|
1698
|
-
*/
|
|
1699
|
-
|
|
1700
|
-
import { convertRecipeToDependencyMap } from '@c-time/frelio-data-json-recipe-to-dependency-map'
|
|
1701
|
-
import { validateSiteRecipe, formatZodErrors } from '@c-time/frelio-types/schemas'
|
|
1702
|
-
import { isFrelioDependencyMap } from '@c-time/frelio-dependency-map'
|
|
1703
|
-
import type { FrelioBuildDataRecipe } from '@c-time/frelio-data-json-recipe'
|
|
1704
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
1705
|
-
import { dirname } from 'path'
|
|
1706
|
-
|
|
1707
|
-
const RECIPE_PATH = 'frelio-data/admin/recipes/build-data-recipe.json'
|
|
1708
|
-
const OUTPUT_PATH = 'frelio-data/site/data/_dependency-map.json'
|
|
1709
|
-
|
|
1710
|
-
const raw = JSON.parse(readFileSync(RECIPE_PATH, 'utf-8'))
|
|
1711
|
-
|
|
1712
|
-
const result = validateSiteRecipe(raw)
|
|
1713
|
-
if (!result.success) {
|
|
1714
|
-
console.error('Invalid recipe:', formatZodErrors(result.errors))
|
|
1715
|
-
process.exit(1)
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
const recipe: FrelioBuildDataRecipe = result.data
|
|
1719
|
-
const dependencyMap = convertRecipeToDependencyMap(recipe)
|
|
1720
|
-
|
|
1721
|
-
if (!isFrelioDependencyMap(dependencyMap)) {
|
|
1722
|
-
console.error('Generated dependency map is invalid')
|
|
1723
|
-
process.exit(1)
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
mkdirSync(dirname(OUTPUT_PATH), { recursive: true })
|
|
1727
|
-
writeFileSync(OUTPUT_PATH, JSON.stringify(dependencyMap, null, 2))
|
|
1728
|
-
console.log(\`Dependency map written to \${OUTPUT_PATH}\`)
|
|
1729
|
-
`);
|
|
1730
|
-
writeFile(s('rebuild-indexes.ts'), `/**
|
|
1731
|
-
* インデックス・ダッシュボードメタデータ一括再構築スクリプト
|
|
1732
|
-
*
|
|
1733
|
-
* @example
|
|
1734
|
-
* npx tsx scripts/rebuild-indexes.ts
|
|
1735
|
-
* npx tsx scripts/rebuild-indexes.ts --dry-run
|
|
1736
|
-
*/
|
|
1737
|
-
|
|
1738
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs'
|
|
1739
|
-
import { dirname } from 'path'
|
|
1740
|
-
import { parseArgs } from 'util'
|
|
1741
|
-
import {
|
|
1742
|
-
rebuildAllIndexes,
|
|
1743
|
-
calcMetadataOnSave,
|
|
1744
|
-
SITE_CONTENTS,
|
|
1745
|
-
CONTENT_TYPE_LIST_PATH,
|
|
1746
|
-
DASHBOARD_METADATA_PATH,
|
|
1747
|
-
type ContentStorePort,
|
|
1748
|
-
type ContentData,
|
|
1749
|
-
type FileChange,
|
|
1750
|
-
type BasePath,
|
|
1751
|
-
} from '@c-time/frelio-content-ops'
|
|
1752
|
-
import type { DashboardMetadata } from '@c-time/frelio-types'
|
|
1753
|
-
|
|
1754
|
-
const { values } = parseArgs({
|
|
1755
|
-
options: {
|
|
1756
|
-
'dry-run': { type: 'boolean', default: false },
|
|
1757
|
-
'log-level': { type: 'string', default: 'info' },
|
|
1758
|
-
},
|
|
1759
|
-
})
|
|
1760
|
-
|
|
1761
|
-
const DRY_RUN = values['dry-run']!
|
|
1762
|
-
const LOG_LEVEL = values['log-level'] as 'debug' | 'info' | 'quiet'
|
|
1763
|
-
|
|
1764
|
-
function logInfo(...args: unknown[]) {
|
|
1765
|
-
if (LOG_LEVEL !== 'quiet') console.log('[info]', ...args)
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
function createFsContentStore(): ContentStorePort {
|
|
1769
|
-
return {
|
|
1770
|
-
async readJson<T>(path: string): Promise<T | null> {
|
|
1771
|
-
if (!existsSync(path)) return null
|
|
1772
|
-
try {
|
|
1773
|
-
return JSON.parse(readFileSync(path, 'utf-8')) as T
|
|
1774
|
-
} catch {
|
|
1775
|
-
return null
|
|
1776
|
-
}
|
|
1777
|
-
},
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
function applyFileChanges(changes: FileChange[]) {
|
|
1782
|
-
for (const change of changes) {
|
|
1783
|
-
mkdirSync(dirname(change.path), { recursive: true })
|
|
1784
|
-
writeFileSync(change.path, change.content + '\\n')
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
async function main() {
|
|
1789
|
-
const store = createFsContentStore()
|
|
1790
|
-
const listRaw = await store.readJson<{ contentTypes: { id: string }[] }>(CONTENT_TYPE_LIST_PATH)
|
|
1791
|
-
if (!listRaw) {
|
|
1792
|
-
console.error(\`Content type list not found: \${CONTENT_TYPE_LIST_PATH}\`)
|
|
1793
|
-
process.exit(1)
|
|
1794
|
-
}
|
|
1795
|
-
const contentTypeIds = listRaw.contentTypes.map((ct) => ct.id)
|
|
1796
|
-
logInfo(\`Content types: \${contentTypeIds.join(', ')}\`)
|
|
1797
|
-
|
|
1798
|
-
const allChanges: FileChange[] = []
|
|
1799
|
-
let dashboardMetadata: DashboardMetadata = { contentTypes: {} }
|
|
1800
|
-
const now = new Date().toISOString()
|
|
1801
|
-
|
|
1802
|
-
for (const contentTypeId of contentTypeIds) {
|
|
1803
|
-
const allContent: { basePath: BasePath; content: ContentData }[] = []
|
|
1804
|
-
for (const basePath of ['published', 'private'] as const) {
|
|
1805
|
-
const dir = \`\${SITE_CONTENTS}/\${basePath}/\${contentTypeId}\`
|
|
1806
|
-
if (!existsSync(dir)) continue
|
|
1807
|
-
const files = readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('_'))
|
|
1808
|
-
for (const file of files) {
|
|
1809
|
-
const content = await store.readJson<ContentData>(\`\${dir}/\${file}\`)
|
|
1810
|
-
if (content) allContent.push({ basePath, content })
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
logInfo(\`\${contentTypeId}: \${allContent.length} content(s) found\`)
|
|
1814
|
-
const indexChanges = await rebuildAllIndexes(store, { contentTypeId, allContent })
|
|
1815
|
-
allChanges.push(...indexChanges)
|
|
1816
|
-
for (const { content } of allContent) {
|
|
1817
|
-
dashboardMetadata = calcMetadataOnSave(
|
|
1818
|
-
dashboardMetadata, contentTypeId, content.updatedBy,
|
|
1819
|
-
null, content.status, now,
|
|
1820
|
-
)
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
allChanges.push({
|
|
1825
|
-
path: DASHBOARD_METADATA_PATH,
|
|
1826
|
-
content: JSON.stringify(dashboardMetadata, null, 2),
|
|
1827
|
-
})
|
|
1828
|
-
|
|
1829
|
-
logInfo(\`Files to write: \${allChanges.length}\`)
|
|
1830
|
-
if (DRY_RUN) {
|
|
1831
|
-
logInfo('Dry run — no files written.')
|
|
1832
|
-
return
|
|
1833
|
-
}
|
|
1834
|
-
applyFileChanges(allChanges)
|
|
1835
|
-
logInfo(\`Done. \${allChanges.length} file(s) written.\`)
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
main().catch((error) => {
|
|
1839
|
-
console.error('Fatal error:', error)
|
|
1840
|
-
process.exit(1)
|
|
1841
|
-
})
|
|
1842
|
-
`);
|
|
1843
|
-
writeFile(s('watch-content.ts'), `/**
|
|
1844
|
-
* コンテンツ変更監視スクリプト
|
|
1845
|
-
*
|
|
1846
|
-
* frelio-data/ 内のコンテンツ JSON の変更を検知し、
|
|
1847
|
-
* ビューインデックスとダッシュボードメタデータを自動更新する。
|
|
1848
|
-
*
|
|
1849
|
-
* @example
|
|
1850
|
-
* npx tsx scripts/watch-content.ts
|
|
1851
|
-
* npx tsx scripts/watch-content.ts --log-level debug
|
|
1852
|
-
*/
|
|
1853
|
-
|
|
1854
|
-
import { watch, readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, readdirSync } from 'fs'
|
|
1855
|
-
import { dirname } from 'path'
|
|
1856
|
-
import { parseArgs } from 'util'
|
|
1857
|
-
import {
|
|
1858
|
-
computeViewIndexUpsert,
|
|
1859
|
-
computeViewIndexRemoval,
|
|
1860
|
-
computeDashboardMetadataOnSave,
|
|
1861
|
-
computeDashboardMetadataOnDelete,
|
|
1862
|
-
rebuildAllIndexes,
|
|
1863
|
-
SITE_CONTENTS,
|
|
1864
|
-
ADMIN_CONTENT_TYPES,
|
|
1865
|
-
type ContentStorePort,
|
|
1866
|
-
type ContentData,
|
|
1867
|
-
type FileChange,
|
|
1868
|
-
type BasePath,
|
|
1869
|
-
} from '@c-time/frelio-content-ops'
|
|
1870
|
-
|
|
1871
|
-
const { values } = parseArgs({
|
|
1872
|
-
options: {
|
|
1873
|
-
'log-level': { type: 'string', default: 'info' },
|
|
1874
|
-
'debounce-ms': { type: 'string', default: '300' },
|
|
1875
|
-
},
|
|
1876
|
-
})
|
|
1877
|
-
|
|
1878
|
-
const LOG_LEVEL = values['log-level'] as 'debug' | 'info' | 'quiet'
|
|
1879
|
-
const DEBOUNCE_MS = Number(values['debounce-ms'])
|
|
1880
|
-
|
|
1881
|
-
function logDebug(...args: unknown[]) {
|
|
1882
|
-
if (LOG_LEVEL === 'debug') console.log('[debug]', ...args)
|
|
1883
|
-
}
|
|
1884
|
-
function logInfo(...args: unknown[]) {
|
|
1885
|
-
if (LOG_LEVEL !== 'quiet') console.log('[info]', ...args)
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
function createFsContentStore(): ContentStorePort {
|
|
1889
|
-
return {
|
|
1890
|
-
async readJson<T>(path: string): Promise<T | null> {
|
|
1891
|
-
if (!existsSync(path)) return null
|
|
1892
|
-
try {
|
|
1893
|
-
return JSON.parse(readFileSync(path, 'utf-8')) as T
|
|
1894
|
-
} catch {
|
|
1895
|
-
return null
|
|
1896
|
-
}
|
|
1897
|
-
},
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
type ContentChangeEvent = {
|
|
1902
|
-
kind: 'content-save' | 'content-delete'
|
|
1903
|
-
basePath: BasePath
|
|
1904
|
-
contentTypeId: string
|
|
1905
|
-
contentId: string
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
type ViewsChangeEvent = {
|
|
1909
|
-
kind: 'views-change'
|
|
1910
|
-
contentTypeId: string
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
type ChangeEvent = ContentChangeEvent | ViewsChangeEvent
|
|
1914
|
-
|
|
1915
|
-
function classifyChange(filePath: string): ChangeEvent | null {
|
|
1916
|
-
const normalized = filePath.replace(/\\\\\\\\/g, '/')
|
|
1917
|
-
const contentMatch = normalized.match(
|
|
1918
|
-
/^frelio-data\\/site\\/contents\\/(published|private)\\/([^/]+)\\/([^/]+)\\.json$/,
|
|
1919
|
-
)
|
|
1920
|
-
if (contentMatch) {
|
|
1921
|
-
const [, basePath, contentTypeId, fileName] = contentMatch
|
|
1922
|
-
if (fileName.startsWith('_')) return null
|
|
1923
|
-
const exists = existsSync(filePath)
|
|
1924
|
-
return {
|
|
1925
|
-
kind: exists ? 'content-save' : 'content-delete',
|
|
1926
|
-
basePath: basePath as BasePath,
|
|
1927
|
-
contentTypeId,
|
|
1928
|
-
contentId: fileName,
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
const viewsMatch = normalized.match(
|
|
1932
|
-
/^frelio-data\\/admin\\/content_types\\/([^/]+)\\.views\\.json$/,
|
|
1933
|
-
)
|
|
1934
|
-
if (viewsMatch) {
|
|
1935
|
-
return { kind: 'views-change', contentTypeId: viewsMatch[1] }
|
|
1936
|
-
}
|
|
1937
|
-
return null
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
function applyFileChanges(changes: FileChange[]) {
|
|
1941
|
-
for (const change of changes) {
|
|
1942
|
-
if (change.delete) {
|
|
1943
|
-
if (existsSync(change.path)) unlinkSync(change.path)
|
|
1944
|
-
} else {
|
|
1945
|
-
mkdirSync(dirname(change.path), { recursive: true })
|
|
1946
|
-
writeFileSync(change.path, change.content + '\\n')
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
const store = createFsContentStore()
|
|
1952
|
-
const suppressedPaths = new Set<string>()
|
|
1953
|
-
|
|
1954
|
-
function applyFileChangesWithSuppress(changes: FileChange[]) {
|
|
1955
|
-
for (const change of changes) {
|
|
1956
|
-
const normalized = change.path.replace(/\\\\\\\\/g, '/')
|
|
1957
|
-
suppressedPaths.add(normalized)
|
|
1958
|
-
setTimeout(() => suppressedPaths.delete(normalized), DEBOUNCE_MS + 200)
|
|
1959
|
-
}
|
|
1960
|
-
applyFileChanges(changes)
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
async function handleContentSave(event: ContentChangeEvent) {
|
|
1964
|
-
const { basePath, contentTypeId, contentId } = event
|
|
1965
|
-
const contentPath = \`\${SITE_CONTENTS}/\${basePath}/\${contentTypeId}/\${contentId}.json\`
|
|
1966
|
-
const content = await store.readJson<ContentData>(contentPath)
|
|
1967
|
-
if (!content) return
|
|
1968
|
-
|
|
1969
|
-
const viewChanges = await computeViewIndexUpsert(store, { basePath, contentTypeId, content })
|
|
1970
|
-
const metaChange = await computeDashboardMetadataOnSave(store, {
|
|
1971
|
-
contentTypeId, updatedBy: content.updatedBy,
|
|
1972
|
-
oldStatus: null, newStatus: content.status,
|
|
1973
|
-
})
|
|
1974
|
-
applyFileChangesWithSuppress([...viewChanges, metaChange])
|
|
1975
|
-
logInfo(\`Content saved: \${basePath}/\${contentTypeId}/\${contentId}\`)
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
async function handleContentDelete(event: ContentChangeEvent) {
|
|
1979
|
-
const { basePath, contentTypeId, contentId } = event
|
|
1980
|
-
const viewChanges = await computeViewIndexRemoval(store, { basePath, contentTypeId, contentId })
|
|
1981
|
-
const metaChange = await computeDashboardMetadataOnDelete(store, {
|
|
1982
|
-
contentTypeId, deletedBy: 'unknown', deletedStatus: 'draft' as ContentData['status'],
|
|
1983
|
-
})
|
|
1984
|
-
applyFileChangesWithSuppress([...viewChanges, metaChange])
|
|
1985
|
-
logInfo(\`Content deleted: \${basePath}/\${contentTypeId}/\${contentId}\`)
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
async function handleViewsChange(event: ViewsChangeEvent) {
|
|
1989
|
-
const { contentTypeId } = event
|
|
1990
|
-
logInfo(\`Views changed for: \${contentTypeId}, rebuilding indexes...\`)
|
|
1991
|
-
const allContent: { basePath: BasePath; content: ContentData }[] = []
|
|
1992
|
-
for (const basePath of ['published', 'private'] as const) {
|
|
1993
|
-
const dir = \`\${SITE_CONTENTS}/\${basePath}/\${contentTypeId}\`
|
|
1994
|
-
if (!existsSync(dir)) continue
|
|
1995
|
-
const files = readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('_'))
|
|
1996
|
-
for (const file of files) {
|
|
1997
|
-
const content = await store.readJson<ContentData>(\`\${dir}/\${file}\`)
|
|
1998
|
-
if (content) allContent.push({ basePath, content })
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
const changes = await rebuildAllIndexes(store, { contentTypeId, allContent })
|
|
2002
|
-
applyFileChangesWithSuppress(changes)
|
|
2003
|
-
logInfo(\`Rebuilt \${changes.length} index file(s) for \${contentTypeId}\`)
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
const pending = new Map<string, NodeJS.Timeout>()
|
|
2007
|
-
let processing = Promise.resolve()
|
|
2008
|
-
|
|
2009
|
-
function enqueue(fn: () => Promise<void>) {
|
|
2010
|
-
processing = processing.then(fn).catch((err) => console.error('Handler error:', err))
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
function scheduleHandler(filePath: string, handler: () => Promise<void>) {
|
|
2014
|
-
const existing = pending.get(filePath)
|
|
2015
|
-
if (existing) clearTimeout(existing)
|
|
2016
|
-
pending.set(filePath, setTimeout(() => {
|
|
2017
|
-
pending.delete(filePath)
|
|
2018
|
-
enqueue(handler)
|
|
2019
|
-
}, DEBOUNCE_MS))
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
async function main() {
|
|
2023
|
-
logInfo('Starting content watcher...')
|
|
2024
|
-
const watchTargets = [
|
|
2025
|
-
{ path: SITE_CONTENTS, label: 'contents' },
|
|
2026
|
-
{ path: ADMIN_CONTENT_TYPES, label: 'admin/content_types' },
|
|
2027
|
-
]
|
|
2028
|
-
|
|
2029
|
-
for (const target of watchTargets) {
|
|
2030
|
-
if (!existsSync(target.path)) {
|
|
2031
|
-
console.error(\`Watch target not found: \${target.path}\`)
|
|
2032
|
-
process.exit(1)
|
|
2033
|
-
}
|
|
2034
|
-
watch(target.path, { recursive: true }, (_eventType, fileName) => {
|
|
2035
|
-
if (!fileName) return
|
|
2036
|
-
const fullPath = \`\${target.path}/\${fileName.replace(/\\\\\\\\/g, '/')}\`
|
|
2037
|
-
if (!fullPath.endsWith('.json')) return
|
|
2038
|
-
if (suppressedPaths.has(fullPath)) return
|
|
2039
|
-
const event = classifyChange(fullPath)
|
|
2040
|
-
if (!event) return
|
|
2041
|
-
scheduleHandler(fullPath, async () => {
|
|
2042
|
-
const latestEvent = classifyChange(fullPath)
|
|
2043
|
-
if (!latestEvent) return
|
|
2044
|
-
switch (latestEvent.kind) {
|
|
2045
|
-
case 'content-save': await handleContentSave(latestEvent); break
|
|
2046
|
-
case 'content-delete': await handleContentDelete(latestEvent); break
|
|
2047
|
-
case 'views-change': await handleViewsChange(latestEvent); break
|
|
2048
|
-
}
|
|
2049
|
-
})
|
|
2050
|
-
})
|
|
2051
|
-
logInfo(\`Watching: \${target.path}\`)
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
logInfo('Ready. Press Ctrl+C to stop.')
|
|
2055
|
-
process.on('SIGINT', () => {
|
|
2056
|
-
logInfo('\\nShutting down...')
|
|
2057
|
-
for (const timer of pending.values()) clearTimeout(timer)
|
|
2058
|
-
process.exit(0)
|
|
2059
|
-
})
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
main().catch((error) => {
|
|
2063
|
-
console.error('Fatal error:', error)
|
|
2064
|
-
process.exit(1)
|
|
2065
|
-
})
|
|
2066
|
-
`);
|
|
2067
|
-
}
|
|
2068
|
-
// ========== CLAUDE.md ==========
|
|
2069
|
-
function generateClaudeMd(projectDir) {
|
|
2070
|
-
writeFile(path.join(projectDir, 'CLAUDE.md'), `# CLAUDE.md
|
|
2071
|
-
|
|
2072
|
-
Frelio(ヘッドレス CMS)で構築されたサイトリポジトリ。
|
|
2073
|
-
お知らせブログ付きのシンプルなコーポレートサイト。
|
|
2074
|
-
|
|
2075
|
-
## プロジェクト構成
|
|
2076
|
-
|
|
2077
|
-
- \`frelio-data/\` — CMS データ(コンテンツタイプ、コンテンツ、テンプレート、レシピ)
|
|
2078
|
-
- \`site/templates/\` — テンプレート(配置 = 出力先 URL 構造)
|
|
2079
|
-
- \`site/templates/common/styles/\` — 共有 SCSS パーシャル(FLOCSS 亜種)
|
|
2080
|
-
- \`site/templates/common/scripts/\` — 共有 TypeScript(features/)
|
|
2081
|
-
- \`site/data/data-json/\` — SSG 中間データ(git 追跡対象)
|
|
2082
|
-
- \`public/\` — SSG 出力(HTML + ビルド済みアセット、git 管理外)
|
|
2083
|
-
- \`functions/storage/\` — R2 ファイル配信(/storage/*)
|
|
2084
|
-
- \`scripts/\` — ビルドスクリプト(tsx)
|
|
2085
|
-
|
|
2086
|
-
CMS 管理画面関連(\`admin/\`, \`functions/api/\`, \`workers/\`, \`wrangler.toml\`, \`_redirects\`)は
|
|
2087
|
-
\`npx @frelio/cli update\` で追加・更新される。
|
|
2088
|
-
|
|
2089
|
-
## よく使うコマンド
|
|
2090
|
-
|
|
2091
|
-
\`\`\`bash
|
|
2092
|
-
npm run dev # Vite dev server(テンプレートプレビュー + コンテンツ監視)
|
|
2093
|
-
npm run build # 静的アセットコピー + SCSS/TS ビルド(ページ別エントリー)
|
|
2094
|
-
npm run generate # data-json 生成(差分ビルド)
|
|
2095
|
-
npm run generate:full # data-json 生成(フルリビルド)
|
|
2096
|
-
npm run generate:html # HTML 生成(data-json → public/)
|
|
2097
|
-
npm run generate:sitemap # sitemap.xml 生成
|
|
2098
|
-
npm run generate:dep-map # 依存マップ生成
|
|
2099
|
-
npm run watch:content # コンテンツ変更監視(インデックス自動更新)
|
|
2100
|
-
npm run rebuild:indexes # インデックス一括再構築
|
|
2101
|
-
npx @frelio/cli update # CMS Admin バンドル更新
|
|
2102
|
-
npx @frelio/cli add-staging # カスタムステージング追加
|
|
2103
|
-
\`\`\`
|
|
2104
|
-
|
|
2105
|
-
## ビルドパイプライン
|
|
2106
|
-
|
|
2107
|
-
\`\`\`
|
|
2108
|
-
1. Recipe → 依存マップ (npm run generate:dep-map)
|
|
2109
|
-
2. コンテンツ → data-json (npm run generate)
|
|
2110
|
-
3. data-json → HTML (npm run generate:html)
|
|
2111
|
-
4. SCSS/TS → CSS/JS (npm run build)
|
|
2112
|
-
5. sitemap.xml 生成 (npm run generate:sitemap)
|
|
2113
|
-
\`\`\`
|
|
2114
|
-
|
|
2115
|
-
## テンプレート構造(= URL 構造)
|
|
2116
|
-
|
|
2117
|
-
テンプレートの配置がそのまま出力先の URL パスになる。各ページに \`styles/index.scss\` と \`scripts/index.ts\` がある。
|
|
2118
|
-
|
|
2119
|
-
\`\`\`
|
|
2120
|
-
frelio-data/site/templates/
|
|
2121
|
-
├── _parts/ — 共通パーツ(head.htm, header.htm, footer.htm)
|
|
2122
|
-
├── common/ — 全ページ共通
|
|
2123
|
-
│ ├── scripts/ — JS 初期化 + features/
|
|
2124
|
-
│ ├── styles/ — SCSS パーシャル(FLOCSS: foundation/, layout/, component/, element/, project/)
|
|
2125
|
-
│ └── images/ — favicon, logo 等
|
|
2126
|
-
├── index.html — / (ホーム)
|
|
2127
|
-
├── scripts/index.ts — ホーム用 JS
|
|
2128
|
-
├── styles/index.scss — ホーム用 CSS(p-hero, p-news-list)
|
|
2129
|
-
├── images/ — ホーム画像
|
|
2130
|
-
├── about/ — /about/
|
|
2131
|
-
│ ├── index.html
|
|
2132
|
-
│ ├── scripts/index.ts
|
|
2133
|
-
│ ├── styles/index.scss(p-about)
|
|
2134
|
-
│ └── images/
|
|
2135
|
-
├── contact/ — /contact/
|
|
2136
|
-
│ ├── index.html
|
|
2137
|
-
│ ├── scripts/index.ts
|
|
2138
|
-
│ ├── styles/index.scss(p-contact)
|
|
2139
|
-
│ └── images/
|
|
2140
|
-
└── news/ — /news/
|
|
2141
|
-
├── index.html — 一覧テンプレート
|
|
2142
|
-
├── detail.html — 詳細テンプレート(レシピで news/{slug}.html に展開)
|
|
2143
|
-
├── scripts/index.ts — 一覧・詳細で共有
|
|
2144
|
-
├── styles/index.scss(p-news-list, p-article)
|
|
2145
|
-
└── images/
|
|
2146
|
-
\`\`\`
|
|
2147
|
-
|
|
2148
|
-
- \`_parts/head.htm\` で common の CSS/JS を読み込み
|
|
2149
|
-
- 各ページテンプレートでページ固有の CSS/JS を読み込み
|
|
2150
|
-
|
|
2151
|
-
### スラッグ展開
|
|
2152
|
-
|
|
2153
|
-
テンプレートファイル名と出力パスの対応はレシピ(\`build-data-recipe.json\`)で制御する。
|
|
2154
|
-
gentl の規約ではなく、レシピの書き方次第。
|
|
2155
|
-
|
|
2156
|
-
- **ファイルベース**: \`news/detail.html\` → \`news/{slug}.html\`(現在の設定)
|
|
2157
|
-
- **ディレクトリベース**: \`news/_detail/index.html\` → \`news/{slug}/index.html\`(別方式、必要に応じて変更可)
|
|
2158
|
-
|
|
2159
|
-
## CSS 記法ルール(FLOCSS 亜種・厳格)
|
|
2160
|
-
|
|
2161
|
-
- **プレフィックス**: \`l-\`(layout)、\`c-\`(component)、\`p-\`(project)、\`e-\`(element)のみ
|
|
2162
|
-
- **1 class ルール**: HTML の class 属性には必ず 1 クラスのみ
|
|
2163
|
-
- **Utility は絶対使用禁止**
|
|
2164
|
-
- **SCSS の \`&\` でクラス名を接続することは禁止**(grep で追跡可能を維持)
|
|
2165
|
-
- **@extend**: 同ファイル内でのみ許可。\`%placeholder\` または実体クラスに \`--variant\` サフィックス
|
|
2166
|
-
- **子要素**: \`__\` で繋げる(深さ制限なし)
|
|
2167
|
-
- **バリアント**: \`--\` サフィックス(数に制限なし)
|
|
2168
|
-
- **メディアクエリ**: \`@mixin\` で定義し、1 ファイルに各 mixin を 1 回のみ \`@include\`
|
|
2169
|
-
|
|
2170
|
-
### 例
|
|
2171
|
-
|
|
2172
|
-
\`\`\`scss
|
|
2173
|
-
%c-section__inner__item {} // ベースデザイン
|
|
2174
|
-
.c-section__inner__item--red { @extend %c-section__inner__item; color: red; }
|
|
2175
|
-
\`\`\`
|
|
2176
|
-
|
|
2177
|
-
## TypeScript ルール
|
|
2178
|
-
|
|
2179
|
-
- 共通の初期化ロジックは \`common/scripts/index.ts\` に集約
|
|
2180
|
-
- 各機能は \`common/scripts/features/\` にファイル分離
|
|
2181
|
-
- ページ固有の JS が必要な場合は \`{page}/scripts/index.ts\` に追加
|
|
2182
|
-
|
|
2183
|
-
## テンプレート規約
|
|
2184
|
-
|
|
2185
|
-
- テンプレートエンジン: gentl(\`data-gen-*\` 属性ベース)
|
|
2186
|
-
- テンプレートは valid HTML(そのままブラウザで開ける)
|
|
2187
|
-
- 共通パーツ: \`_parts/*.htm\`(head, header, footer)
|
|
2188
|
-
- ページテンプレート: \`{page}/index.html\`(ホームは \`index.html\`)
|
|
2189
|
-
- 詳細テンプレート: \`{page}/detail.html\`(レシピでスラッグ展開)
|
|
2190
|
-
|
|
2191
|
-
## Cloudflare Pages 構成
|
|
2192
|
-
|
|
2193
|
-
\`npx @frelio/cli update\` 実行後に以下が配置される:
|
|
2194
|
-
- \`_redirects\`: \`/admin/*\` → SPA、\`/*\` → \`/public/:splat\`
|
|
2195
|
-
- \`_routes.json\`: \`/api/*\`, \`/storage/*\` → Functions
|
|
2196
|
-
- \`wrangler.toml\`: R2 バケットバインディング
|
|
2197
|
-
|
|
2198
|
-
## 型パッケージ活用方針
|
|
2199
|
-
|
|
2200
|
-
JSON ファイルの読み書きでは、以下の型・ガード・スキーマを使用する。
|
|
2201
|
-
|
|
2202
|
-
| ファイル | 型 | パッケージ |
|
|
2203
|
-
|---|---|---|
|
|
2204
|
-
| \`content_types/*.json\` | \`ContentType\` / \`validateContentType\` | \`@c-time/frelio-types\` |
|
|
2205
|
-
| \`*.ui.json\` | \`ContentTypeUi\` / \`validateContentTypeUi\` | 同上 |
|
|
2206
|
-
| \`*.views.json\` | \`ContentTypeViews\` / \`validateContentTypeViews\` | 同上 |
|
|
2207
|
-
| \`build-data-recipe.json\` | \`FrelioBuildDataRecipe\` / \`validateSiteRecipe\` | 同上 |
|
|
2208
|
-
| \`data-json/*.json\` | \`FrelioDataJson\` / \`isFrelioDataJson\` | \`@c-time/frelio-data-json\` |
|
|
2209
|
-
| \`contents/*/*.json\` | \`Content\` / \`isContent\` | \`@c-time/frelio-types\` |
|
|
2210
|
-
`);
|
|
2211
|
-
}
|
|
2212
|
-
// ========== Helpers ==========
|
|
2213
|
-
function json(obj) {
|
|
2214
|
-
return JSON.stringify(obj, null, 2);
|
|
2215
|
-
}
|