ultimate-jekyll-manager 1.6.1 → 1.6.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/CHANGELOG.md CHANGED
@@ -14,6 +14,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ ---
18
+ ## [1.6.3] - 2026-06-03
19
+
20
+ ### Changed
21
+
22
+ - **Workflow template dynamically generates secrets from `.env`.** `defaults.js` reads the default `_.env`, extracts all key names, and produces a `{ github.secrets }` template variable — no more hardcoding individual secrets in `build.yml`.
23
+ - **`publishSecrets()` replaces `publishGitHubToken()` in setup.** Now reads the consumer's `.env` and publishes ALL non-empty keys as GitHub Actions repo secrets (not just `GH_TOKEN`).
24
+
25
+ ### Added
26
+
27
+ - **Country flag SVGs:** id, in, ph, pk, ru, vn (modern-square style).
28
+ - **Auto-create `pages/` dir for custom themes** in webpack.js — prevents `Module not found: __theme__/pages` error when a consumer theme lacks a `pages/` directory.
29
+
30
+ ### Removed
31
+
32
+ - **`BACKEND_MANAGER_KEY`** removed from workflow template (replaced by dynamic `.env` secrets).
33
+
34
+ ---
35
+ ## [1.6.2] - 2026-06-02
36
+
37
+ ### Fixed
38
+
39
+ - **`npx mgr setup` now merges new `.env` keys into existing consumer projects.** Previously, `ensureCoreFiles()` returned early when `src/_config.yml` existed, skipping the `gulp defaults` task that handles `.env` merging. New framework keys (like `BACKEND_MANAGER_OPENAI_API_KEY`) were never added to consumers that had already run setup once.
40
+
17
41
  ---
18
42
  ## [1.6.1] - 2026-06-02
19
43
 
package/CLAUDE.md CHANGED
@@ -200,7 +200,7 @@ Deep references live in `docs/`. Treat docs as a first-class deliverable. **When
200
200
  - [docs/themes.md](docs/themes.md) — theme system: selection + resolution (SCSS loadPaths, `__theme__`, classy layout fallback), shared vs per-theme layers, authoring a theme inside UJM OR in a consumer project, live validation
201
201
  - [docs/layouts-and-pages.md](docs/layouts-and-pages.md) — page types, layout chain, `asset_path` frontmatter
202
202
  - [docs/images.md](docs/images.md) — `@post/` shortcut for blog post images, BEM admin/post image handling, imagemin pipeline + source-size constraints + `UJ_IMAGEMIN_REWRITE_SOURCES` cleanup flag
203
- - [docs/icons.md](docs/icons.md) — Font Awesome conventions, `{% uj_icon %}` vs prerendered icons in JS, size reference
203
+ - [docs/icons.md](docs/icons.md) — Font Awesome conventions, `{% uj_icon %}` vs prerendered icons in JS, size reference, country flag SVGs (`assets/icons/flags/modern-square/`)
204
204
  - [docs/seo.md](docs/seo.md) — Alternatives collection (competitor comparison pages) + Schema/JSON-LD (`SoftwareApplication`, `FAQPage`)
205
205
 
206
206
  ### Frontend behavior
@@ -0,0 +1 @@
1
+ <svg height="512" viewBox="0 0 152 152" width="512" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="clip"><path d="m124.81 149.4a459 459 0 0 1 -97.62 0 27.69 27.69 0 0 1 -24.59-24.59 459 459 0 0 1 0-97.62 27.69 27.69 0 0 1 24.59-24.59 459 459 0 0 1 97.62 0 27.69 27.69 0 0 1 24.59 24.59 459 459 0 0 1 0 97.62 27.69 27.69 0 0 1 -24.59 24.59z"/></clipPath></defs><g clip-path="url(#clip)"><rect width="152" height="76" fill="#f40055"/><rect y="76" width="152" height="76" fill="#f0f9ff"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg height="512" viewBox="0 0 152 152" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Layer_2" data-name="Layer 2"><g id="india" data-name="india"><path id="path" d="m124.81 149.4a459 459 0 0 1 -97.62 0 27.69 27.69 0 0 1 -24.59-24.59 459 459 0 0 1 0-97.62 27.69 27.69 0 0 1 24.59-24.59 459 459 0 0 1 97.62 0 27.69 27.69 0 0 1 24.59 24.59 459 459 0 0 1 0 97.62 27.69 27.69 0 0 1 -24.59 24.59z" fill="#f0f9ff"/><path d="m151.38 52.26h-150.76q.63-12.54 2-25.08a27.68 27.68 0 0 1 24.57-24.58 459 459 0 0 1 97.62 0 27.68 27.68 0 0 1 24.59 24.58q1.33 12.53 1.98 25.08z" fill="#ff9933"/><path d="m151.38 99.73q-.63 12.54-2 25.08a27.68 27.68 0 0 1 -24.59 24.58 459 459 0 0 1 -97.62 0 27.68 27.68 0 0 1 -24.57-24.58q-1.34-12.54-2-25.08z" fill="#138808"/><circle cx="76" cy="76" r="13" fill="#000080"/><circle cx="76" cy="76" r="10.5" fill="#f0f9ff"/><circle cx="76" cy="76" r="3" fill="#000080"/></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg height="512" viewBox="0 0 152 152" width="512" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="clip"><path d="m124.81 149.4a459 459 0 0 1 -97.62 0 27.69 27.69 0 0 1 -24.59-24.59 459 459 0 0 1 0-97.62 27.69 27.69 0 0 1 24.59-24.59 459 459 0 0 1 97.62 0 27.69 27.69 0 0 1 24.59 24.59 459 459 0 0 1 0 97.62 27.69 27.69 0 0 1 -24.59 24.59z"/></clipPath></defs><g clip-path="url(#clip)"><rect width="152" height="76" fill="#406bd4"/><rect y="76" width="152" height="76" fill="#f40055"/><path d="m0 0v152l76-76z" fill="#f0f9ff"/><g fill="#ffcb24"><circle cx="25" cy="76" r="7"/><circle cx="10" cy="24" r="3.5"/><circle cx="10" cy="128" r="3.5"/><circle cx="52" cy="76" r="3.5"/></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg height="512" viewBox="0 0 152 152" width="512" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="clip"><path d="m124.81 149.4a459 459 0 0 1 -97.62 0 27.69 27.69 0 0 1 -24.59-24.59 459 459 0 0 1 0-97.62 27.69 27.69 0 0 1 24.59-24.59 459 459 0 0 1 97.62 0 27.69 27.69 0 0 1 24.59 24.59 459 459 0 0 1 0 97.62 27.69 27.69 0 0 1 -24.59 24.59z"/></clipPath></defs><g clip-path="url(#clip)"><rect width="152" height="152" fill="#01411c"/><rect width="38" height="152" fill="#f0f9ff"/><g transform="rotate(-270, 90, 76)"><circle cx="90" cy="76" r="24" fill="#f0f9ff"/><circle cx="82" cy="68" r="20" fill="#01411c"/></g><path d="m117.72 58.47a5 5 0 0 0 -4.5-5 5 5 0 0 0 -10 0 5 5 0 0 0 0 10 5 5 0 0 0 10 0 5 5 0 0 0 4.5-5z" fill="#f0f9ff"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg height="512" viewBox="0 0 152 152" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Layer_2" data-name="Layer 2"><g id="russia" data-name="russia"><path id="path" d="m124.81 149.4a459 459 0 0 1 -97.62 0 27.69 27.69 0 0 1 -24.59-24.59 459 459 0 0 1 0-97.62 27.69 27.69 0 0 1 24.59-24.59 459 459 0 0 1 97.62 0 27.69 27.69 0 0 1 24.59 24.59 459 459 0 0 1 0 97.62 27.69 27.69 0 0 1 -24.59 24.59z" fill="#406bd4"/><path d="m151.38 52.26h-150.76q.63-12.54 2-25.08a27.68 27.68 0 0 1 24.57-24.58 459 459 0 0 1 97.62 0 27.68 27.68 0 0 1 24.59 24.58q1.33 12.53 1.98 25.08z" fill="#f0f9ff"/><path d="m151.38 99.73q-.63 12.54-2 25.08a27.68 27.68 0 0 1 -24.59 24.58 459 459 0 0 1 -97.62 0 27.68 27.68 0 0 1 -24.57-24.58q-1.34-12.54-2-25.08z" fill="#f40055"/></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg height="512" viewBox="0 0 152 152" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Layer_2" data-name="Layer 2"><g id="vietnam" data-name="vietnam"><path id="path" d="m124.81 149.4a459 459 0 0 1 -97.62 0 27.69 27.69 0 0 1 -24.59-24.59 459 459 0 0 1 0-97.62 27.69 27.69 0 0 1 24.59-24.59 459 459 0 0 1 97.62 0 27.69 27.69 0 0 1 24.59 24.59 459 459 0 0 1 0 97.62 27.69 27.69 0 0 1 -24.59 24.59z" fill="#f40055"/><g transform="translate(20,0)"><path d="m84.77 66.21a5 5 0 0 0 -4.77-3.46h-12a3.67 3.67 0 0 1 -3.5-2.54l-3.78-11.47a5 5 0 0 0 -9.53 0l-3.72 11.47a3.67 3.67 0 0 1 -3.47 2.54h-12.09a5 5 0 0 0 -2.91 9.07l9.76 7.08a3.69 3.69 0 0 1 1.3 4.1l-3.73 11.49a5 5 0 0 0 7.71 5.6l9.76-7.09a3.66 3.66 0 0 1 4.32 0l9.76 7.09a5 5 0 0 0 7.71-5.6l-3.73-11.49a3.68 3.68 0 0 1 1.34-4.1l9.8-7.08a5 5 0 0 0 1.82-5.61z" fill="#ffcb24"/></g></g></g></svg>
@@ -129,9 +129,9 @@ module.exports = async function (options) {
129
129
  checkLocality();
130
130
  }
131
131
 
132
- // Publish GH_TOKEN as repository secret
132
+ // Publish .env secrets as repository secrets
133
133
  if (options.publishGitHubToken) {
134
- await publishGitHubToken();
134
+ await publishSecrets();
135
135
  }
136
136
 
137
137
  // Deduplicate posts (remove duplicate posts with same slug but different dates)
@@ -268,31 +268,28 @@ function setupScripts() {
268
268
  }
269
269
 
270
270
  async function ensureCoreFiles() {
271
- if (jetpack.exists('src/_config.yml')) {
272
- return;
273
- }
274
-
275
- logger.log('No src/_config.yml found. Creating default config file...');
276
-
277
- // Files that must exist BEFORE the gulpfile loads. Several task modules
278
- // (sass/distribute/imagemin) read these at module top-level, so a fresh
279
- // consumer can't even invoke `gulp defaults` without them.
280
- const coreFiles = [
281
- 'src/_config.yml',
282
- 'config/ultimate-jekyll-manager.json',
283
- ];
284
-
285
- coreFiles.forEach((relPath) => {
286
- const sourcePath = path.join(rootPathPackage, 'dist/defaults', relPath);
287
- const targetPath = path.join(rootPathProject, relPath);
288
- jetpack.copy(sourcePath, targetPath, { overwrite: false });
289
- logger.log(`Copied default ${relPath}`);
290
- });
271
+ // First-time setup: scaffold core files that must exist before gulp can load
272
+ if (!jetpack.exists('src/_config.yml')) {
273
+ logger.log('No src/_config.yml found. Creating default config file...');
274
+
275
+ const coreFiles = [
276
+ 'src/_config.yml',
277
+ 'config/ultimate-jekyll-manager.json',
278
+ ];
279
+
280
+ coreFiles.forEach((relPath) => {
281
+ const sourcePath = path.join(rootPathPackage, 'dist/defaults', relPath);
282
+ const targetPath = path.join(rootPathProject, relPath);
283
+ jetpack.copy(sourcePath, targetPath, { overwrite: false });
284
+ logger.log(`Copied default ${relPath}`);
285
+ });
291
286
 
292
- // Inject new config into config variable
293
- config = Manager.getConfig('project');
287
+ // Inject new config into config variable
288
+ config = Manager.getConfig('project');
289
+ }
294
290
 
295
- // Run gulp defaults task since this is likely the first run
291
+ // Always run gulp defaults merges new framework keys into .env, .gitignore,
292
+ // CLAUDE.md, and config files without overwriting user values
296
293
  await execute('UJ_BUILD_MODE=true npm run gulp -- defaults', { log: true });
297
294
  }
298
295
 
@@ -392,7 +389,7 @@ function checkLocality() {
392
389
  }
393
390
  }
394
391
 
395
- async function publishGitHubToken() {
392
+ async function publishSecrets() {
396
393
  if (!process.env.GH_TOKEN) {
397
394
  logger.warn('GH_TOKEN not found in environment variables. Skipping secret publication.');
398
395
  return;
@@ -404,37 +401,67 @@ async function publishGitHubToken() {
404
401
  }
405
402
 
406
403
  if (Manager.isBuildMode()) {
407
- logger.log('Skipping GH_TOKEN publication in build mode.');
404
+ logger.log('Skipping secret publication in build mode.');
405
+ return;
406
+ }
407
+
408
+ // Read .env and collect all keys with non-empty values
409
+ const envPath = path.join(process.cwd(), '.env');
410
+ if (!jetpack.exists(envPath)) {
411
+ logger.warn('.env file not found. Skipping secret publication.');
412
+ return;
413
+ }
414
+
415
+ const envContent = jetpack.read(envPath);
416
+ const secrets = {};
417
+
418
+ envContent.split('\n').forEach(line => {
419
+ const trimmed = line.trim();
420
+ if (!trimmed || trimmed.startsWith('#')) {
421
+ return;
422
+ }
423
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=["']?(.+?)["']?$/);
424
+ if (match && match[2]) {
425
+ secrets[match[1]] = match[2];
426
+ }
427
+ });
428
+
429
+ const secretNames = Object.keys(secrets);
430
+ if (!secretNames.length) {
431
+ logger.warn('No secrets with values found in .env. Skipping secret publication.');
408
432
  return;
409
433
  }
410
434
 
411
435
  try {
412
436
  const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
413
-
414
437
  const octokit = new Octokit({ auth: process.env.GH_TOKEN });
415
438
 
416
- logger.log(`Publishing GH_TOKEN as repository secret for ${owner}/${repo}...`);
439
+ logger.log(`Publishing ${secretNames.length} secret(s) for ${owner}/${repo}: ${secretNames.join(', ')}`);
417
440
 
418
441
  await sodium.ready;
419
442
 
420
443
  const { data: publicKeyData } = await octokit.actions.getRepoPublicKey({ owner, repo });
421
-
422
- const secretBytes = Buffer.from(process.env.GH_TOKEN);
423
444
  const keyBytes = Buffer.from(publicKeyData.key, 'base64');
424
- const encryptedBytes = sodium.crypto_box_seal(secretBytes, keyBytes);
425
- const encryptedValue = Buffer.from(encryptedBytes).toString('base64');
426
-
427
- await octokit.actions.createOrUpdateRepoSecret({
428
- owner,
429
- repo,
430
- secret_name: 'GH_TOKEN',
431
- encrypted_value: encryptedValue,
432
- key_id: publicKeyData.key_id,
433
- });
434
445
 
435
- logger.log(`Successfully published GH_TOKEN as repository secret`);
446
+ for (const [name, value] of Object.entries(secrets)) {
447
+ const secretBytes = Buffer.from(value);
448
+ const encryptedBytes = sodium.crypto_box_seal(secretBytes, keyBytes);
449
+ const encryptedValue = Buffer.from(encryptedBytes).toString('base64');
450
+
451
+ await octokit.actions.createOrUpdateRepoSecret({
452
+ owner,
453
+ repo,
454
+ secret_name: name,
455
+ encrypted_value: encryptedValue,
456
+ key_id: publicKeyData.key_id,
457
+ });
458
+
459
+ logger.log(` ✅ ${name}`);
460
+ }
461
+
462
+ logger.log(`Successfully published ${secretNames.length} secret(s)`);
436
463
  } catch (error) {
437
- logger.error(`Failed to publish GH_TOKEN as repository secret: ${error.message}`);
464
+ logger.error(`Failed to publish secrets: ${error.message}`);
438
465
  }
439
466
  }
440
467
 
@@ -18,8 +18,7 @@ concurrency:
18
18
  # contents: write
19
19
 
20
20
  env:
21
- GH_TOKEN: ${{ secrets.GH_TOKEN }}
22
- BACKEND_MANAGER_KEY: ${{ secrets.BACKEND_MANAGER_KEY }}
21
+ { github.secrets }
23
22
  RUBY_VERSION: '{ versions.ruby }'
24
23
  BUNDLER_VERSION: '{ versions.bundler }'
25
24
  NODE_VERSION: '{ versions.node }'
@@ -1,6 +1,5 @@
1
1
  # ========== Default Values ==========
2
2
  # Github Token
3
- # Get token at: https://github.com/settings/tokens
4
3
  GH_TOKEN=""
5
4
 
6
5
  # Backend Manager
@@ -30,6 +30,19 @@ const ujConfig = jetpack.exists(ujConfigPath) ? JSON5.parse(jetpack.read(ujConfi
30
30
  // const cleanVersions = { versions: Manager.getCleanVersions()};
31
31
  const cleanVersions = { versions: package.engines };
32
32
 
33
+ // Build GitHub Actions secrets env block from default .env
34
+ const defaultEnvPath = path.join(rootPathPackage, 'dist/defaults/_.env');
35
+ const githubSecrets = (() => {
36
+ const content = jetpack.exists(defaultEnvPath) ? jetpack.read(defaultEnvPath) : '';
37
+ const lines = content.split('\n')
38
+ .map(l => l.trim())
39
+ .filter(l => l && !l.startsWith('#') && l.includes('='))
40
+ .map(l => l.split('=')[0].trim())
41
+ .map(key => `${key}: \${{ secrets.${key} }}`);
42
+
43
+ return { github: { secrets: lines.join('\n ') } };
44
+ })();
45
+
33
46
  // File MAP
34
47
  const FILE_MAP = {
35
48
  // Files to skip overwrite
@@ -116,7 +129,7 @@ const FILE_MAP = {
116
129
 
117
130
  // Files to run templating on
118
131
  '.github/workflows/build.yml': {
119
- template: { ...cleanVersions, ...ujConfig },
132
+ template: { ...cleanVersions, ...ujConfig, ...githubSecrets },
120
133
  },
121
134
  '.nvmrc': {
122
135
  template: cleanVersions,
@@ -158,7 +158,10 @@ function getSettings() {
158
158
  const projectThemePath = path.resolve(rootPathProject, 'src/assets/themes', config.theme.id);
159
159
  const ujmThemePath = path.resolve(rootPathPackage, 'dist/assets/themes', config.theme.id);
160
160
  // Use project theme if it exists, otherwise fall back to UJM theme
161
- return jetpack.exists(projectThemePath) ? projectThemePath : ujmThemePath;
161
+ const resolved = jetpack.exists(projectThemePath) ? projectThemePath : ujmThemePath;
162
+ // Ensure pages/ directory exists so dynamic imports don't fail
163
+ jetpack.dir(path.join(resolved, 'pages'));
164
+ return resolved;
162
165
  })(),
163
166
  },
164
167
  // Add module resolution paths for local web-manager
package/docs/icons.md CHANGED
@@ -117,6 +117,21 @@ $el.innerHTML = '<i class="fa-solid fa-check"></i> Text';
117
117
  $el.innerHTML = `${getPrerenderedIcon('circle-check', 'fa-sm me-1')} Text`;
118
118
  ```
119
119
 
120
+ ## Country Flag Icons
121
+
122
+ UJM ships rounded-square country flag SVGs at `assets/icons/flags/modern-square/`. The `{% uj_icon %}` tag resolves language codes to country flags via a `LANGUAGE_TO_COUNTRY` mapping in [jekyll-uj-powertools](https://github.com/itw-creative-works/jekyll-uj-powertools) (`lib/tags/icon.rb`). For example, `{% uj_icon "es" %}` resolves to `es.svg` (Spain), and `{% uj_icon "ja" %}` maps `ja` → `jp` → `jp.svg` (Japan).
123
+
124
+ Most flags are from a Flaticon rounded-square icon pack. The following 6 were hand-created to match the pack's style:
125
+
126
+ - `in.svg` (India) — saffron/white/green tricolor + Ashoka Chakra
127
+ - `ru.svg` (Russia) — white/blue/red tricolor
128
+ - `id.svg` (Indonesia) — red/white bicolor
129
+ - `vn.svg` (Vietnam) — red background + yellow star
130
+ - `pk.svg` (Pakistan) — green + white stripe, crescent & star
131
+ - `ph.svg` (Philippines) — blue/red + white triangle, sun & stars
132
+
133
+ When adding new flags, match the existing style: 152×152 viewBox, `clipPath` using the shared rounded-square path, flat/simplified design.
134
+
120
135
  ## Benefits
121
136
 
122
137
  - Icons are rendered server-side with proper Font Awesome classes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {