@koehler8/cms 1.0.0-beta.5 → 1.0.0-beta.7
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/README.md +18 -8
- package/package.json +1 -1
- package/scripts/generate-public-assets.js +14 -3
- package/scripts/migrate-to-content-dirs.mjs +223 -0
- package/src/components/Header.vue +2 -2
- package/src/utils/loadConfig.js +80 -32
- package/vite-plugin.js +18 -10
package/README.md
CHANGED
|
@@ -75,15 +75,25 @@ vite-ssg build # Static site generation
|
|
|
75
75
|
|
|
76
76
|
## Site Configuration
|
|
77
77
|
|
|
78
|
-
Site content lives in `site/
|
|
78
|
+
Site content lives in `site/content/`, organized by locale:
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
```
|
|
81
|
+
site/content/
|
|
82
|
+
content.config.json # { "baseLocale": "en" }
|
|
83
|
+
en/
|
|
84
|
+
site.json # Site metadata (flat dot-notation keys, sorted)
|
|
85
|
+
shared.json # Shared content (header, footer, socials)
|
|
86
|
+
pages/
|
|
87
|
+
home.json # Per-page content
|
|
88
|
+
privacy.json
|
|
89
|
+
de/
|
|
90
|
+
site.json # German overrides (only translated keys)
|
|
91
|
+
shared.json
|
|
92
|
+
pages/
|
|
93
|
+
home.json
|
|
94
|
+
```
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
All files use flat dot-notation keys in alphabetical order. The base locale (specified in `content.config.json`) is loaded first; other locale directories override only the keys they specify. Missing keys fall back to the base locale value.
|
|
87
97
|
|
|
88
98
|
**Supported locales:** en, fr, es, de, ja, ko, pt, ru, tr, vi, id, zh, th, hi, fil
|
|
89
99
|
|
|
@@ -91,7 +101,7 @@ Place locale-specific configs in `site/config/i18n/{locale}/` mirroring the same
|
|
|
91
101
|
|
|
92
102
|
Themes export a manifest with design tokens (palette, typography, surfaces, CTAs, etc.) that are applied as CSS variables at runtime.
|
|
93
103
|
|
|
94
|
-
- Set `
|
|
104
|
+
- Set the `theme` key in `content/{baseLocale}/site.json` to a theme slug
|
|
95
105
|
- Omit it to use the built-in `base` theme
|
|
96
106
|
- External themes are npm packages registered via the `themes` plugin option
|
|
97
107
|
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Reads from CMS_SITE_DIR env var (e.g. /path/to/my-site/site/).
|
|
5
5
|
// Assets are read from {CMS_SITE_DIR}/assets/, config from
|
|
6
|
-
// {CMS_SITE_DIR}/
|
|
6
|
+
// {CMS_SITE_DIR}/content/{baseLocale}/site.json, output is written to
|
|
7
7
|
// {dirname(CMS_SITE_DIR)}/public/.
|
|
8
8
|
|
|
9
9
|
import 'dotenv/config';
|
|
@@ -260,12 +260,23 @@ function renderOgImageBuffer(text, { width = 1200, height = 630, bg = '#000000',
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
function loadSiteTitle() {
|
|
263
|
-
const
|
|
263
|
+
const contentDir = path.join(CMS_SITE_DIR, 'content');
|
|
264
|
+
const contentConfigPath = path.join(contentDir, 'content.config.json');
|
|
265
|
+
let baseLocale = 'en';
|
|
266
|
+
try {
|
|
267
|
+
if (fs.existsSync(contentConfigPath)) {
|
|
268
|
+
const cc = JSON.parse(fs.readFileSync(contentConfigPath, 'utf-8'));
|
|
269
|
+
baseLocale = cc.baseLocale || 'en';
|
|
270
|
+
}
|
|
271
|
+
} catch { /* use default */ }
|
|
272
|
+
|
|
273
|
+
const configPath = path.join(contentDir, baseLocale, 'site.json');
|
|
264
274
|
if (!fs.existsSync(configPath)) return '';
|
|
265
275
|
try {
|
|
266
276
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
267
277
|
const parsed = JSON.parse(raw);
|
|
268
|
-
|
|
278
|
+
// Flat format: key is "title"; nested format: key is "site.title" or nested
|
|
279
|
+
const title = parsed?.title || parsed?.['site.title'] || parsed?.site?.title;
|
|
269
280
|
if (typeof title === 'string' && title.trim()) {
|
|
270
281
|
return title.trim();
|
|
271
282
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Migration script: converts site/config/ structure to site/content/ structure.
|
|
4
|
+
*
|
|
5
|
+
* Old layout:
|
|
6
|
+
* site/config/site.json (nested)
|
|
7
|
+
* site/config/shared.json (nested)
|
|
8
|
+
* site/config/pages/*.json (nested)
|
|
9
|
+
* site/config/i18n/{locale}.json (flat dot-notation)
|
|
10
|
+
*
|
|
11
|
+
* New layout:
|
|
12
|
+
* site/content/content.config.json ({ "baseLocale": "en" })
|
|
13
|
+
* site/content/en/site.json (flat dot-notation, sorted)
|
|
14
|
+
* site/content/en/shared.json (flat dot-notation, sorted)
|
|
15
|
+
* site/content/en/pages/*.json (flat dot-notation, sorted)
|
|
16
|
+
* site/content/{locale}/site.json (flat dot-notation, sorted, overrides only)
|
|
17
|
+
* site/content/{locale}/shared.json
|
|
18
|
+
* site/content/{locale}/pages/*.json
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node scripts/migrate-to-content-dirs.mjs /path/to/site-repo
|
|
22
|
+
* node scripts/migrate-to-content-dirs.mjs /path/to/site-repo --commit
|
|
23
|
+
*
|
|
24
|
+
* Without --commit, runs in dry-run mode (prints what it would do).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
|
|
30
|
+
// ---- Flatten utility ----
|
|
31
|
+
|
|
32
|
+
const ARRAY_INDEX_RE = /\[(\d+)]/g;
|
|
33
|
+
|
|
34
|
+
function flattenObject(obj, prefix = '') {
|
|
35
|
+
const result = {};
|
|
36
|
+
if (!obj || typeof obj !== 'object') return result;
|
|
37
|
+
|
|
38
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
39
|
+
if (key.startsWith('$')) continue;
|
|
40
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
value.forEach((item, idx) => {
|
|
44
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
45
|
+
Object.assign(result, flattenObject(item, `${fullKey}[${idx}]`));
|
|
46
|
+
} else {
|
|
47
|
+
result[`${fullKey}[${idx}]`] = item;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
} else if (value && typeof value === 'object') {
|
|
51
|
+
Object.assign(result, flattenObject(value, fullKey));
|
|
52
|
+
} else {
|
|
53
|
+
result[fullKey] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sortedJson(obj) {
|
|
60
|
+
const sorted = {};
|
|
61
|
+
for (const key of Object.keys(obj).sort()) {
|
|
62
|
+
sorted[key] = obj[key];
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify(sorted, null, 2) + '\n';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- Main ----
|
|
68
|
+
|
|
69
|
+
const siteRepoDir = process.argv[2];
|
|
70
|
+
const commit = process.argv.includes('--commit');
|
|
71
|
+
|
|
72
|
+
if (!siteRepoDir) {
|
|
73
|
+
console.error('Usage: node scripts/migrate-to-content-dirs.mjs /path/to/site-repo [--commit]');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const siteDir = path.join(siteRepoDir, 'site');
|
|
78
|
+
const configDir = path.join(siteDir, 'config');
|
|
79
|
+
const contentDir = path.join(siteDir, 'content');
|
|
80
|
+
|
|
81
|
+
if (!fs.existsSync(configDir)) {
|
|
82
|
+
console.error(`Config directory not found: ${configDir}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const actions = [];
|
|
87
|
+
|
|
88
|
+
function plan(action, detail) {
|
|
89
|
+
actions.push({ action, detail });
|
|
90
|
+
if (!commit) console.log(` [dry-run] ${action}: ${detail}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 1. Flatten and move English base files
|
|
94
|
+
const enDir = path.join(contentDir, 'en');
|
|
95
|
+
const enPagesDir = path.join(enDir, 'pages');
|
|
96
|
+
|
|
97
|
+
// site.json
|
|
98
|
+
const siteJsonPath = path.join(configDir, 'site.json');
|
|
99
|
+
if (fs.existsSync(siteJsonPath)) {
|
|
100
|
+
const nested = JSON.parse(fs.readFileSync(siteJsonPath, 'utf-8'));
|
|
101
|
+
const flat = flattenObject(nested);
|
|
102
|
+
plan('write', `content/en/site.json (${Object.keys(flat).length} keys)`);
|
|
103
|
+
if (commit) {
|
|
104
|
+
fs.mkdirSync(enDir, { recursive: true });
|
|
105
|
+
fs.writeFileSync(path.join(enDir, 'site.json'), sortedJson(flat));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// shared.json
|
|
110
|
+
const sharedJsonPath = path.join(configDir, 'shared.json');
|
|
111
|
+
if (fs.existsSync(sharedJsonPath)) {
|
|
112
|
+
const nested = JSON.parse(fs.readFileSync(sharedJsonPath, 'utf-8'));
|
|
113
|
+
const flat = flattenObject(nested);
|
|
114
|
+
plan('write', `content/en/shared.json (${Object.keys(flat).length} keys)`);
|
|
115
|
+
if (commit) {
|
|
116
|
+
fs.mkdirSync(enDir, { recursive: true });
|
|
117
|
+
fs.writeFileSync(path.join(enDir, 'shared.json'), sortedJson(flat));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// pages/*.json
|
|
122
|
+
const pagesDir = path.join(configDir, 'pages');
|
|
123
|
+
if (fs.existsSync(pagesDir)) {
|
|
124
|
+
for (const file of fs.readdirSync(pagesDir)) {
|
|
125
|
+
if (!file.endsWith('.json')) continue;
|
|
126
|
+
const nested = JSON.parse(fs.readFileSync(path.join(pagesDir, file), 'utf-8'));
|
|
127
|
+
const flat = flattenObject(nested);
|
|
128
|
+
plan('write', `content/en/pages/${file} (${Object.keys(flat).length} keys)`);
|
|
129
|
+
if (commit) {
|
|
130
|
+
fs.mkdirSync(enPagesDir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(enPagesDir, file), sortedJson(flat));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Split i18n flat files into locale directories
|
|
137
|
+
const i18nDir = path.join(configDir, 'i18n');
|
|
138
|
+
if (fs.existsSync(i18nDir)) {
|
|
139
|
+
// Read page filenames from config/pages/ to know valid page scopes
|
|
140
|
+
const pageFiles = fs.existsSync(pagesDir)
|
|
141
|
+
? fs.readdirSync(pagesDir).filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', ''))
|
|
142
|
+
: [];
|
|
143
|
+
|
|
144
|
+
for (const file of fs.readdirSync(i18nDir)) {
|
|
145
|
+
if (!file.endsWith('.json')) continue;
|
|
146
|
+
const locale = file.replace('.json', '');
|
|
147
|
+
const localeDir = path.join(contentDir, locale);
|
|
148
|
+
const localePagesDir = path.join(localeDir, 'pages');
|
|
149
|
+
|
|
150
|
+
const raw = JSON.parse(fs.readFileSync(path.join(i18nDir, file), 'utf-8'));
|
|
151
|
+
|
|
152
|
+
// Buckets: site.*, shared.*, pages.{pageId}.*
|
|
153
|
+
const siteBucket = {};
|
|
154
|
+
const sharedBucket = {};
|
|
155
|
+
const pageBuckets = {};
|
|
156
|
+
|
|
157
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
158
|
+
if (key.startsWith('$')) continue;
|
|
159
|
+
|
|
160
|
+
if (key.startsWith('site.')) {
|
|
161
|
+
siteBucket[key.slice('site.'.length)] = value;
|
|
162
|
+
} else if (key.startsWith('shared.')) {
|
|
163
|
+
sharedBucket[key.slice('shared.'.length)] = value;
|
|
164
|
+
} else if (key.startsWith('pages.')) {
|
|
165
|
+
const rest = key.slice('pages.'.length);
|
|
166
|
+
const dotIdx = rest.indexOf('.');
|
|
167
|
+
if (dotIdx === -1) continue;
|
|
168
|
+
const pageId = rest.slice(0, dotIdx);
|
|
169
|
+
const pageKey = rest.slice(dotIdx + 1);
|
|
170
|
+
if (!pageBuckets[pageId]) pageBuckets[pageId] = {};
|
|
171
|
+
pageBuckets[pageId][pageKey] = value;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (Object.keys(siteBucket).length > 0) {
|
|
176
|
+
plan('write', `content/${locale}/site.json (${Object.keys(siteBucket).length} keys)`);
|
|
177
|
+
if (commit) {
|
|
178
|
+
fs.mkdirSync(localeDir, { recursive: true });
|
|
179
|
+
fs.writeFileSync(path.join(localeDir, 'site.json'), sortedJson(siteBucket));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (Object.keys(sharedBucket).length > 0) {
|
|
184
|
+
plan('write', `content/${locale}/shared.json (${Object.keys(sharedBucket).length} keys)`);
|
|
185
|
+
if (commit) {
|
|
186
|
+
fs.mkdirSync(localeDir, { recursive: true });
|
|
187
|
+
fs.writeFileSync(path.join(localeDir, 'shared.json'), sortedJson(sharedBucket));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const [pageId, pageData] of Object.entries(pageBuckets)) {
|
|
192
|
+
if (Object.keys(pageData).length === 0) continue;
|
|
193
|
+
plan('write', `content/${locale}/pages/${pageId}.json (${Object.keys(pageData).length} keys)`);
|
|
194
|
+
if (commit) {
|
|
195
|
+
fs.mkdirSync(localePagesDir, { recursive: true });
|
|
196
|
+
fs.writeFileSync(path.join(localePagesDir, `${pageId}.json`), sortedJson(pageData));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 3. Create content.config.json
|
|
203
|
+
plan('write', 'content/content.config.json');
|
|
204
|
+
if (commit) {
|
|
205
|
+
fs.mkdirSync(contentDir, { recursive: true });
|
|
206
|
+
fs.writeFileSync(
|
|
207
|
+
path.join(contentDir, 'content.config.json'),
|
|
208
|
+
JSON.stringify({ baseLocale: 'en' }, null, 2) + '\n'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 4. Remove old config directory
|
|
213
|
+
if (commit) {
|
|
214
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
215
|
+
plan('remove', 'config/ directory');
|
|
216
|
+
} else {
|
|
217
|
+
plan('remove', 'config/ directory (would be removed)');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`\n${commit ? 'Migration complete' : 'Dry run complete'}. ${actions.length} actions.`);
|
|
221
|
+
if (!commit) {
|
|
222
|
+
console.log('Run with --commit to apply changes.');
|
|
223
|
+
}
|
|
@@ -85,14 +85,14 @@ import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } fr
|
|
|
85
85
|
import { resolveAsset } from '../utils/assetResolver.js';
|
|
86
86
|
import { trackEvent } from '../utils/analytics.js';
|
|
87
87
|
import { getExtensionComponent } from '../extensions/extensionLoader.js';
|
|
88
|
-
import
|
|
88
|
+
import { availableLocales as siteLocales } from '../utils/loadConfig.js';
|
|
89
89
|
|
|
90
90
|
const logoSrc = computed(() => resolveAsset('img/logo.png'));
|
|
91
91
|
|
|
92
92
|
const isLangOpen = ref(false);
|
|
93
93
|
const isHeaderCompact = ref(false);
|
|
94
94
|
const currentLocale = ref('');
|
|
95
|
-
const availableLocales = computed(() =>
|
|
95
|
+
const availableLocales = computed(() => siteLocales.map((code) => code.trim()).filter(Boolean));
|
|
96
96
|
const localeLabels = {
|
|
97
97
|
en: 'English',
|
|
98
98
|
fr: 'Français',
|
package/src/utils/loadConfig.js
CHANGED
|
@@ -133,44 +133,84 @@ export function mergeConfigTrees(target, source, options = {}) {
|
|
|
133
133
|
export function createConfigLoader(allModules) {
|
|
134
134
|
const toPlain = unwrapDefault;
|
|
135
135
|
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
function
|
|
136
|
+
// Discover which locale directories exist in the content folder by
|
|
137
|
+
// extracting unique locale codes from the allModules glob keys.
|
|
138
|
+
function discoverAvailableLocales() {
|
|
139
|
+
const locales = new Set();
|
|
140
|
+
const re = /\/content\/([a-z]{2,3}(?:-[a-zA-Z]{2,4})?)\//;
|
|
141
|
+
for (const key of Object.keys(allModules)) {
|
|
142
|
+
const match = key.match(re);
|
|
143
|
+
if (match) locales.add(match[1].toLowerCase());
|
|
144
|
+
}
|
|
145
|
+
return Array.from(locales).sort();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const availableLocales = discoverAvailableLocales();
|
|
149
|
+
|
|
150
|
+
// Find the content root (path ending in `/content/`) and resolve the base
|
|
151
|
+
// locale from content.config.json.
|
|
152
|
+
function findContentRoot() {
|
|
139
153
|
const keys = Object.keys(allModules);
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
return key.slice(0, key.length - 'site.json'.length);
|
|
154
|
+
const configKey = keys.find((key) => key.endsWith('/content/content.config.json'));
|
|
155
|
+
if (configKey) {
|
|
156
|
+
return configKey.slice(0, configKey.length - 'content.config.json'.length);
|
|
144
157
|
}
|
|
145
158
|
return null;
|
|
146
159
|
}
|
|
147
160
|
|
|
148
|
-
async function
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
161
|
+
async function resolveBaseLocale(contentRoot) {
|
|
162
|
+
const configLoader = allModules[`${contentRoot}content.config.json`];
|
|
163
|
+
if (!configLoader) return 'en';
|
|
164
|
+
const config = toPlain(await configLoader());
|
|
165
|
+
return (config && config.baseLocale) || 'en';
|
|
166
|
+
}
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
// Assemble a config from a locale directory: inflates flat-format files
|
|
169
|
+
// (site.json, shared.json, pages/*.json) and returns { site, shared, pages }.
|
|
170
|
+
// When require is true, returns null if site.json is missing (used for
|
|
171
|
+
// base config). When false, assembles whatever files exist (used for
|
|
172
|
+
// locale overrides that may only contain partial translations).
|
|
173
|
+
async function assembleSplitConfig(localePrefix, { require: requireSite = false } = {}) {
|
|
174
|
+
const siteLoader = allModules[`${localePrefix}site.json`];
|
|
175
|
+
if (!siteLoader && requireSite) return null;
|
|
176
|
+
|
|
177
|
+
const sharedLoader = allModules[`${localePrefix}shared.json`];
|
|
178
|
+
const [rawSite, rawShared] = await Promise.all([
|
|
179
|
+
siteLoader ? siteLoader().then(toPlain) : Promise.resolve({}),
|
|
158
180
|
sharedLoader ? sharedLoader().then(toPlain) : Promise.resolve({}),
|
|
159
181
|
]);
|
|
182
|
+
const siteData = inflateFlatConfig(rawSite);
|
|
183
|
+
const sharedData = inflateFlatConfig(rawShared);
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
const pagePrefix = `${prefix}pages/`;
|
|
185
|
+
const pagePrefix = `${localePrefix}pages/`;
|
|
163
186
|
const pages = {};
|
|
164
187
|
const pageLoads = [];
|
|
165
188
|
for (const [path, loader] of Object.entries(allModules)) {
|
|
166
189
|
if (path.startsWith(pagePrefix) && path.endsWith('.json')) {
|
|
167
190
|
const pageId = path.slice(pagePrefix.length, -5);
|
|
168
|
-
pageLoads.push(
|
|
191
|
+
pageLoads.push(
|
|
192
|
+
loader().then((mod) => { pages[pageId] = inflateFlatConfig(toPlain(mod)); })
|
|
193
|
+
);
|
|
169
194
|
}
|
|
170
195
|
}
|
|
171
196
|
await Promise.all(pageLoads);
|
|
172
197
|
|
|
173
|
-
|
|
198
|
+
// Return null if no files were found at all for this locale
|
|
199
|
+
if (!siteLoader && !sharedLoader && Object.keys(pages).length === 0) return null;
|
|
200
|
+
|
|
201
|
+
return { site: siteData, shared: sharedData, pages };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function assembleBaseConfig() {
|
|
205
|
+
const contentRoot = findContentRoot();
|
|
206
|
+
if (!contentRoot) return null;
|
|
207
|
+
|
|
208
|
+
const baseLocale = await resolveBaseLocale(contentRoot);
|
|
209
|
+
const basePrefix = `${contentRoot}${baseLocale}/`;
|
|
210
|
+
const config = await assembleSplitConfig(basePrefix, { require: true });
|
|
211
|
+
if (!config) return null;
|
|
212
|
+
|
|
213
|
+
return { ...config, _contentRoot: contentRoot, _baseLocale: baseLocale };
|
|
174
214
|
}
|
|
175
215
|
|
|
176
216
|
async function loadConfigData(options) {
|
|
@@ -202,22 +242,24 @@ export function createConfigLoader(allModules) {
|
|
|
202
242
|
|
|
203
243
|
if (normalizedLocale === undefined) {
|
|
204
244
|
const config = await getBaseConfig();
|
|
205
|
-
delete config.
|
|
245
|
+
delete config._contentRoot;
|
|
246
|
+
delete config._baseLocale;
|
|
206
247
|
return config;
|
|
207
248
|
}
|
|
208
249
|
|
|
209
250
|
const baseConfig = await getBaseConfig();
|
|
210
251
|
|
|
211
252
|
let mergedConfig = baseConfig;
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
const contentRoot = baseConfig._contentRoot;
|
|
254
|
+
const baseLocale = baseConfig._baseLocale;
|
|
255
|
+
|
|
256
|
+
// Only load locale overrides if the requested locale differs from the base
|
|
257
|
+
if (contentRoot && normalizedLocale !== baseLocale) {
|
|
258
|
+
const localePrefix = `${contentRoot}${normalizedLocale}/`;
|
|
259
|
+
const localeConfig = await assembleSplitConfig(localePrefix);
|
|
260
|
+
if (localeConfig) {
|
|
261
|
+
mergedConfig = mergeConfigTrees(baseConfig, localeConfig, { cloneTarget: true, skipEmpty: true });
|
|
262
|
+
}
|
|
221
263
|
}
|
|
222
264
|
if (mergedConfig && mergedConfig.pages && typeof mergedConfig.pages === 'object') {
|
|
223
265
|
for (const page of Object.values(mergedConfig.pages)) {
|
|
@@ -247,11 +289,12 @@ export function createConfigLoader(allModules) {
|
|
|
247
289
|
});
|
|
248
290
|
}
|
|
249
291
|
}
|
|
250
|
-
delete mergedConfig.
|
|
292
|
+
delete mergedConfig._contentRoot;
|
|
293
|
+
delete mergedConfig._baseLocale;
|
|
251
294
|
return mergedConfig;
|
|
252
295
|
}
|
|
253
296
|
|
|
254
|
-
return { loadConfigData, mergeConfigTrees, cloneConfig };
|
|
297
|
+
return { loadConfigData, mergeConfigTrees, cloneConfig, availableLocales };
|
|
255
298
|
}
|
|
256
299
|
|
|
257
300
|
// ---- Runtime singleton ----
|
|
@@ -263,9 +306,14 @@ let _loadConfigData = async () => {
|
|
|
263
306
|
throw new Error('[@koehler8/cms] loadConfigData() called before config loader was initialized');
|
|
264
307
|
};
|
|
265
308
|
|
|
309
|
+
let _availableLocales = [];
|
|
310
|
+
|
|
266
311
|
export function setConfigLoader(instance) {
|
|
267
312
|
if (!instance) return;
|
|
268
313
|
_loadConfigData = instance.loadConfigData;
|
|
314
|
+
_availableLocales = instance.availableLocales || [];
|
|
269
315
|
}
|
|
270
316
|
|
|
317
|
+
export { _availableLocales as availableLocales };
|
|
318
|
+
|
|
271
319
|
export const loadConfigData = (...args) => _loadConfigData(...args);
|
package/vite-plugin.js
CHANGED
|
@@ -12,24 +12,25 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import vue from '@vitejs/plugin-vue';
|
|
13
13
|
|
|
14
14
|
import { SUPPORTED_LOCALES } from './src/constants/locales.js';
|
|
15
|
+
import { inflateFlatConfig } from './src/utils/inflateFlatConfig.js';
|
|
15
16
|
|
|
16
17
|
// ---- Helpers ----
|
|
17
18
|
|
|
18
|
-
function loadSplitConfig(
|
|
19
|
-
const sitePath = path.join(
|
|
19
|
+
function loadSplitConfig(contentLocaleDir) {
|
|
20
|
+
const sitePath = path.join(contentLocaleDir, 'site.json');
|
|
20
21
|
if (!fs.existsSync(sitePath)) return null;
|
|
21
22
|
|
|
22
|
-
const site = JSON.parse(fs.readFileSync(sitePath, 'utf-8'));
|
|
23
|
-
const sharedPath = path.join(
|
|
24
|
-
const shared = fs.existsSync(sharedPath) ? JSON.parse(fs.readFileSync(sharedPath, 'utf-8')) : {};
|
|
23
|
+
const site = inflateFlatConfig(JSON.parse(fs.readFileSync(sitePath, 'utf-8')));
|
|
24
|
+
const sharedPath = path.join(contentLocaleDir, 'shared.json');
|
|
25
|
+
const shared = fs.existsSync(sharedPath) ? inflateFlatConfig(JSON.parse(fs.readFileSync(sharedPath, 'utf-8'))) : {};
|
|
25
26
|
|
|
26
|
-
const pagesDir = path.join(
|
|
27
|
+
const pagesDir = path.join(contentLocaleDir, 'pages');
|
|
27
28
|
const pages = {};
|
|
28
29
|
if (fs.existsSync(pagesDir)) {
|
|
29
30
|
for (const file of fs.readdirSync(pagesDir)) {
|
|
30
31
|
if (!file.endsWith('.json')) continue;
|
|
31
32
|
const pageId = file.replace('.json', '');
|
|
32
|
-
pages[pageId] = JSON.parse(fs.readFileSync(path.join(pagesDir, file), 'utf-8'));
|
|
33
|
+
pages[pageId] = inflateFlatConfig(JSON.parse(fs.readFileSync(path.join(pagesDir, file), 'utf-8')));
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -356,7 +357,13 @@ export default function cmsPlugin(options = {}) {
|
|
|
356
357
|
tempEntryPath = path.join(siteRoot, ENTRY_FILENAME);
|
|
357
358
|
|
|
358
359
|
// Load site config at config-resolution time (needed for SSG routes + HTML injection)
|
|
359
|
-
const
|
|
360
|
+
const contentDir = path.join(siteDir, 'content');
|
|
361
|
+
const contentConfigPath = path.join(contentDir, 'content.config.json');
|
|
362
|
+
const contentConfig = fs.existsSync(contentConfigPath)
|
|
363
|
+
? JSON.parse(fs.readFileSync(contentConfigPath, 'utf-8'))
|
|
364
|
+
: {};
|
|
365
|
+
const baseLocale = contentConfig.baseLocale || 'en';
|
|
366
|
+
const configDir = path.join(contentDir, baseLocale);
|
|
360
367
|
siteConfig = loadSplitConfig(configDir);
|
|
361
368
|
if (!siteConfig) {
|
|
362
369
|
throw new Error(`[@koehler8/cms] site config not found at ${configDir}`);
|
|
@@ -431,7 +438,7 @@ export default function cmsPlugin(options = {}) {
|
|
|
431
438
|
// native ESM resolver cannot handle. Marking them as noExternal
|
|
432
439
|
// tells Vite to bundle them during SSR so its Vue/JSON plugins
|
|
433
440
|
// process the files instead of Node.
|
|
434
|
-
noExternal: ['@koehler8/cms', ...extensionPackages],
|
|
441
|
+
noExternal: ['@koehler8/cms', ...themePackages, ...extensionPackages],
|
|
435
442
|
},
|
|
436
443
|
test: {
|
|
437
444
|
environment: 'happy-dom',
|
|
@@ -479,11 +486,12 @@ export default function cmsPlugin(options = {}) {
|
|
|
479
486
|
if (virtualId === VIRTUAL_CONFIG) {
|
|
480
487
|
return `
|
|
481
488
|
import { createConfigLoader } from '@koehler8/cms/utils/loadConfig';
|
|
482
|
-
const allModules = import.meta.glob('@cms-site/
|
|
489
|
+
const allModules = import.meta.glob('@cms-site/content/**/*.json');
|
|
483
490
|
const loader = createConfigLoader(allModules);
|
|
484
491
|
export const loadConfigData = loader.loadConfigData;
|
|
485
492
|
export const mergeConfigTrees = loader.mergeConfigTrees;
|
|
486
493
|
export const cloneConfig = loader.cloneConfig;
|
|
494
|
+
export const availableLocales = loader.availableLocales;
|
|
487
495
|
`;
|
|
488
496
|
}
|
|
489
497
|
|