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 +24 -0
- package/CLAUDE.md +1 -1
- package/assets/icons/flags/modern-square/id.svg +1 -0
- package/assets/icons/flags/modern-square/in.svg +1 -0
- package/assets/icons/flags/modern-square/ph.svg +1 -0
- package/assets/icons/flags/modern-square/pk.svg +1 -0
- package/assets/icons/flags/modern-square/ru.svg +1 -0
- package/assets/icons/flags/modern-square/vn.svg +1 -0
- package/dist/commands/setup.js +70 -43
- package/dist/defaults/.github/workflows/build.yml +1 -2
- package/dist/defaults/_.env +0 -1
- package/dist/gulp/tasks/defaults.js +14 -1
- package/dist/gulp/tasks/webpack.js +4 -1
- package/docs/icons.md +15 -0
- package/package.json +1 -1
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>
|
package/dist/commands/setup.js
CHANGED
|
@@ -129,9 +129,9 @@ module.exports = async function (options) {
|
|
|
129
129
|
checkLocality();
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
// Publish
|
|
132
|
+
// Publish .env secrets as repository secrets
|
|
133
133
|
if (options.publishGitHubToken) {
|
|
134
|
-
await
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
293
|
-
|
|
287
|
+
// Inject new config into config variable
|
|
288
|
+
config = Manager.getConfig('project');
|
|
289
|
+
}
|
|
294
290
|
|
|
295
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 }'
|
package/dist/defaults/_.env
CHANGED
|
@@ -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
|
-
|
|
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
|