@c-time/frelio-cli 1.4.0 → 1.4.1

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.
@@ -66,7 +66,9 @@ jobs:
66
66
  command: pages deploy dist --project-name=${config.adminPagesProjectName}
67
67
  `;
68
68
  case 'build-staging':
69
- return `name: Build Staging
69
+ return `# staging ブランチ(デフォルト + カスタム)への push で発火し、SSG を実行する。
70
+ # カスタム staging(staging-*)は artifact アップロードなし(プレビュー専用)。
71
+ name: Build Staging
70
72
  on:
71
73
  push:
72
74
  branches: [staging, 'staging-*']
@@ -83,6 +85,8 @@ jobs:
83
85
  with:
84
86
  node-version: 20
85
87
  cache: 'npm'
88
+
89
+ # 前回のビルド状態を復元(差分ビルド用)
86
90
  - name: Restore SSG cache
87
91
  uses: actions/cache@v4
88
92
  with:
@@ -92,9 +96,22 @@ jobs:
92
96
  key: ssg-cache-\${{ github.ref_name }}-\${{ github.sha }}
93
97
  restore-keys: |
94
98
  ssg-cache-\${{ github.ref_name }}-
99
+
95
100
  - run: npm ci
96
- - name: SSG Build
97
- run: echo "TODO: Add SSG build steps"
101
+
102
+ # Phase 1: JSON 中間データ生成
103
+ - name: Generate JSON data
104
+ run: npx frelio-data-json-generator
105
+
106
+ # Phase 2: HTML 生成
107
+ - name: Generate HTML
108
+ run: npx frelio-gentl
109
+
110
+ # Phase 3: アセットビルド(静的アセットコピー + Vite)
111
+ - name: Build assets
112
+ run: npm run build
113
+
114
+ # デフォルト staging のみ: artifact をアップロード(本番デプロイ用)
98
115
  - name: Upload artifact
99
116
  if: github.ref == 'refs/heads/staging'
100
117
  uses: actions/upload-artifact@v4
@@ -102,6 +119,8 @@ jobs:
102
119
  name: staging-build
103
120
  path: public/
104
121
  retention-days: 14
122
+
123
+ # Cloudflare Pages にブランチプレビューとしてデプロイ
105
124
  - name: Deploy preview
106
125
  uses: cloudflare/wrangler-action@v3
107
126
  with:
@@ -110,7 +129,10 @@ jobs:
110
129
  command: pages deploy public --project-name=${config.pagesProjectName} --branch=\${{ github.ref_name }}
111
130
  `;
112
131
  case 'promote-production':
113
- return `name: Promote to Production
132
+ return `# staging → main マージ(push to main)で発火。
133
+ # 最新の staging artifact をダウンロードし、Pages 本番にデプロイする。
134
+ # artifact 欠損時はフォールバックとしてフル SSG ビルドを実行する。
135
+ name: Promote to Production
114
136
  on:
115
137
  push:
116
138
  branches: [main]
@@ -126,28 +148,101 @@ jobs:
126
148
  - uses: actions/checkout@v4
127
149
  with:
128
150
  fetch-depth: 0
151
+
152
+ # 最新の成功した Build Staging ワークフロー run を検索
153
+ - name: Find latest staging build run
154
+ id: find-run
155
+ uses: actions/github-script@v7
156
+ with:
157
+ script: |
158
+ const runs = await github.rest.actions.listWorkflowRuns({
159
+ owner: context.repo.owner,
160
+ repo: context.repo.repo,
161
+ workflow_id: 'build-staging.yml',
162
+ branch: 'staging',
163
+ status: 'success',
164
+ per_page: 1,
165
+ });
166
+ if (runs.data.workflow_runs.length > 0) {
167
+ core.setOutput('run-id', runs.data.workflow_runs[0].id);
168
+ } else {
169
+ core.setOutput('run-id', '');
170
+ }
171
+
172
+ # cross-workflow artifact ダウンロード
129
173
  - name: Download staging artifact
174
+ id: download-artifact
175
+ if: steps.find-run.outputs.run-id != ''
176
+ continue-on-error: true
130
177
  uses: actions/download-artifact@v4
131
178
  with:
132
179
  name: staging-build
133
180
  path: public/
181
+ github-token: \${{ secrets.GITHUB_TOKEN }}
182
+ run-id: \${{ steps.find-run.outputs.run-id }}
183
+
184
+ # artifact 取得結果を判定
185
+ - name: Check if artifact was downloaded
186
+ id: check-artifact
187
+ run: |
188
+ if [ -d "public" ] && [ "\$(ls -A public 2>/dev/null)" ]; then
189
+ echo "has_artifact=true" >> "\$GITHUB_OUTPUT"
190
+ else
191
+ echo "::warning::Staging artifact not found or expired. Running full SSG build."
192
+ echo "has_artifact=false" >> "\$GITHUB_OUTPUT"
193
+ fi
194
+
195
+ # --- フォールバックビルド(artifact 欠損時のみ) ---
196
+ - name: Setup Node.js (fallback)
197
+ if: steps.check-artifact.outputs.has_artifact == 'false'
198
+ uses: actions/setup-node@v4
199
+ with:
200
+ node-version: 20
201
+ cache: 'npm'
202
+
203
+ - name: Install dependencies (fallback)
204
+ if: steps.check-artifact.outputs.has_artifact == 'false'
205
+ run: npm ci
206
+
207
+ - name: Full SSG build (fallback)
208
+ if: steps.check-artifact.outputs.has_artifact == 'false'
209
+ run: |
210
+ npx frelio-data-json-generator --full-rebuild
211
+ npx frelio-gentl
212
+ npm run build
213
+
214
+ # Cloudflare Pages 本番にデプロイ
134
215
  - name: Deploy to production
135
216
  uses: cloudflare/wrangler-action@v3
136
217
  with:
137
218
  apiToken: \${{ secrets.CLOUDFLARE_API_TOKEN }}
138
219
  accountId: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
139
220
  command: pages deploy public --project-name=${config.pagesProjectName} --branch=main
221
+
222
+ # デプロイタグ作成
140
223
  - name: Create deploy tag
141
224
  run: |
142
- VERSION=\$(cat version.json | node -e "process.stdin.on('data',d=>console.log(JSON.parse(d).version))")
143
- EXISTING=\$(git tag -l "d\${VERSION}.*" | wc -l)
144
- NEXT=\$((EXISTING + 1))
145
- TAG="d\${VERSION}.\${NEXT}"
146
- git tag "\$TAG"
225
+ VERSION=\$(jq -r '.version' version.json 2>/dev/null || echo "0.0.0")
226
+ PREFIX="d\${VERSION}."
227
+
228
+ LATEST=\$(git tag -l "\${PREFIX}*" | sed "s/^\${PREFIX}//" | sort -n | tail -1)
229
+ NEXT=\$(( \${LATEST:-0} + 1 ))
230
+
231
+ TAG="\${PREFIX}\${NEXT}"
232
+ echo "Creating tag: \$TAG"
233
+
234
+ git config user.name "github-actions[bot]"
235
+ git config user.email "github-actions[bot]@users.noreply.github.com"
236
+ git tag -a "\$TAG" -m "Production deploy \$TAG"
147
237
  git push origin "\$TAG"
238
+
239
+ echo "::notice::Tagged as \$TAG"
148
240
  `;
149
241
  case 'direct-deploy':
150
- return `name: Direct Deploy
242
+ return `# CMS の「直接デプロイ」ボタンから workflow_dispatch で発火。
243
+ # develop → staging マージ → SSG ビルド → artifact → staging プレビュー →
244
+ # staging → main マージ → 本番デプロイ → デプロイタグ付与
245
+ name: Direct Deploy
151
246
  on:
152
247
  workflow_dispatch:
153
248
 
@@ -155,26 +250,96 @@ permissions:
155
250
  contents: write
156
251
 
157
252
  jobs:
158
- deploy:
253
+ build:
159
254
  runs-on: ubuntu-latest
160
255
  steps:
161
256
  - uses: actions/checkout@v4
162
257
  with:
163
258
  fetch-depth: 0
164
- - name: Merge develop to staging
259
+
260
+ - name: Configure git
165
261
  run: |
166
262
  git config user.name "github-actions[bot]"
167
263
  git config user.email "github-actions[bot]@users.noreply.github.com"
264
+
265
+ # Step 1: develop → staging マージ
266
+ - name: Merge develop into staging
267
+ run: |
168
268
  git checkout staging
169
269
  git merge origin/develop --no-edit
170
270
  git push origin staging
171
- - name: Wait for staging build
172
- run: echo "Staging build will be triggered by the push above. Monitor build-staging.yml."
173
- - name: Fast-forward main
271
+
272
+ # Step 2: SSG ビルド
273
+ - uses: actions/setup-node@v4
274
+ with:
275
+ node-version: 20
276
+ cache: 'npm'
277
+
278
+ # 前回の staging ビルド状態を復元(差分ビルド用)
279
+ - name: Restore SSG cache
280
+ uses: actions/cache@v4
281
+ with:
282
+ path: |
283
+ frelio-data/site/data/data-json
284
+ public
285
+ key: ssg-cache-staging-\${{ github.sha }}
286
+ restore-keys: |
287
+ ssg-cache-staging-
288
+
289
+ - run: npm ci
290
+
291
+ - name: Generate JSON data
292
+ run: npx frelio-data-json-generator
293
+
294
+ - name: Generate HTML
295
+ run: npx frelio-gentl
296
+
297
+ - name: Build assets
298
+ run: npm run build
299
+
300
+ # Step 3: artifact アップロード
301
+ - name: Upload artifact
302
+ uses: actions/upload-artifact@v4
303
+ with:
304
+ name: staging-build
305
+ path: public/
306
+ retention-days: 14
307
+
308
+ # Step 4: Cloudflare Pages プレビューデプロイ(staging ブランチ)
309
+ - name: Deploy staging preview
310
+ uses: cloudflare/wrangler-action@v3
311
+ with:
312
+ apiToken: \${{ secrets.CLOUDFLARE_API_TOKEN }}
313
+ accountId: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
314
+ command: pages deploy public --project-name=${config.pagesProjectName} --branch=staging
315
+
316
+ # Step 5: staging → main マージ
317
+ - name: Fast-forward main to staging
174
318
  run: |
175
319
  git checkout main
176
- git merge staging --ff-only
320
+ git merge --ff-only staging
177
321
  git push origin main
322
+
323
+ # Step 6: Cloudflare Pages 本番デプロイ
324
+ - name: Deploy to production
325
+ uses: cloudflare/wrangler-action@v3
326
+ with:
327
+ apiToken: \${{ secrets.CLOUDFLARE_API_TOKEN }}
328
+ accountId: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
329
+ command: pages deploy public --project-name=${config.pagesProjectName} --branch=main
330
+
331
+ # Step 7: デプロイタグ作成
332
+ - name: Create deploy tag
333
+ run: |
334
+ VERSION=\$(jq -r '.version' version.json 2>/dev/null || echo "0.0.0")
335
+ PREFIX="d\${VERSION}."
336
+ LATEST=\$(git tag -l "\${PREFIX}*" | sed "s/^\${PREFIX}//" | sort -n | tail -1)
337
+ NEXT=\$(( \${LATEST:-0} + 1 ))
338
+ TAG="\${PREFIX}\${NEXT}"
339
+
340
+ git tag -a "\$TAG" -m "Direct deploy \$TAG"
341
+ git push origin "\$TAG"
342
+ echo "::notice::Tagged as \$TAG"
178
343
  `;
179
344
  }
180
345
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * GitHub Release からの tarball ダウンロード
3
+ */
4
+ type ReleaseAsset = {
5
+ name: string;
6
+ browser_download_url: string;
7
+ };
8
+ type Release = {
9
+ tag_name: string;
10
+ assets: ReleaseAsset[];
11
+ };
12
+ export declare function getLatestRelease(): Promise<Release>;
13
+ export declare function getRelease(version: string): Promise<Release>;
14
+ export declare function downloadTarball(release: Release, destDir: string): Promise<string>;
15
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GitHub Release からの tarball ダウンロード
3
+ */
4
+ import { createWriteStream } from 'node:fs';
5
+ import { pipeline } from 'node:stream/promises';
6
+ import { Readable } from 'node:stream';
7
+ import path from 'node:path';
8
+ const REPO = 'ctime-projects/frelio';
9
+ export async function getLatestRelease() {
10
+ const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
11
+ headers: { Accept: 'application/vnd.github.v3+json' },
12
+ });
13
+ if (!res.ok) {
14
+ throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`);
15
+ }
16
+ return res.json();
17
+ }
18
+ export async function getRelease(version) {
19
+ const tag = version.startsWith('v') ? version : `v${version}`;
20
+ const res = await fetch(`https://api.github.com/repos/${REPO}/releases/tags/${tag}`, {
21
+ headers: { Accept: 'application/vnd.github.v3+json' },
22
+ });
23
+ if (!res.ok) {
24
+ throw new Error(`Release ${tag} not found: ${res.status}`);
25
+ }
26
+ return res.json();
27
+ }
28
+ export async function downloadTarball(release, destDir) {
29
+ const asset = release.assets.find((a) => a.name.endsWith('.tar.gz'));
30
+ if (!asset) {
31
+ throw new Error('No tarball found in release assets');
32
+ }
33
+ const destPath = path.join(destDir, asset.name);
34
+ const res = await fetch(asset.browser_download_url);
35
+ if (!res.ok || !res.body) {
36
+ throw new Error(`Failed to download: ${res.status}`);
37
+ }
38
+ const readable = Readable.fromWeb(res.body);
39
+ await pipeline(readable, createWriteStream(destPath));
40
+ return destPath;
41
+ }
@@ -111,8 +111,8 @@ export function generateInitialContent(projectDir) {
111
111
  details: [
112
112
  {
113
113
  type: 'detail',
114
- outputPath: 'news/{$this.slug}/index.json',
115
- templatePath: 'news/_detail/index.html',
114
+ outputPath: 'news/{$this.slug}.json',
115
+ templatePath: 'news/detail.html',
116
116
  },
117
117
  ],
118
118
  lists: [
@@ -206,8 +206,8 @@ function generateTemplates(projectDir) {
206
206
  <meta charset="UTF-8">
207
207
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
208
208
  <title>Frelio Demo</title>
209
- <link rel="stylesheet" href="/home/styles/index.css">
210
- <script type="module" src="/home/scripts/index.js"></script>
209
+ <link rel="stylesheet" href="/styles/index.css">
210
+ <script type="module" src="/scripts/index.js"></script>
211
211
  </head>
212
212
  <body>
213
213
  <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
@@ -274,8 +274,8 @@ function generateTemplates(projectDir) {
274
274
  </body>
275
275
  </html>
276
276
  `);
277
- // about.html
278
- writeFile(t('about.html'), `<!DOCTYPE html>
277
+ // about/index.html
278
+ writeFile(t('about/index.html'), `<!DOCTYPE html>
279
279
  <html lang="ja">
280
280
  <head>
281
281
  <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
@@ -344,8 +344,8 @@ function generateTemplates(projectDir) {
344
344
  </body>
345
345
  </html>
346
346
  `);
347
- // contact.html
348
- writeFile(t('contact.html'), `<!DOCTYPE html>
347
+ // contact/index.html
348
+ writeFile(t('contact/index.html'), `<!DOCTYPE html>
349
349
  <html lang="ja">
350
350
  <head>
351
351
  <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
@@ -472,16 +472,16 @@ function generateTemplates(projectDir) {
472
472
  </body>
473
473
  </html>
474
474
  `);
475
- // news/_detail/index.html
476
- writeFile(t('news/_detail/index.html'), `<!DOCTYPE html>
475
+ // news/detail.html
476
+ writeFile(t('news/detail.html'), `<!DOCTYPE html>
477
477
  <html lang="ja">
478
478
  <head>
479
479
  <template data-gen-scope="" data-gen-include="_parts/head.htm"></template>
480
480
  <meta charset="UTF-8">
481
481
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
482
482
  <title>記事タイトル | Frelio Demo</title>
483
- <link rel="stylesheet" href="/news/detail/styles/index.css">
484
- <script type="module" src="/news/detail/scripts/index.js"></script>
483
+ <link rel="stylesheet" href="/news/styles/index.css">
484
+ <script type="module" src="/news/scripts/index.js"></script>
485
485
  </head>
486
486
  <body>
487
487
  <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
@@ -535,10 +535,21 @@ function generateTemplates(projectDir) {
535
535
  </body>
536
536
  </html>
537
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
+ }
538
549
  }
539
550
  // ========== SCSS ==========
540
551
  function generateScss(projectDir) {
541
- const s = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/scss', ...p);
552
+ const s = (...p) => path.join(projectDir, 'frelio-data/site/templates/common/styles', ...p);
542
553
  // Foundation
543
554
  writeFile(s('foundation/_variables.scss'), `// Colors
544
555
  $color-primary: #0070f3;
@@ -1195,7 +1206,7 @@ button {
1195
1206
  }
1196
1207
  // ========== TypeScript ==========
1197
1208
  function generateTypeScript(projectDir) {
1198
- const ts = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/ts', ...p);
1209
+ const ts = (...p) => path.join(projectDir, 'frelio-data/site/templates/common/scripts', ...p);
1199
1210
  writeFile(ts('features/mobile-nav.ts'), `export function initMobileNav(): void {
1200
1211
  const toggle = document.querySelector<HTMLButtonElement>('.c-nav__toggle')
1201
1212
  const list = document.querySelector<HTMLUListElement>('.c-nav__list')
@@ -1238,9 +1249,9 @@ function generateTypeScript(projectDir) {
1238
1249
  }
1239
1250
  // ========== Entry Points ==========
1240
1251
  function generateEntries(projectDir) {
1241
- const e = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/entries', ...p);
1252
+ const t = (...p) => path.join(projectDir, 'frelio-data/site/templates', ...p);
1242
1253
  // common
1243
- writeFile(e('common/styles/index.scss'), `// Foundation
1254
+ writeFile(t('common/styles/index.scss'), `// Foundation
1244
1255
  @use 'foundation/variables' as *;
1245
1256
  @use 'foundation/mixins' as *;
1246
1257
  @use 'foundation/reset';
@@ -1260,55 +1271,48 @@ function generateEntries(projectDir) {
1260
1271
  // Element
1261
1272
  @use 'element/e-heading';
1262
1273
  `);
1263
- writeFile(e('common/scripts/index.ts'), `import '../styles/index.scss'
1264
- import { initMobileNav } from '@features/mobile-nav'
1265
- import { initSmoothScroll } from '@features/smooth-scroll'
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'
1266
1277
 
1267
1278
  document.addEventListener('DOMContentLoaded', () => {
1268
1279
  initMobileNav()
1269
1280
  initSmoothScroll()
1270
1281
  })
1271
1282
  `);
1272
- // home
1273
- writeFile(e('home/styles/index.scss'), `@use 'foundation/variables' as *;
1283
+ // home (root level — / maps to templates root)
1284
+ writeFile(t('styles/index.scss'), `@use 'foundation/variables' as *;
1274
1285
  @use 'foundation/mixins' as *;
1275
1286
 
1276
1287
  @use 'project/p-hero';
1277
1288
  @use 'project/p-news-list';
1278
1289
  `);
1279
- writeFile(e('home/scripts/index.ts'), `import '../styles/index.scss'
1290
+ writeFile(t('scripts/index.ts'), `import '../styles/index.scss'
1280
1291
  `);
1281
1292
  // about
1282
- writeFile(e('about/styles/index.scss'), `@use 'foundation/variables' as *;
1293
+ writeFile(t('about/styles/index.scss'), `@use 'foundation/variables' as *;
1283
1294
  @use 'foundation/mixins' as *;
1284
1295
 
1285
1296
  @use 'project/p-about';
1286
1297
  `);
1287
- writeFile(e('about/scripts/index.ts'), `import '../styles/index.scss'
1298
+ writeFile(t('about/scripts/index.ts'), `import '../styles/index.scss'
1288
1299
  `);
1289
1300
  // contact
1290
- writeFile(e('contact/styles/index.scss'), `@use 'foundation/variables' as *;
1301
+ writeFile(t('contact/styles/index.scss'), `@use 'foundation/variables' as *;
1291
1302
  @use 'foundation/mixins' as *;
1292
1303
 
1293
1304
  @use 'project/p-contact';
1294
1305
  `);
1295
- writeFile(e('contact/scripts/index.ts'), `import '../styles/index.scss'
1306
+ writeFile(t('contact/scripts/index.ts'), `import '../styles/index.scss'
1296
1307
  `);
1297
- // news
1298
- writeFile(e('news/styles/index.scss'), `@use 'foundation/variables' as *;
1308
+ // news (list + detail share the same scripts/styles)
1309
+ writeFile(t('news/styles/index.scss'), `@use 'foundation/variables' as *;
1299
1310
  @use 'foundation/mixins' as *;
1300
1311
 
1301
1312
  @use 'project/p-news-list';
1302
- `);
1303
- writeFile(e('news/scripts/index.ts'), `import '../styles/index.scss'
1304
- `);
1305
- // news/detail
1306
- writeFile(e('news/detail/styles/index.scss'), `@use 'foundation/variables' as *;
1307
- @use 'foundation/mixins' as *;
1308
-
1309
1313
  @use 'project/p-article';
1310
1314
  `);
1311
- writeFile(e('news/detail/scripts/index.ts'), `import '../styles/index.scss'
1315
+ writeFile(t('news/scripts/index.ts'), `import '../styles/index.scss'
1312
1316
  `);
1313
1317
  }
1314
1318
  // ========== Build Scripts ==========
@@ -2071,13 +2075,13 @@ Frelio(ヘッドレス CMS)で構築されたサイトリポジトリ。
2071
2075
  ## プロジェクト構成
2072
2076
 
2073
2077
  - \`frelio-data/\` — CMS データ(コンテンツタイプ、コンテンツ、テンプレート、レシピ)
2074
- - \`site/templates/assets/scss/\` — 共有 SCSS パーシャル(FLOCSS 亜種)
2075
- - \`site/templates/assets/ts/\` — 共有 TypeScript(features/)
2076
- - \`site/templates/assets/entries/\` — ページ別エントリーポイント
2077
- - \`public/\` — SSG 出力(HTML + ビルド済みアセット)
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 管理外)
2078
2083
  - \`functions/storage/\` — R2 ファイル配信(/storage/*)
2079
2084
  - \`scripts/\` — ビルドスクリプト(tsx)
2080
- - \`public/images/\` — 静的ファイル(画像等、git 追跡対象)
2081
2085
 
2082
2086
  CMS 管理画面関連(\`admin/\`, \`functions/api/\`, \`workers/\`, \`wrangler.toml\`, \`_redirects\`)は
2083
2087
  \`npx @frelio/cli update\` で追加・更新される。
@@ -2086,7 +2090,7 @@ CMS 管理画面関連(\`admin/\`, \`functions/api/\`, \`workers/\`, \`wrangle
2086
2090
 
2087
2091
  \`\`\`bash
2088
2092
  npm run dev # Vite dev server(テンプレートプレビュー + コンテンツ監視)
2089
- npm run build # SCSS/TS ビルド(ページ別エントリー)
2093
+ npm run build # 静的アセットコピー + SCSS/TS ビルド(ページ別エントリー)
2090
2094
  npm run generate # data-json 生成(差分ビルド)
2091
2095
  npm run generate:full # data-json 生成(フルリビルド)
2092
2096
  npm run generate:html # HTML 生成(data-json → public/)
@@ -2108,23 +2112,50 @@ npx @frelio/cli add-staging # カスタムステージング追加
2108
2112
  5. sitemap.xml 生成 (npm run generate:sitemap)
2109
2113
  \`\`\`
2110
2114
 
2111
- ## ページ別エントリーポイント
2115
+ ## テンプレート構造(= URL 構造)
2112
2116
 
2113
- アセットはページ単位でコード分割される。各ページに \`styles/index.scss\` と \`scripts/index.ts\` がある。
2117
+ テンプレートの配置がそのまま出力先の URL パスになる。各ページに \`styles/index.scss\` と \`scripts/index.ts\` がある。
2114
2118
 
2115
2119
  \`\`\`
2116
- assets/entries/
2117
- ├── common/ 全ページ共通(foundation, layout, component, element + JS初期化)
2118
- ├── home/ トップページ(p-hero, p-news-list)
2119
- ├── about/ 会社概要(p-about)
2120
- ├── contact/ お問い合わせ(p-contact)
2121
- ├── news/ お知らせ一覧(p-news-list)
2122
- └── news/detail/ — 記事詳細(p-article)
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/
2123
2146
  \`\`\`
2124
2147
 
2125
2148
  - \`_parts/head.htm\` で common の CSS/JS を読み込み
2126
2149
  - 各ページテンプレートでページ固有の CSS/JS を読み込み
2127
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
+
2128
2159
  ## CSS 記法ルール(FLOCSS 亜種・厳格)
2129
2160
 
2130
2161
  - **プレフィックス**: \`l-\`(layout)、\`c-\`(component)、\`p-\`(project)、\`e-\`(element)のみ
@@ -2145,16 +2176,17 @@ assets/entries/
2145
2176
 
2146
2177
  ## TypeScript ルール
2147
2178
 
2148
- - 共通の初期化ロジックは \`entries/common/scripts/index.ts\` に集約
2149
- - 各機能は \`assets/ts/features/\` にファイル分離
2150
- - ページ固有の JS が必要な場合は \`entries/{page}/scripts/index.ts\` に追加
2179
+ - 共通の初期化ロジックは \`common/scripts/index.ts\` に集約
2180
+ - 各機能は \`common/scripts/features/\` にファイル分離
2181
+ - ページ固有の JS が必要な場合は \`{page}/scripts/index.ts\` に追加
2151
2182
 
2152
2183
  ## テンプレート規約
2153
2184
 
2154
2185
  - テンプレートエンジン: gentl(\`data-gen-*\` 属性ベース)
2155
2186
  - テンプレートは valid HTML(そのままブラウザで開ける)
2156
2187
  - 共通パーツ: \`_parts/*.htm\`(head, header, footer)
2157
- - ページテンプレート: \`*.html\`
2188
+ - ページテンプレート: \`{page}/index.html\`(ホームは \`index.html\`)
2189
+ - 詳細テンプレート: \`{page}/detail.html\`(レシピでスラッグ展開)
2158
2190
 
2159
2191
  ## Cloudflare Pages 構成
2160
2192
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * テンプレート変数置換
3
+ *
4
+ * {{variable}} 形式のプレースホルダーを値に置換する。
5
+ * GitHub Actions の ${{ expression }} とは衝突しない。
6
+ */
7
+ import type { ProjectConfig } from './templates.js';
8
+ export type TemplateVariables = Record<string, string>;
9
+ /**
10
+ * テンプレート文字列内の {{variable}} を置換する
11
+ */
12
+ export declare function renderTemplate(content: string, vars: TemplateVariables): string;
13
+ /**
14
+ * ProjectConfig からテンプレート変数マップを生成する
15
+ */
16
+ export declare function projectConfigToVars(config: ProjectConfig): TemplateVariables;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * テンプレート変数置換
3
+ *
4
+ * {{variable}} 形式のプレースホルダーを値に置換する。
5
+ * GitHub Actions の ${{ expression }} とは衝突しない。
6
+ */
7
+ /**
8
+ * テンプレート文字列内の {{variable}} を置換する
9
+ */
10
+ export function renderTemplate(content, vars) {
11
+ return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
12
+ return key in vars ? vars[key] : match;
13
+ });
14
+ }
15
+ /**
16
+ * ProjectConfig からテンプレート変数マップを生成する
17
+ */
18
+ export function projectConfigToVars(config) {
19
+ return {
20
+ contentRepo: config.contentRepo,
21
+ githubClientId: config.githubClientId,
22
+ siteTitle: config.siteTitle,
23
+ productionUrl: config.productionUrl,
24
+ previewUrl: config.previewUrl,
25
+ fileUploadUrl: '/api/storage',
26
+ pagesProjectName: config.pagesProjectName,
27
+ adminPagesProjectName: config.adminPagesProjectName,
28
+ r2BucketName: config.r2BucketName,
29
+ r2PublicUrl: config.r2PublicUrl,
30
+ ownerUsername: config.ownerUsername,
31
+ };
32
+ }