@lad-sapienza/scms-core 0.1.0-alpha.9 → 0.1.0

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 CHANGED
@@ -10,7 +10,20 @@ This package supplies the `core/` framework layer that used to live inside the s
10
10
  npm install @lad-sapienza/scms-core
11
11
  ```
12
12
 
13
- Peer dependencies (install alongside): `astro`, `react`, `react-dom`, `maplibre-gl`, `@types/react`, `@types/react-dom`.
13
+ Peer dependencies (install alongside): `astro`, `react`, `react-dom`, `maplibre-gl` (v6+), `@types/react`, `@types/react-dom`.
14
+
15
+ ### Requirements
16
+
17
+ scms-core is an **Astro** package. It ships TypeScript, React and `.astro` sources
18
+ that the host site's Astro/Vite pipeline compiles; it is not usable from plain
19
+ Node, or from tooling that doesn't understand `.astro` and `.tsx`. Astro is a
20
+ requirement of the package, not an optional extra.
21
+
22
+ | Dependency | Minimum |
23
+ |---|---|
24
+ | `astro` | 7.2.4 |
25
+ | `react`, `react-dom` | 19.2.4 |
26
+ | `maplibre-gl` | 6.7.0 (needs WebGL2) |
14
27
 
15
28
  ## Usage
16
29
 
@@ -37,7 +50,7 @@ export default defineConfig({
37
50
  });
38
51
  ```
39
52
 
40
- `scms()` bundles content-asset serving, the Gallery virtual module, Expressive Code, MDX, React, and sitemap integrations into one call, and self-registers the Vite `dedupe`/`optimizeDeps` config React and `@tanstack/react-table` need to avoid duplicate-module "Invalid hook call" errors — no extra Vite config required in the consumer.
53
+ `scms()` bundles content-asset serving, the Gallery virtual module, Expressive Code, MDX, React, and sitemap integrations into one call, and self-registers the Vite config the components need — `dedupe`/`optimizeDeps` for React and `@tanstack/react-table` (avoids duplicate-module "Invalid hook call" errors) and `ssr.noExternal` for `maplibre-gl` (so the `Map` component's MapLibre v6 worker import resolves in the SSR pass). No extra Vite config required in the consumer.
41
54
 
42
55
  Import components from the package root in any `.mdx` file:
43
56
 
@@ -48,7 +61,7 @@ import { DataTb, Map, Gallery } from '@lad-sapienza/scms-core';
48
61
  | Component | Description |
49
62
  |---|---|
50
63
  | `DataTb` | Sortable, filterable, paginated data table |
51
- | `MapComponent` (`Map`) | Interactive map with MapLibre GL JS |
64
+ | `Map` | Interactive map with MapLibre GL JS |
52
65
  | `Gallery` | Responsive image gallery with lightbox |
53
66
  | `TableOfContents` | Auto-generated TOC from headings |
54
67
  | `RedirectPage` | Static client-side redirect stub (meta-refresh + JS fallback) for static hosts |
@@ -56,7 +69,27 @@ import { DataTb, Map, Gallery } from '@lad-sapienza/scms-core';
56
69
  | `RecordProvider`, `Field`, `Image`, `RecordFetcher`, `useRecordFetcher` | Build single-record detail pages against Directus |
57
70
  | `SearchUI`, `SearchUISimple`, `SearchUIAdvanced` | Field/operator/value search UI (used by `Map`'s `searchInFields`) |
58
71
 
59
- A few components are reachable only via subpath import, not the root barrel — this mirrors the internal layout, no bundler config or `exports` map restricts it:
72
+ Only the entry points below are public API: the package declares an
73
+ [`exports`](https://nodejs.org/api/packages.html#exports) map, and importing any
74
+ other path (e.g. `.../utils/...`, `.../components/DataTb/DataTbMdx`) fails to
75
+ resolve. Everything is also available from the root barrel except the `.astro`
76
+ components and the ones listed here:
77
+
78
+ | Import path | Provides |
79
+ |---|---|
80
+ | `@lad-sapienza/scms-core` | root barrel — `DataTb`, `Map`, `Gallery`, `Record*`, `RedirectPage`, `ZoteroGeoViewer`, i18n helpers |
81
+ | `@lad-sapienza/scms-core/scms` | `scms()` — use this (not the root) in `astro.config.mjs` |
82
+ | `.../components/Map` | `Map`, map types, `defaultBasemaps` |
83
+ | `.../components/DataTb`, `.../components/DataTb/types` | `DataTb` and the source components; table types |
84
+ | `.../components/Gallery`, `.../components/Gallery/Gallery.astro` | `Gallery` (named export / `.astro` default) |
85
+ | `.../components/Record` | `RecordProvider`, `Field`, `Image`, `RecordFetcher`, `useRecordFetcher` |
86
+ | `.../components/ZoteroGeoViewer` | `ZoteroGeoViewer` |
87
+ | `.../components/TableOfContents/TableOfContents.astro` | `TableOfContents` |
88
+ | `.../components/SEO/SEO.astro` | `SEO` |
89
+ | `.../components/BSNavbar`, `.../components/LocaleSwitcher`, `.../components/ThemeToggle` | layout components (default exports) |
90
+ | `.../components/RedirectPage/RedirectPage.astro` | `RedirectPage` |
91
+ | `.../components/i18n`, `.../components/i18n/I18nInit.astro` | translation helpers; `I18nInit` |
92
+ | `.../integrations/directusLoader` | `directusLoader` for `content.config.ts` |
60
93
 
61
94
  ```js
62
95
  import SEO from '@lad-sapienza/scms-core/components/SEO/SEO.astro';
@@ -87,6 +120,98 @@ npx scms-add-content # add a new content file to an existing collection
87
120
 
88
121
  A consuming site typically wires these up as `npm run add-collection` / `npm run add-content` in its own `package.json` (this is what `scms-create` sets up automatically).
89
122
 
123
+ Both commands are multilingual-aware. `scms-add-collection` optionally takes a list of locale codes and scaffolds one sample per `src/content/<name>/<locale>/` folder; `scms-add-content` detects those language folders (every sub-folder is a locale code, nothing loose at the collection root) and asks which language the new file belongs in — a single locale, a comma-separated subset, or `all`.
124
+
125
+ ## Image optimization
126
+
127
+ `scms-optimize-images` is a maintenance command (also a `bin`, run from the site's project root) that keeps the images in your content light: it converts every JPG/JPEG/PNG under the content folder to WebP, scales them down to a maximum size, deletes the originals and rewrites every reference to them in your `.md`/`.mdx` files. It uses [sharp](https://sharp.pixelplumbing.com/), so nothing else needs to be installed on the machine.
128
+
129
+ ```bash
130
+ npx scms-optimize-images # convert, delete the originals, rewrite references
131
+ npx scms-optimize-images --dry-run # print the estimated savings, touch nothing
132
+ npx scms-optimize-images --check # touch nothing; exit 1 if something is left to do
133
+ npx scms-optimize-images --only a.jpg b.png # limit the work to these files
134
+ ```
135
+
136
+ Wire it up in the site's `package.json`:
137
+
138
+ ```json
139
+ {
140
+ "scripts": {
141
+ "images": "scms-optimize-images",
142
+ "prebuild": "scms-optimize-images --check"
143
+ }
144
+ }
145
+ ```
146
+
147
+ `--check` never changes anything — the build must not modify or delete source files. It exits with a non-zero code (and lists the offenders) if it finds a JPG/PNG that could be converted, a WebP larger than the maximum size, or a stale reference from code (see below), so a forgotten image fails the build or the CI instead of being published at 5 MB. Fix it locally with `npm run images`, review the diff, commit.
148
+
149
+ ### What it does
150
+
151
+ - **Resize**: the long side is limited to `maxSize` (2000 px); images are only ever scaled down. EXIF orientation is applied and metadata stripped.
152
+ - **JPG → lossy WebP** at quality 82. **PNG → lossless WebP**; if the result is still larger than 50% of the original *and* the original is over 500 KB, it falls back to lossy WebP (quality 85, alpha quality 100).
153
+ - **Not lighter, not converted**: when the WebP would not be smaller than the original, the original is left as it is. The verdict is stored (file hash + settings) in `.scms-optimize-images.json` at the project root — **commit it** — so such images are neither re-encoded on every run nor reported again by `--check`. Replace the file, or change a setting, and it is evaluated again.
154
+ - **References** in `.md`/`.mdx` files (frontmatter such as `img:`, `![](…)`, relative and absolute paths) are rewritten. Absolute paths are rooted at the content folder, as in `contentAssetsIntegration`. `%20` is decoded and names are compared case-insensitively.
155
+ - **Languages**: pages in a non-default language usually have no assets of their own and reuse those of the default-language twin (`blog/en/post/index.md` citing `a.jpg` that lives in `blog/it/post/`). Such references are resolved by swapping the language folder for the default one — using the languages declared in `userConfig.i18n` (below), not fixed codes.
156
+ - **Name conflicts**: `bdus.jpg` + `bdus.png` would both become `bdus.webp`. The one cited by a `.md`/`.mdx` in the same folder (or its twin in another language) is kept, the other is deleted; if that does not single one out, both are skipped and reported.
157
+ - **WebP already larger than `maxSize`** are scaled down in place (lossless stays lossless, lossy is re-encoded at `quality`), and reported by `--check`.
158
+ - **Idempotent**: a second run changes nothing (and takes a fraction of a second).
159
+ - The report lists converted files, skipped ones (with the reason), deleted duplicates and converted WebP that nothing cites (images in `gallery/` folders are not expected to be cited).
160
+
161
+ ### References in code
162
+
163
+ Only `.md`/`.mdx` files are rewritten. Images can also be referenced from `.astro`, `.jsx`, `.tsx`, `.ts`, `.js` and `.mjs` files under `src/` (`<img src="/didattica/cover.jpg">`, `import x from '@content/…'`): those are **never rewritten** — every one that points at an image that is, or was, converted is reported with file and line, and the command exits with an error:
164
+
165
+ ```
166
+ src/pages/[locale]/didattica/index.astro:22 /didattica/cover.jpg is now broken: the image was converted → use /didattica/cover.webp
167
+ ```
168
+
169
+ The report keeps coming back (also from `--check`) until the code is fixed, so a broken link cannot slip through silently. Paths are resolved from the content folder (`/…`), from the file (`./…`, `../…`) and through the `@content/` and `@user/` aliases.
170
+
171
+ ### Configuration
172
+
173
+ All optional; defaults shown. In `src/user.config.mjs`:
174
+
175
+ ```js
176
+ export const userConfig = {
177
+ // Languages of the site. Pages in a language other than defaultLocale may reuse
178
+ // the images of the defaultLocale twin. Omit for a monolingual site.
179
+ i18n: {
180
+ defaultLocale: 'it',
181
+ locales: ['it', 'en'],
182
+ },
183
+
184
+ images: {
185
+ contentDir: 'src/content', // scanned for images and .md/.mdx files
186
+ srcDir: 'src', // scanned for references from code
187
+ maxSize: 2000, // px, long side
188
+ quality: 82, // WebP quality for JPGs (and for re-encoded lossy WebP)
189
+ pngLossyRatio: 0.5, // PNG: go lossy if lossless WebP > this fraction of the original...
190
+ pngLossyMinSize: 500, // ...and the original is larger than this many KB
191
+ pngLossyQuality: 85, // quality of that lossy fallback
192
+ exclude: [], // folders left alone: paths relative to contentDir, or plain folder names
193
+ },
194
+ };
195
+ ```
196
+
197
+ Keep `i18n` in sync with the site's own locale list (e.g. `LOCALES`/`DEFAULT_LOCALE` in `src/utils/i18n.ts`). Images outside the content folder (e.g. `public/`) are never touched.
198
+
199
+ ### Optional: pre-commit hook
200
+
201
+ Nothing is installed automatically. If you want images checked at commit time, add `.git/hooks/pre-commit` (or the equivalent in husky/lefthook) and make it executable. It only looks at the images staged in the commit:
202
+
203
+ ```sh
204
+ #!/bin/sh
205
+ # Block the commit if a staged image is not optimized
206
+ git diff --cached --name-only --diff-filter=AM -z -- '*.jpg' '*.jpeg' '*.png' '*.JPG' '*.JPEG' '*.PNG' '*.webp' \
207
+ | xargs -0 npx scms-optimize-images --check --only || {
208
+ echo "Run 'npm run images', review the changes and stage them." >&2
209
+ exit 1
210
+ }
211
+ ```
212
+
213
+ An empty list is fine: `--only` with no files does nothing and exits 0. To convert instead of just blocking, run `npx scms-optimize-images --only <files>` and then `git add` the converted `.webp` files and the rewritten `.md`/`.mdx` — the command never stages anything itself.
214
+
90
215
  ## Development
91
216
 
92
217
  ```bash
@@ -468,6 +468,27 @@ async function main() {
468
468
  const typeRaw = (await ask('Choose type [1/2/3] (default: 3): ')).trim() || '3';
469
469
  const colType = typeRaw === '1' ? 'blog' : typeRaw === '2' ? 'docs' : 'generic';
470
470
  ok(`Type: ${colType}`);
471
+
472
+ // ─── Prompt: language folders (multilingual collection) ──────────────────
473
+ // A site can organise a collection's content per language, as
474
+ // src/content/<col>/<locale>/… — s:CMS's i18n layer is structural, it keys
475
+ // off those folders existing rather than a config flag. Ask up front so the
476
+ // sample file(s) land in the right place. (The page templates written below
477
+ // are single-language starting points regardless — see the closing notes.)
478
+ console.log('');
479
+ console.log(`${B}Language folders${X} — for a multilingual collection, list the locale`);
480
+ console.log(` codes to scaffold (e.g. ${B}en, it${X}). Leave blank for a single-language collection.`);
481
+ let locales = [];
482
+ while (true) {
483
+ const raw = (await ask(' Locale codes [blank = none]: ')).trim();
484
+ if (!raw) break;
485
+ const parsed = raw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
486
+ const bad = parsed.filter(p => !/^[a-z]{2}(?:[-_][a-z0-9]{2,4})?$/i.test(p));
487
+ if (bad.length) { warn(`Not a locale code: ${bad.join(', ')} (expected e.g. en, it, pt-BR)`); continue; }
488
+ locales = [...new Set(parsed)];
489
+ break;
490
+ }
491
+ if (locales.length) ok(`Language folders: ${locales.join(', ')}`);
471
492
  rl.close();
472
493
 
473
494
  // ─── 1. Update src/content.config.ts ──────────────────────────────────────
@@ -505,10 +526,9 @@ async function main() {
505
526
  writeFileSync(CONFIG_FILE, before + after, 'utf8');
506
527
  ok('src/content.config.ts updated');
507
528
 
508
- // ─── 2. Sample content file ───────────────────────────────────────────────
529
+ // ─── 2. Sample content file(s) ───────────────────────────────────────────
509
530
  const today = new Date().toISOString().slice(0, 10);
510
531
  const contentDir = join(ROOT_DIR, 'src', 'content', colName);
511
- mkdirSync(contentDir, { recursive: true });
512
532
 
513
533
  const SAMPLE_NAME = { blog: 'sample-post.md', docs: 'sample-doc.md', generic: 'sample.md' };
514
534
  const SAMPLE_BODY = {
@@ -529,9 +549,16 @@ async function main() {
529
549
  `# Sample Entry\n\nThis is a sample entry. Replace this file with real content.\n`,
530
550
  };
531
551
 
532
- const samplePath = join(contentDir, SAMPLE_NAME[colType]);
533
- writeFileSync(samplePath, SAMPLE_BODY[colType], 'utf8');
534
- ok(relative(ROOT_DIR, samplePath));
552
+ // One sample per language folder for a multilingual collection, otherwise a
553
+ // single sample in the collection root. The copies are identical starting
554
+ // points — translations get written by hand afterwards.
555
+ const sampleDirs = locales.length ? locales.map(loc => join(contentDir, loc)) : [contentDir];
556
+ for (const dir of sampleDirs) {
557
+ mkdirSync(dir, { recursive: true });
558
+ const samplePath = join(dir, SAMPLE_NAME[colType]);
559
+ writeFileSync(samplePath, SAMPLE_BODY[colType], 'utf8');
560
+ ok(relative(ROOT_DIR, samplePath));
561
+ }
535
562
 
536
563
  // ─── 3. Page templates ────────────────────────────────────────────────────
537
564
  const pagesDir = join(ROOT_DIR, 'src', 'pages', colName);
@@ -553,7 +580,11 @@ async function main() {
553
580
  console.log(` ${B}Next steps:${X}`);
554
581
  console.log(` 1. Open ${B}src/content.config.ts${X} and review the schema for '${colName}'`);
555
582
  console.log(` 2. Edit the page files in ${B}src/pages/${colName}/${X} to customise the UI`);
556
- console.log(` 3. Replace the sample file in ${B}src/content/${colName}/${X} with real content`);
583
+ if (locales.length) {
584
+ console.log(` ${Y}These render a single language — adapt them to your locale`);
585
+ console.log(` routing (e.g. move them under src/pages/[locale]/${colName}/).${X}`);
586
+ }
587
+ console.log(` 3. Replace the sample file${locales.length ? 's' : ''} in ${B}src/content/${colName}/${X} with real content`);
557
588
  console.log(` 4. Run ${B}npm run dev${X} and visit ${B}/${colName}${X}`);
558
589
  console.log('');
559
590
  }
@@ -75,6 +75,36 @@ function hasMdx(dir) {
75
75
  } catch { return false; }
76
76
  }
77
77
 
78
+ /** True if any .md/.mdx file sits directly in dir (not in a sub-folder) */
79
+ function hasRootContentFiles(dir) {
80
+ try {
81
+ return readdirSync(dir, { withFileTypes: true })
82
+ .some(d => d.isFile() && /\.mdx?$/.test(d.name));
83
+ } catch { return false; }
84
+ }
85
+
86
+ /**
87
+ * Locale-code sub-directories directly inside a collection dir — e.g.
88
+ * `en`, `it`, `pt-BR`. Their presence is how a site opts a collection in to
89
+ * per-language content: there is no config flag (s:CMS's i18n layer is
90
+ * structural — it keys off these folders existing). The pattern is a
91
+ * 2-letter base with an optional region/script suffix (`pt-BR`, `zh-Hans`),
92
+ * and — to keep ordinary topic folders (`api`, `guides`) from being taken
93
+ * for languages — this returns nothing unless *every* sub-folder matches.
94
+ * Sorted, for a stable prompt order.
95
+ */
96
+ function detectLocaleDirs(dir) {
97
+ let subdirs;
98
+ try {
99
+ subdirs = readdirSync(dir, { withFileTypes: true })
100
+ .filter(d => d.isDirectory())
101
+ .map(d => d.name);
102
+ } catch { return []; }
103
+ if (subdirs.length === 0) return [];
104
+ const locales = subdirs.filter(n => /^[a-z]{2}(?:[-_][a-z0-9]{2,4})?$/i.test(n));
105
+ return locales.length === subdirs.length ? locales.sort() : [];
106
+ }
107
+
78
108
  /** Extract z.object field lines for a named collection from config.ts */
79
109
  function parseSchemaFields(src, colName) {
80
110
  const pat = new RegExp(
@@ -163,9 +193,37 @@ async function main() {
163
193
  }
164
194
  ok(`Collection: ${collection}`);
165
195
 
196
+ const colDir = join(CONTENT_BASE, collection);
197
+
198
+ // ─── Prompt: language folder (multilingual collections only) ─────────────
199
+ // A collection counts as multilingual purely by its layout: content split
200
+ // into locale-named sub-folders (src/content/<col>/<locale>/…) with nothing
201
+ // loose at the collection root. When that's the case, ask which locale
202
+ // folder(s) the new file belongs in — one, a comma-separated subset, or
203
+ // `all`. Collections that aren't organised this way skip the step entirely
204
+ // and behave exactly as before.
205
+ const localeDirs = hasRootContentFiles(colDir) ? [] : detectLocaleDirs(colDir);
206
+ let targetLocales = ['']; // '' → straight into the collection root (single-language)
207
+ if (localeDirs.length > 0) {
208
+ console.log('');
209
+ console.log(` This collection is organised by language folder: ${B}${localeDirs.join(', ')}${X}`);
210
+ console.log(' Enter one language, a comma-separated list, or "all".');
211
+ console.log('');
212
+ while (true) {
213
+ const raw = (await ask(`${B}Language${X} [${[...localeDirs, 'all'].join(' / ')}]: `)).trim().toLowerCase();
214
+ if (!raw) { warn('Please choose a language (or "all").'); continue; }
215
+ if (raw === 'all') { targetLocales = [...localeDirs]; break; }
216
+ const picked = raw.split(',').map(s => s.trim()).filter(Boolean);
217
+ const unknown = picked.filter(p => !localeDirs.includes(p));
218
+ if (unknown.length) { warn(`Not an available language: ${unknown.join(', ')}`); continue; }
219
+ targetLocales = [...new Set(picked)];
220
+ break;
221
+ }
222
+ ok(`Language: ${targetLocales.join(', ')}`);
223
+ }
224
+
166
225
  // ─── Prompt: file format ─────────────────────────────────────────────────
167
- const colDir = join(CONTENT_BASE, collection);
168
- const defaultExt = hasMdx(colDir) ? 'mdx' : 'md';
226
+ const defaultExt = targetLocales.some(loc => hasMdx(join(colDir, loc))) ? 'mdx' : 'md';
169
227
  console.log('');
170
228
  const extRaw = (await ask(`${B}File format${X} [md/mdx] (default: ${defaultExt}): `)).trim() || defaultExt;
171
229
  const ext = (extRaw === 'md' || extRaw === 'mdx') ? extRaw : defaultExt;
@@ -183,8 +241,9 @@ async function main() {
183
241
  warn('Slug must contain only lowercase letters, numbers, hyphens, underscores, and forward slashes.');
184
242
  continue;
185
243
  }
186
- if (existsSync(join(colDir, raw, `index.${ext}`))) {
187
- warn(`File already exists: src/content/${collection}/${raw}/index.${ext}`);
244
+ const clash = targetLocales.find(loc => existsSync(join(colDir, loc, raw, `index.${ext}`)));
245
+ if (clash !== undefined) {
246
+ warn(`File already exists: ${relative(ROOT_DIR, join(colDir, clash, raw, `index.${ext}`))}`);
188
247
  continue;
189
248
  }
190
249
  slug = raw;
@@ -234,11 +293,7 @@ async function main() {
234
293
 
235
294
  rl.close();
236
295
 
237
- // ─── Write file ───────────────────────────────────────────────────────────
238
- const targetDir = join(colDir, slug);
239
- const targetPath = join(targetDir, `index.${ext}`);
240
- mkdirSync(targetDir, { recursive: true });
241
-
296
+ // ─── Write file(s) ────────────────────────────────────────────────────────
242
297
  const content = [
243
298
  '---',
244
299
  ...fmLines,
@@ -250,19 +305,39 @@ async function main() {
250
305
  '',
251
306
  ].join('\n');
252
307
 
253
- writeFileSync(targetPath, content, 'utf8');
308
+ // One copy per chosen language folder (just one entry, '', for a
309
+ // single-language collection). Each copy is an identical starting point —
310
+ // the per-locale translations are written afterwards, by hand.
311
+ const created = [];
312
+ for (const loc of targetLocales) {
313
+ const targetDir = join(colDir, loc, slug);
314
+ const targetPath = join(targetDir, `index.${ext}`);
315
+ mkdirSync(targetDir, { recursive: true });
316
+ writeFileSync(targetPath, content, 'utf8');
317
+ created.push(relative(ROOT_DIR, targetPath));
318
+ }
254
319
 
255
320
  console.log('');
256
- ok(`Created: ${relative(ROOT_DIR, targetPath)}`);
321
+ for (const p of created) ok(`Created: ${p}`);
257
322
 
258
323
  // ─── Summary ──────────────────────────────────────────────────────────────
324
+ const multilingual = targetLocales.some(Boolean);
259
325
  console.log('');
260
326
  console.log(`${G}${B}Done!${X}`);
261
327
  console.log('');
262
328
  console.log(` ${B}Next steps:${X}`);
263
- console.log(` 1. Open ${B}src/content/${collection}/${slug}/index.${ext}${X} and write your content`);
329
+ if (multilingual) {
330
+ console.log(` 1. Open the new file${created.length > 1 ? 's' : ''} and write ${created.length > 1 ? 'each translation' : 'your content'}:`);
331
+ for (const p of created) console.log(` ${B}${p}${X}`);
332
+ } else {
333
+ console.log(` 1. Open ${B}${created[0]}${X} and write your content`);
334
+ }
264
335
  console.log(` 2. Set ${B}draft: false${X} when the content is ready to publish`);
265
- console.log(` 3. Run ${B}npm run dev${X} and visit ${B}/${collection}/${slug}${X}`);
336
+ if (multilingual) {
337
+ console.log(` 3. Run ${B}npm run dev${X} and visit the new page (e.g. ${B}/${targetLocales[0]}/${collection}/${slug}${X}, depending on your locale routing)`);
338
+ } else {
339
+ console.log(` 3. Run ${B}npm run dev${X} and visit ${B}/${collection}/${slug}${X}`);
340
+ }
266
341
  console.log('');
267
342
  }
268
343
 
package/bin/create.mjs CHANGED
@@ -115,7 +115,7 @@ async function main() {
115
115
  __ASTRO_VERSION__: bareVersion(peers.astro ?? '7.0.0'),
116
116
  __REACT_VERSION__: bareVersion(peers.react ?? '19.0.0'),
117
117
  __REACT_DOM_VERSION__: bareVersion(peers['react-dom'] ?? '19.0.0'),
118
- __MAPLIBRE_VERSION__: bareVersion(peers['maplibre-gl'] ?? '5.0.0'),
118
+ __MAPLIBRE_VERSION__: bareVersion(peers['maplibre-gl'] ?? '6.0.0'),
119
119
  __TYPES_REACT_VERSION__: bareVersion(peers['@types/react'] ?? '19.0.0'),
120
120
  __TYPES_REACT_DOM_VERSION__: bareVersion(peers['@types/react-dom'] ?? '19.0.0'),
121
121
  };