@lad-sapienza/scms-core 0.1.0-alpha.1 → 0.1.0-alpha.2
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/bin/add-collection.mjs +37 -23
- package/bin/add-content.mjs +14 -7
- package/bin/create.mjs +149 -0
- package/components/BSNavbar.tsx +2 -2
- package/components/Gallery/__tests__/galleryUtils.test.ts +9 -9
- package/components/Gallery/galleryUtils.ts +2 -2
- package/components/Gallery/types.ts +1 -1
- package/components/Record/useRecordFetcher.ts +2 -2
- package/integrations/__tests__/galleryIntegration.test.ts +13 -13
- package/integrations/contentAssetsIntegration.ts +5 -5
- package/integrations/galleryIntegration.ts +7 -7
- package/package.json +4 -2
- package/scms.ts +3 -3
- package/template/.github/workflows/deploy.yml +51 -0
- package/template/README.md.tmpl +37 -0
- package/template/astro.config.mjs +26 -0
- package/template/package.json.tmpl +31 -0
- package/template/src/content.config.ts +27 -0
- package/template/src/layouts/BaseLayout.astro +62 -0
- package/template/src/pages/index.astro +14 -0
- package/template/src/styles/global.css +8 -0
- package/template/src/user.config.mjs.tmpl +34 -0
- package/template/tsconfig.json +10 -0
- package/utils/content.ts +1 -1
package/bin/add-collection.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { join, relative } from 'node:path';
|
|
|
11
11
|
// own location says nothing about the consuming site's layout. ROOT_DIR must
|
|
12
12
|
// be the invoking project's root, i.e. wherever the CLI was run from.
|
|
13
13
|
const ROOT_DIR = process.cwd();
|
|
14
|
-
const CONFIG_FILE = join(ROOT_DIR, '
|
|
14
|
+
const CONFIG_FILE = join(ROOT_DIR, 'src', 'content.config.ts');
|
|
15
15
|
|
|
16
16
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
17
17
|
const G = '\x1b[32m', Y = '\x1b[33m', R = '\x1b[31m';
|
|
@@ -150,7 +150,7 @@ const entries = (await getCollection('__COL__'))
|
|
|
150
150
|
{entries.length === 0 && (
|
|
151
151
|
<p class="text-center text-muted mt-5">
|
|
152
152
|
No entries yet — add Markdown files to{' '}
|
|
153
|
-
<code>
|
|
153
|
+
<code>src/content/__COL__/</code> and set <code>draft: false</code>.
|
|
154
154
|
</p>
|
|
155
155
|
)}
|
|
156
156
|
</main>
|
|
@@ -207,7 +207,7 @@ const entries = (await getCollection('__COL__'))
|
|
|
207
207
|
{entries.length === 0 && (
|
|
208
208
|
<p class="text-center text-muted mt-5">
|
|
209
209
|
No entries yet — add Markdown files to{' '}
|
|
210
|
-
<code>
|
|
210
|
+
<code>src/content/__COL__/</code> and set <code>draft: false</code>.
|
|
211
211
|
</p>
|
|
212
212
|
)}
|
|
213
213
|
</main>
|
|
@@ -254,7 +254,7 @@ const entries = (await getCollection('__COL__'))
|
|
|
254
254
|
{entries.length === 0 && (
|
|
255
255
|
<p class="text-center text-muted mt-5">
|
|
256
256
|
No entries yet — add Markdown files to{' '}
|
|
257
|
-
<code>
|
|
257
|
+
<code>src/content/__COL__/</code> and set <code>draft: false</code>.
|
|
258
258
|
</p>
|
|
259
259
|
)}
|
|
260
260
|
</main>
|
|
@@ -420,10 +420,15 @@ const { Content } = await render(entry);
|
|
|
420
420
|
|
|
421
421
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
422
422
|
async function main() {
|
|
423
|
-
// Parse existing collections
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
423
|
+
// Parse existing collections. Search only from the `// Export all
|
|
424
|
+
// collections` marker onward (the real export always follows it) — a plain
|
|
425
|
+
// whole-file regex would also match doc-comment examples mentioning the
|
|
426
|
+
// same "export const collections = {...}" text earlier in the file.
|
|
427
|
+
const cfgSrc = readFileSync(CONFIG_FILE, 'utf8');
|
|
428
|
+
const markerIdx = cfgSrc.indexOf('// Export all collections');
|
|
429
|
+
const searchFrom = markerIdx === -1 ? cfgSrc : cfgSrc.slice(markerIdx);
|
|
430
|
+
const exportM = searchFrom.match(/export const collections\s*=\s*\{([^}]+)\}/s);
|
|
431
|
+
const existing = exportM
|
|
427
432
|
? [...exportM[1].matchAll(/(?:['"]([\w-]+)['"]|(\w+))\s*:/g)].map(m => m[1] ?? m[2])
|
|
428
433
|
: [];
|
|
429
434
|
|
|
@@ -465,14 +470,14 @@ async function main() {
|
|
|
465
470
|
ok(`Type: ${colType}`);
|
|
466
471
|
rl.close();
|
|
467
472
|
|
|
468
|
-
// ─── 1. Update
|
|
473
|
+
// ─── 1. Update src/content.config.ts ──────────────────────────────────────
|
|
469
474
|
const newBlock =
|
|
470
475
|
`\n// Schema for ${colName} entries [${colType} type]\n` +
|
|
471
476
|
`// TODO: customize the schema fields to match your content structure\n` +
|
|
472
477
|
`const ${varName} = defineCollection({\n` +
|
|
473
478
|
` loader: glob({\n` +
|
|
474
479
|
` pattern: '**/*.{md,mdx}',\n` +
|
|
475
|
-
` base: './
|
|
480
|
+
` base: './src/content/${colName}',\n` +
|
|
476
481
|
` }),\n` +
|
|
477
482
|
` schema: z.object({\n` +
|
|
478
483
|
`${SCHEMA[colType]}\n` +
|
|
@@ -480,20 +485,29 @@ async function main() {
|
|
|
480
485
|
`});\n`;
|
|
481
486
|
|
|
482
487
|
const MARKER = '// Export all collections';
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
488
|
+
const cfg = readFileSync(CONFIG_FILE, 'utf8');
|
|
489
|
+
const markerIndex = cfg.indexOf(MARKER);
|
|
490
|
+
if (markerIndex === -1) error(`Marker '${MARKER}' not found in config.ts`);
|
|
491
|
+
|
|
492
|
+
// Only ever scan/edit the file from the marker onward — the real `export
|
|
493
|
+
// const collections = {...}` always lives there. Scanning the whole file
|
|
494
|
+
// for that pattern is fragile: doc-comment examples earlier in the file
|
|
495
|
+
// (e.g. in the template this script ships against) can contain the exact
|
|
496
|
+
// same text, and a plain first-match regex would silently edit the
|
|
497
|
+
// comment instead of the real export.
|
|
498
|
+
const before = cfg.slice(0, markerIndex);
|
|
499
|
+
const after = (newBlock + cfg.slice(markerIndex)).replace(
|
|
500
|
+
// [^}]* (zero or more), not [^}]+ — a fresh scms-create site starts with
|
|
501
|
+
// an empty `export const collections = {};`, and `+` would fail to match it.
|
|
502
|
+
/(export const collections\s*=\s*\{)([^}]*)(\};)/s,
|
|
489
503
|
(_, open, inner, close) => `${open}${inner.trimEnd()}\n ${toObjectKey(colName)}: ${varName},\n${close}`
|
|
490
504
|
);
|
|
491
|
-
writeFileSync(CONFIG_FILE,
|
|
492
|
-
ok('
|
|
505
|
+
writeFileSync(CONFIG_FILE, before + after, 'utf8');
|
|
506
|
+
ok('src/content.config.ts updated');
|
|
493
507
|
|
|
494
508
|
// ─── 2. Sample content file ───────────────────────────────────────────────
|
|
495
509
|
const today = new Date().toISOString().slice(0, 10);
|
|
496
|
-
const contentDir = join(ROOT_DIR, '
|
|
510
|
+
const contentDir = join(ROOT_DIR, 'src', 'content', colName);
|
|
497
511
|
mkdirSync(contentDir, { recursive: true });
|
|
498
512
|
|
|
499
513
|
const SAMPLE_NAME = { blog: 'sample-post.md', docs: 'sample-doc.md', generic: 'sample.md' };
|
|
@@ -520,7 +534,7 @@ async function main() {
|
|
|
520
534
|
ok(relative(ROOT_DIR, samplePath));
|
|
521
535
|
|
|
522
536
|
// ─── 3. Page templates ────────────────────────────────────────────────────
|
|
523
|
-
const pagesDir = join(ROOT_DIR, '
|
|
537
|
+
const pagesDir = join(ROOT_DIR, 'src', 'pages', colName);
|
|
524
538
|
mkdirSync(pagesDir, { recursive: true });
|
|
525
539
|
|
|
526
540
|
const sub = (s) => s.replaceAll('__COL__', colName);
|
|
@@ -537,9 +551,9 @@ async function main() {
|
|
|
537
551
|
console.log(`${G}${B}Collection '${colName}' scaffolded successfully!${X}`);
|
|
538
552
|
console.log('');
|
|
539
553
|
console.log(` ${B}Next steps:${X}`);
|
|
540
|
-
console.log(` 1. Open ${B}
|
|
541
|
-
console.log(` 2. Edit the page files in ${B}
|
|
542
|
-
console.log(` 3. Replace the sample file in ${B}
|
|
554
|
+
console.log(` 1. Open ${B}src/content.config.ts${X} and review the schema for '${colName}'`);
|
|
555
|
+
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`);
|
|
543
557
|
console.log(` 4. Run ${B}npm run dev${X} and visit ${B}/${colName}${X}`);
|
|
544
558
|
console.log('');
|
|
545
559
|
}
|
package/bin/add-content.mjs
CHANGED
|
@@ -12,8 +12,8 @@ import { execSync } from 'node:child_process';
|
|
|
12
12
|
// own location says nothing about the consuming site's layout. ROOT_DIR must
|
|
13
13
|
// be the invoking project's root, i.e. wherever the CLI was run from.
|
|
14
14
|
const ROOT_DIR = process.cwd();
|
|
15
|
-
const CONFIG_FILE = join(ROOT_DIR, '
|
|
16
|
-
const CONTENT_BASE = join(ROOT_DIR, '
|
|
15
|
+
const CONFIG_FILE = join(ROOT_DIR, 'src', 'content.config.ts');
|
|
16
|
+
const CONTENT_BASE = join(ROOT_DIR, 'src', 'content');
|
|
17
17
|
|
|
18
18
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
19
19
|
const G = '\x1b[32m', Y = '\x1b[33m', R = '\x1b[31m';
|
|
@@ -43,9 +43,16 @@ const ask = async (prompt) => {
|
|
|
43
43
|
|
|
44
44
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
45
45
|
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Parse top-level collection keys from export const collections = { ... }.
|
|
48
|
+
* Searches only from the `// Export all collections` marker onward (the real
|
|
49
|
+
* export always follows it) — a plain whole-file regex would also match
|
|
50
|
+
* doc-comment examples mentioning the same text earlier in the file.
|
|
51
|
+
*/
|
|
47
52
|
function parseCollections(src) {
|
|
48
|
-
const
|
|
53
|
+
const markerIdx = src.indexOf('// Export all collections');
|
|
54
|
+
const searchFrom = markerIdx === -1 ? src : src.slice(markerIdx);
|
|
55
|
+
const m = searchFrom.match(/export const collections\s*=\s*\{([^}]+)\}/s);
|
|
49
56
|
if (!m) return [];
|
|
50
57
|
// Keys may be quoted (e.g. 'my-collection': ...) when the name isn't a
|
|
51
58
|
// valid bare identifier — see toObjectKey() in add-collection.mjs.
|
|
@@ -71,7 +78,7 @@ function hasMdx(dir) {
|
|
|
71
78
|
/** Extract z.object field lines for a named collection from config.ts */
|
|
72
79
|
function parseSchemaFields(src, colName) {
|
|
73
80
|
const pat = new RegExp(
|
|
74
|
-
`defineCollection\\(\\{.*?base\\s*:\\s*['"]\\./
|
|
81
|
+
`defineCollection\\(\\{.*?base\\s*:\\s*['"]\\./src/content/${colName}['"].*?z\\.object\\(\\{(.*?)\\}\\)`,
|
|
75
82
|
's'
|
|
76
83
|
);
|
|
77
84
|
const m = src.match(pat);
|
|
@@ -177,7 +184,7 @@ async function main() {
|
|
|
177
184
|
continue;
|
|
178
185
|
}
|
|
179
186
|
if (existsSync(join(colDir, raw, `index.${ext}`))) {
|
|
180
|
-
warn(`File already exists:
|
|
187
|
+
warn(`File already exists: src/content/${collection}/${raw}/index.${ext}`);
|
|
181
188
|
continue;
|
|
182
189
|
}
|
|
183
190
|
slug = raw;
|
|
@@ -253,7 +260,7 @@ async function main() {
|
|
|
253
260
|
console.log(`${G}${B}Done!${X}`);
|
|
254
261
|
console.log('');
|
|
255
262
|
console.log(` ${B}Next steps:${X}`);
|
|
256
|
-
console.log(` 1. Open ${B}
|
|
263
|
+
console.log(` 1. Open ${B}src/content/${collection}/${slug}/index.${ext}${X} and write your content`);
|
|
257
264
|
console.log(` 2. Set ${B}draft: false${X} when the content is ready to publish`);
|
|
258
265
|
console.log(` 3. Run ${B}npm run dev${X} and visit ${B}/${collection}/${slug}${X}`);
|
|
259
266
|
console.log('');
|
package/bin/create.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create.mjs — Guided scaffold of a new s:CMS site.
|
|
3
|
+
// Usage: npx --package=@lad-sapienza/scms-core scms-create [directory]
|
|
4
|
+
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
import {
|
|
7
|
+
existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, statSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { join, dirname, relative } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { spawnSync, execSync } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PACKAGE_ROOT = join(__dirname, '..');
|
|
15
|
+
const TEMPLATE_DIR = join(PACKAGE_ROOT, 'template');
|
|
16
|
+
const OWN_PACKAGE = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
17
|
+
|
|
18
|
+
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
19
|
+
const G = '\x1b[32m', Y = '\x1b[33m', R = '\x1b[31m';
|
|
20
|
+
const B = '\x1b[1m', X = '\x1b[0m';
|
|
21
|
+
const ok = (...a) => console.log(`${G}✔${X} ${a.join(' ')}`);
|
|
22
|
+
const warn = (...a) => console.log(`${Y}⚠${X} ${a.join(' ')}`);
|
|
23
|
+
const error = (...a) => { console.error(`${R}✖${X} ${a.join(' ')}`); process.exit(1); };
|
|
24
|
+
|
|
25
|
+
// ─── Readline helper ──────────────────────────────────────────────────────────
|
|
26
|
+
// Same async-iterator pattern as add-collection.mjs / add-content.mjs — avoids
|
|
27
|
+
// the readline hang on piped/pasted (non-TTY) input those scripts already fixed.
|
|
28
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
29
|
+
rl.on('SIGINT', () => { console.log(''); process.exit(0); });
|
|
30
|
+
const rlIterator = rl[Symbol.asyncIterator]();
|
|
31
|
+
const ask = async (prompt) => {
|
|
32
|
+
process.stdout.write(prompt);
|
|
33
|
+
const { value, done } = await rlIterator.next();
|
|
34
|
+
if (done) { console.log(''); process.exit(0); }
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Strip a leading semver range operator (^, ~, >=, etc.) to get a bare version. */
|
|
39
|
+
const bareVersion = (range) => range.replace(/^[^\d]*/, '');
|
|
40
|
+
|
|
41
|
+
/** Recursively copy `template/`, substituting placeholders and stripping .tmpl suffixes. */
|
|
42
|
+
function copyTemplate(srcDir, destDir, replacements) {
|
|
43
|
+
mkdirSync(destDir, { recursive: true });
|
|
44
|
+
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
45
|
+
const srcPath = join(srcDir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
copyTemplate(srcPath, join(destDir, entry.name), replacements);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const isTemplate = entry.name.endsWith('.tmpl');
|
|
51
|
+
const destName = isTemplate ? entry.name.slice(0, -'.tmpl'.length) : entry.name;
|
|
52
|
+
const destPath = join(destDir, destName);
|
|
53
|
+
let content = readFileSync(srcPath, 'utf8');
|
|
54
|
+
if (isTemplate) {
|
|
55
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
56
|
+
content = content.replaceAll(placeholder, value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
writeFileSync(destPath, content, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(`${B}Create a new s:CMS site${X}`);
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
// ─── Prompt: target directory ────────────────────────────────────────────
|
|
69
|
+
const argDir = process.argv[2];
|
|
70
|
+
let targetDir;
|
|
71
|
+
if (argDir) {
|
|
72
|
+
targetDir = argDir;
|
|
73
|
+
} else {
|
|
74
|
+
while (true) {
|
|
75
|
+
const raw = (await ask(`${B}Directory${X} (will be created): `)).trim();
|
|
76
|
+
if (!raw) { warn('Please enter a directory name.'); continue; }
|
|
77
|
+
targetDir = raw;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const targetPath = join(process.cwd(), targetDir);
|
|
82
|
+
if (existsSync(targetPath) && readdirSync(targetPath).length > 0) {
|
|
83
|
+
error(`${targetDir}/ already exists and is not empty.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Prompt: site info ────────────────────────────────────────────────────
|
|
87
|
+
const dirBasedTitle = targetDir.split('/').pop().replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
88
|
+
const title = (await ask(`${B}Site title${X} [${dirBasedTitle}]: `)).trim() || dirBasedTitle;
|
|
89
|
+
|
|
90
|
+
const description = (await ask(`${B}Description${X}: `)).trim() || `A site built with s:CMS.`;
|
|
91
|
+
|
|
92
|
+
let gitAuthor = '';
|
|
93
|
+
try {
|
|
94
|
+
gitAuthor = execSync('git config user.name', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
95
|
+
} catch { /* git not available or no config */ }
|
|
96
|
+
const author = (await ask(`${B}Author${X}${gitAuthor ? ` [${gitAuthor}]` : ''}: `)).trim() || gitAuthor;
|
|
97
|
+
|
|
98
|
+
const siteUrl = (await ask(`${B}Site URL${X} (for sitemap/canonical URLs, e.g. https://example.com): `)).trim();
|
|
99
|
+
|
|
100
|
+
const installNow = (await ask(`${B}Run npm install now?${X} [Y/n]: `)).trim().toLowerCase() !== 'n';
|
|
101
|
+
|
|
102
|
+
rl.close();
|
|
103
|
+
|
|
104
|
+
// ─── Copy + customize the template ────────────────────────────────────────
|
|
105
|
+
const npmPackageName = targetDir.split('/').pop().toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
106
|
+
const peers = OWN_PACKAGE.peerDependencies || {};
|
|
107
|
+
|
|
108
|
+
const replacements = {
|
|
109
|
+
__NAME__: npmPackageName,
|
|
110
|
+
__TITLE__: title,
|
|
111
|
+
__DESCRIPTION__: description,
|
|
112
|
+
__AUTHOR__: author,
|
|
113
|
+
__SITE_URL__: siteUrl,
|
|
114
|
+
__SCMS_VERSION__: OWN_PACKAGE.version,
|
|
115
|
+
__ASTRO_VERSION__: bareVersion(peers.astro ?? '7.0.0'),
|
|
116
|
+
__REACT_VERSION__: bareVersion(peers.react ?? '19.0.0'),
|
|
117
|
+
__REACT_DOM_VERSION__: bareVersion(peers['react-dom'] ?? '19.0.0'),
|
|
118
|
+
__MAPLIBRE_VERSION__: bareVersion(peers['maplibre-gl'] ?? '5.0.0'),
|
|
119
|
+
__TYPES_REACT_VERSION__: bareVersion(peers['@types/react'] ?? '19.0.0'),
|
|
120
|
+
__TYPES_REACT_DOM_VERSION__: bareVersion(peers['@types/react-dom'] ?? '19.0.0'),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
copyTemplate(TEMPLATE_DIR, targetPath, replacements);
|
|
124
|
+
ok(`Created ${relative(process.cwd(), targetPath) || '.'}/`);
|
|
125
|
+
|
|
126
|
+
// ─── Install ───────────────────────────────────────────────────────────────
|
|
127
|
+
if (installNow) {
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log(`${B}Installing dependencies...${X}`);
|
|
130
|
+
const result = spawnSync('npm', ['install'], { cwd: targetPath, stdio: 'inherit' });
|
|
131
|
+
if (result.status !== 0) {
|
|
132
|
+
warn('npm install failed — run it yourself once you\'ve reviewed the error above.');
|
|
133
|
+
} else {
|
|
134
|
+
ok('Dependencies installed');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Summary ──────────────────────────────────────────────────────────────
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log(`${G}${B}Site scaffolded successfully!${X}`);
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(` ${B}Next steps:${X}`);
|
|
143
|
+
console.log(` 1. cd ${targetDir}`);
|
|
144
|
+
if (!installNow) console.log(` 2. npm install`);
|
|
145
|
+
console.log(` ${installNow ? 2 : 3}. npm run dev`);
|
|
146
|
+
console.log('');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main().catch((err) => error(String(err)));
|
package/components/BSNavbar.tsx
CHANGED
|
@@ -43,7 +43,7 @@ interface CssClasses {
|
|
|
43
43
|
* Active state is computed internally from `currentPath` — no need to set
|
|
44
44
|
* `isActive` on individual menu items.
|
|
45
45
|
*
|
|
46
|
-
* Menu data can be loaded from a YAML file (e.g. `
|
|
46
|
+
* Menu data can be loaded from a YAML file (e.g. `src/content/data/menu.yaml`)
|
|
47
47
|
* and passed straight to `menuItems`.
|
|
48
48
|
*
|
|
49
49
|
* Must be hydrated with `client:load` when used inside an Astro component.
|
|
@@ -63,7 +63,7 @@ interface CssClasses {
|
|
|
63
63
|
* />
|
|
64
64
|
* ```
|
|
65
65
|
*
|
|
66
|
-
* @example YAML menu structure (`
|
|
66
|
+
* @example YAML menu structure (`src/content/data/menu.yaml`)
|
|
67
67
|
* ```yaml
|
|
68
68
|
* - href: /
|
|
69
69
|
* label: Home
|
|
@@ -2,23 +2,23 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
const manifest = {
|
|
4
4
|
pagesImages: {
|
|
5
|
-
'/
|
|
5
|
+
'/src/pages/my-page/gallery/photo1.jpg': { default: { src: '/_astro/photo1.abc123.jpg', width: 1200, height: 800 } },
|
|
6
6
|
},
|
|
7
7
|
contentImages: {
|
|
8
|
-
'/
|
|
9
|
-
'/
|
|
8
|
+
'/src/content/blog/my-article/gallery/photo2.jpg': { default: { src: '/_astro/photo2.def456.jpg', width: 1000, height: 600 } },
|
|
9
|
+
'/src/content/blog/other-article/gallery/photo3.jpg': { default: { src: '/_astro/photo3.ghi789.jpg', width: 900, height: 500 } },
|
|
10
10
|
},
|
|
11
11
|
sharedImages: {
|
|
12
|
-
'/
|
|
13
|
-
'/
|
|
14
|
-
'/
|
|
12
|
+
'/src/galleries/scavi-2024/site-a.jpg': { default: { src: '/_astro/site-a.jkl000.jpg', width: 800, height: 600 } },
|
|
13
|
+
'/src/galleries/scavi-2024/site-b_1920.jpg': { default: { src: '/_astro/site-b_1920.mno111.jpg', width: 800, height: 600 } },
|
|
14
|
+
'/src/galleries/other-name/site-c.jpg': { default: { src: '/_astro/site-c.pqr222.jpg', width: 800, height: 600 } },
|
|
15
15
|
},
|
|
16
16
|
pagesCaptions: {},
|
|
17
17
|
contentCaptions: {
|
|
18
|
-
'/
|
|
18
|
+
'/src/content/blog/my-article/gallery/captions.json': { default: { 'photo2.jpg': 'A custom caption' } },
|
|
19
19
|
},
|
|
20
20
|
sharedCaptions: {
|
|
21
|
-
'/
|
|
21
|
+
'/src/galleries/scavi-2024/captions.json': {
|
|
22
22
|
default: {
|
|
23
23
|
'site-a.jpg': 'Exact match caption',
|
|
24
24
|
// Deliberately a different resolution suffix than the real file
|
|
@@ -146,7 +146,7 @@ describe('getSharedCaptions', () => {
|
|
|
146
146
|
function parseMalformedFixture() {
|
|
147
147
|
// Reuses the real function against a manifest entry shaped like a
|
|
148
148
|
// malformed captions.json (nested object instead of Record<string,string>).
|
|
149
|
-
(manifest.sharedCaptions as any)['/
|
|
149
|
+
(manifest.sharedCaptions as any)['/src/galleries/broken/captions.json'] = { default: { nested: { not: 'a string' } } };
|
|
150
150
|
return getSharedCaptions('broken');
|
|
151
151
|
}
|
|
152
152
|
});
|
|
@@ -152,7 +152,7 @@ export function getSharedCaptions(name: string): Record<string, string> | undefi
|
|
|
152
152
|
/**
|
|
153
153
|
* Images from a `gallery/` folder colocated with the current page/content
|
|
154
154
|
* file, matched by comparing the current URL path against the folder
|
|
155
|
-
* structure under
|
|
155
|
+
* structure under src/pages/ or src/content/. Applies captions.json from
|
|
156
156
|
* the same folder, if present.
|
|
157
157
|
*
|
|
158
158
|
* Known limitation (unchanged from before this redesign): if a content
|
|
@@ -173,7 +173,7 @@ export function getColocatedGalleryImages(pathname: string): GalleryImage[] {
|
|
|
173
173
|
.map(([filepath, mod]) => moduleToGalleryImage(filepath, mod, captionLookup));
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
/** Images from a shared gallery at
|
|
176
|
+
/** Images from a shared gallery at src/galleries/<name>/. Applies captions.json from the same folder, if present. */
|
|
177
177
|
export function getSharedGalleryImages(name: string): GalleryImage[] {
|
|
178
178
|
const captions = getSharedCaptions(name);
|
|
179
179
|
const captionLookup = captions ? buildCaptionLookup(captions) : undefined;
|
|
@@ -13,7 +13,7 @@ export interface GalleryImage {
|
|
|
13
13
|
|
|
14
14
|
export interface GalleryProps {
|
|
15
15
|
/**
|
|
16
|
-
* Name of a shared gallery under
|
|
16
|
+
* Name of a shared gallery under src/galleries/<name>/
|
|
17
17
|
* If not provided (and `images` isn't either), auto-loads from the
|
|
18
18
|
* `gallery/` folder colocated with the current page/content file.
|
|
19
19
|
*/
|
|
@@ -31,7 +31,7 @@ export interface UseRecordFetcherResult {
|
|
|
31
31
|
*
|
|
32
32
|
* @example
|
|
33
33
|
* ```tsx
|
|
34
|
-
* //
|
|
34
|
+
* // src/components/RecordView.tsx
|
|
35
35
|
* import { useRecordFetcher } from '@lad-sapienza/scms-core/components/Record/useRecordFetcher';
|
|
36
36
|
*
|
|
37
37
|
* export default function RecordView() {
|
|
@@ -45,7 +45,7 @@ export interface UseRecordFetcherResult {
|
|
|
45
45
|
* ```
|
|
46
46
|
*
|
|
47
47
|
* ```astro
|
|
48
|
-
* <!--
|
|
48
|
+
* <!-- src/pages/record.astro -->
|
|
49
49
|
* <RecordView client:load />
|
|
50
50
|
* ```
|
|
51
51
|
*/
|
|
@@ -26,17 +26,17 @@ describe('galleryIntegration', () => {
|
|
|
26
26
|
expect(plugin.resolveId('some/other/id')).toBeUndefined();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it('generates a virtual module with one static glob per source root, using the default
|
|
29
|
+
it('generates a virtual module with one static glob per source root, using the default src/ paths', () => {
|
|
30
30
|
const plugin = getVitePlugin();
|
|
31
31
|
const resolvedId = plugin.resolveId(GALLERY_VIRTUAL_MODULE_ID);
|
|
32
32
|
const source = plugin.load(resolvedId);
|
|
33
33
|
|
|
34
|
-
expect(source).toContain("import.meta.glob('/
|
|
35
|
-
expect(source).toContain("import.meta.glob('/
|
|
36
|
-
expect(source).toContain("import.meta.glob('/
|
|
37
|
-
expect(source).toContain("import.meta.glob('/
|
|
38
|
-
expect(source).toContain("import.meta.glob('/
|
|
39
|
-
expect(source).toContain("import.meta.glob('/
|
|
34
|
+
expect(source).toContain("import.meta.glob('/src/pages/**/gallery/*.");
|
|
35
|
+
expect(source).toContain("import.meta.glob('/src/content/**/gallery/*.");
|
|
36
|
+
expect(source).toContain("import.meta.glob('/src/galleries/*/*.");
|
|
37
|
+
expect(source).toContain("import.meta.glob('/src/pages/**/gallery/captions.json'");
|
|
38
|
+
expect(source).toContain("import.meta.glob('/src/content/**/gallery/captions.json'");
|
|
39
|
+
expect(source).toContain("import.meta.glob('/src/galleries/*/captions.json'");
|
|
40
40
|
|
|
41
41
|
// Every glob call must use { eager: true } — a lazy glob would return
|
|
42
42
|
// dynamic import() functions instead of already-resolved image metadata.
|
|
@@ -50,14 +50,14 @@ describe('galleryIntegration', () => {
|
|
|
50
50
|
expect(plugin.load('/some/unrelated/module.js')).toBeUndefined();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('honors custom directory options', () => {
|
|
54
|
-
const plugin = getVitePlugin({ pagesDir: '
|
|
53
|
+
it('honors custom directory options, overriding the src/ defaults', () => {
|
|
54
|
+
const plugin = getVitePlugin({ pagesDir: 'custom/pages', contentDir: 'custom/content', galleriesDir: 'custom/galleries' });
|
|
55
55
|
const source = plugin.load(plugin.resolveId(GALLERY_VIRTUAL_MODULE_ID));
|
|
56
56
|
|
|
57
|
-
expect(source).toContain("import.meta.glob('/
|
|
58
|
-
expect(source).toContain("import.meta.glob('/
|
|
59
|
-
expect(source).toContain("import.meta.glob('/
|
|
60
|
-
expect(source).not.toContain('/
|
|
57
|
+
expect(source).toContain("import.meta.glob('/custom/pages/**/gallery/*.");
|
|
58
|
+
expect(source).toContain("import.meta.glob('/custom/content/**/gallery/*.");
|
|
59
|
+
expect(source).toContain("import.meta.glob('/custom/galleries/*/*.");
|
|
60
|
+
expect(source).not.toContain('/src/');
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* contentAssetsIntegration
|
|
3
3
|
*
|
|
4
4
|
* Astro integration that serves static assets (images, PDFs, etc.) co-located
|
|
5
|
-
* in the content directory (
|
|
5
|
+
* in the content directory (src/content/ by default) without requiring a
|
|
6
6
|
* manual copy step.
|
|
7
7
|
*
|
|
8
8
|
* - Dev server: a Vite middleware intercepts requests for static file extensions
|
|
9
9
|
* and serves them directly from the content directory, mapping URL paths 1:1
|
|
10
|
-
* (e.g. /blog/post/image.jpg →
|
|
10
|
+
* (e.g. /blog/post/image.jpg → src/content/blog/post/image.jpg).
|
|
11
11
|
*
|
|
12
12
|
* - Production build: assets are copied from the content directory into dist/
|
|
13
13
|
* with the same relative path structure, so they are available at the
|
|
@@ -23,7 +23,7 @@ import { existsSync, statSync, mkdirSync, copyFileSync, readdirSync, createReadS
|
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
25
|
export interface ContentAssetsOptions {
|
|
26
|
-
/** Path to the content directory, relative to the project root. Defaults to '
|
|
26
|
+
/** Path to the content directory, relative to the project root. Defaults to 'src/content'. */
|
|
27
27
|
contentDir?: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -60,7 +60,7 @@ function collectAssets(dir: string, results: string[] = []): string[] {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function contentAssetsIntegration(options: ContentAssetsOptions = {}): AstroIntegration {
|
|
63
|
-
const contentDirSegment = options.contentDir ?? join('
|
|
63
|
+
const contentDirSegment = options.contentDir ?? join('src', 'content');
|
|
64
64
|
// Resolved from Astro's own config root in astro:config:setup below, rather
|
|
65
65
|
// than assumed from process.cwd() — more portable if the process is ever
|
|
66
66
|
// invoked from somewhere other than the project root.
|
|
@@ -76,7 +76,7 @@ export function contentAssetsIntegration(options: ContentAssetsOptions = {}): As
|
|
|
76
76
|
/**
|
|
77
77
|
* Dev server: add a Connect middleware that serves static assets directly
|
|
78
78
|
* from the content directory without requiring them to be copied to
|
|
79
|
-
*
|
|
79
|
+
* public/.
|
|
80
80
|
*/
|
|
81
81
|
'astro:server:setup': ({ server, logger }) => {
|
|
82
82
|
const contentDir = resolve(projectRoot, contentDirSegment);
|
|
@@ -22,23 +22,23 @@ const RESOLVED_VIRTUAL_MODULE_ID = '\0' + GALLERY_VIRTUAL_MODULE_ID;
|
|
|
22
22
|
const IMAGE_EXTENSIONS = '{jpg,jpeg,png,gif,webp,avif,JPG,JPEG,PNG,GIF,WEBP,AVIF}';
|
|
23
23
|
|
|
24
24
|
export interface GalleryIntegrationOptions {
|
|
25
|
-
/** Path to page files, relative to the project root. Defaults to '
|
|
25
|
+
/** Path to page files, relative to the project root. Defaults to 'src/pages'. */
|
|
26
26
|
pagesDir?: string;
|
|
27
|
-
/** Path to content collections, relative to the project root. Defaults to '
|
|
27
|
+
/** Path to content collections, relative to the project root. Defaults to 'src/content'. */
|
|
28
28
|
contentDir?: string;
|
|
29
|
-
/** Path to shared galleries, relative to the project root. Defaults to '
|
|
29
|
+
/** Path to shared galleries, relative to the project root. Defaults to 'src/galleries'. */
|
|
30
30
|
galleriesDir?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Normalize a directory segment into a Vite root-relative glob prefix, e.g. '
|
|
33
|
+
/** Normalize a directory segment into a Vite root-relative glob prefix, e.g. 'src/pages' -> '/src/pages'. */
|
|
34
34
|
function toRootGlobPrefix(dir: string): string {
|
|
35
35
|
return '/' + dir.replace(/^\/+|\/+$/g, '');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function buildGalleryVirtualModuleSource(options: GalleryIntegrationOptions = {}): string {
|
|
39
|
-
const pages = toRootGlobPrefix(options.pagesDir ?? '
|
|
40
|
-
const content = toRootGlobPrefix(options.contentDir ?? '
|
|
41
|
-
const galleries = toRootGlobPrefix(options.galleriesDir ?? '
|
|
39
|
+
const pages = toRootGlobPrefix(options.pagesDir ?? 'src/pages');
|
|
40
|
+
const content = toRootGlobPrefix(options.contentDir ?? 'src/content');
|
|
41
|
+
const galleries = toRootGlobPrefix(options.galleriesDir ?? 'src/galleries');
|
|
42
42
|
|
|
43
43
|
return `
|
|
44
44
|
export const pagesImages = import.meta.glob('${pages}/**/gallery/*.${IMAGE_EXTENSIONS}', { eager: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lad-sapienza/scms-core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Framework components and Astro integrations for s:CMS — DataTb, Gallery, Map, ZoteroGeoViewer, Record.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-0-Clause",
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
"main": "./index.ts",
|
|
13
13
|
"bin": {
|
|
14
14
|
"scms-add-collection": "./bin/add-collection.mjs",
|
|
15
|
-
"scms-add-content": "./bin/add-content.mjs"
|
|
15
|
+
"scms-add-content": "./bin/add-content.mjs",
|
|
16
|
+
"scms-create": "./bin/create.mjs"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"index.ts",
|
|
19
20
|
"scms.ts",
|
|
20
21
|
"bin",
|
|
22
|
+
"template",
|
|
21
23
|
"components",
|
|
22
24
|
"integrations",
|
|
23
25
|
"utils",
|
package/scms.ts
CHANGED
|
@@ -30,11 +30,11 @@ import { contentAssetsIntegration } from './integrations/contentAssetsIntegratio
|
|
|
30
30
|
import { galleryIntegration } from './integrations/galleryIntegration';
|
|
31
31
|
|
|
32
32
|
export interface ScmsOptions {
|
|
33
|
-
/** Path to the content directory, relative to the project root. Defaults to '
|
|
33
|
+
/** Path to the content directory, relative to the project root. Defaults to 'src/content'. */
|
|
34
34
|
contentDir?: string;
|
|
35
|
-
/** Path to page files, relative to the project root. Defaults to '
|
|
35
|
+
/** Path to page files, relative to the project root. Defaults to 'src/pages'. */
|
|
36
36
|
pagesDir?: string;
|
|
37
|
-
/** Path to shared galleries, relative to the project root. Defaults to '
|
|
37
|
+
/** Path to shared galleries, relative to the project root. Defaults to 'src/galleries'. */
|
|
38
38
|
galleriesDir?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Deploy to GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: "pages"
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Setup Node
|
|
26
|
+
uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 22
|
|
29
|
+
cache: npm
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: npm ci
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: npm run build
|
|
36
|
+
|
|
37
|
+
- name: Upload Pages artifact
|
|
38
|
+
uses: actions/upload-pages-artifact@v3
|
|
39
|
+
with:
|
|
40
|
+
path: dist/
|
|
41
|
+
|
|
42
|
+
deploy:
|
|
43
|
+
needs: build
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment:
|
|
46
|
+
name: github-pages
|
|
47
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
48
|
+
steps:
|
|
49
|
+
- name: Deploy to GitHub Pages
|
|
50
|
+
id: deployment
|
|
51
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# __TITLE__
|
|
2
|
+
|
|
3
|
+
__DESCRIPTION__
|
|
4
|
+
|
|
5
|
+
Built with [s:CMS](https://github.com/lad-sapienza/scms-core), on top of [Astro](https://astro.build).
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
| Command | Action |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `npm install` | Install dependencies |
|
|
12
|
+
| `npm run dev` | Start the local dev server at `localhost:4321` |
|
|
13
|
+
| `npm run build` | Build the production site to `./dist/` |
|
|
14
|
+
| `npm run preview` | Preview the production build locally |
|
|
15
|
+
| `npm run add-collection` | Scaffold a new content collection |
|
|
16
|
+
| `npm run add-content` | Add a new content file to an existing collection |
|
|
17
|
+
|
|
18
|
+
## Updating the framework
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm update @lad-sapienza/scms-core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Project structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
content.config.ts # Content collection schemas
|
|
29
|
+
user.config.mjs # Site configuration and metadata
|
|
30
|
+
pages/ # Routes
|
|
31
|
+
layouts/ # Layouts
|
|
32
|
+
styles/global.css # Stylesheet
|
|
33
|
+
public/ # Static assets, served as-is
|
|
34
|
+
astro.config.mjs # Registers the scms() integration
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
See the [s:CMS documentation](https://github.com/lad-sapienza/sCMS) for available components (`DataTb`, `Map`, `Gallery`, `ZoteroGeoViewer`, `Record`, and more).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'astro/config';
|
|
2
|
+
import rehypeSlug from 'rehype-slug';
|
|
3
|
+
import { unified } from '@astrojs/markdown-remark';
|
|
4
|
+
import { userConfig } from './src/user.config.mjs';
|
|
5
|
+
import { scms } from '@lad-sapienza/scms-core/scms';
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
site: userConfig.site,
|
|
9
|
+
base: userConfig.base,
|
|
10
|
+
output: 'static',
|
|
11
|
+
|
|
12
|
+
// rehypeSlug adds an id to every heading, which the TableOfContents
|
|
13
|
+
// component relies on for its anchor links.
|
|
14
|
+
markdown: {
|
|
15
|
+
processor: unified({ rehypePlugins: [rehypeSlug] }),
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
integrations: [
|
|
19
|
+
...scms(),
|
|
20
|
+
...(userConfig.integrations || []),
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
vite: {
|
|
24
|
+
...(userConfig.vite || {}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__DESCRIPTION__",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "astro dev",
|
|
9
|
+
"build": "astro check && astro build",
|
|
10
|
+
"preview": "astro preview",
|
|
11
|
+
"astro": "astro",
|
|
12
|
+
"add-collection": "scms-add-collection",
|
|
13
|
+
"add-content": "scms-add-content"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@astrojs/check": "^0.9.10",
|
|
17
|
+
"@astrojs/markdown-remark": "^__ASTRO_VERSION__",
|
|
18
|
+
"@lad-sapienza/scms-core": "^__SCMS_VERSION__",
|
|
19
|
+
"astro": "^__ASTRO_VERSION__",
|
|
20
|
+
"bootstrap": "^5.3.8",
|
|
21
|
+
"react": "^__REACT_VERSION__",
|
|
22
|
+
"react-dom": "^__REACT_DOM_VERSION__",
|
|
23
|
+
"maplibre-gl": "^__MAPLIBRE_VERSION__",
|
|
24
|
+
"rehype-slug": "^6.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^__TYPES_REACT_VERSION__",
|
|
28
|
+
"@types/react-dom": "^__TYPES_REACT_DOM_VERSION__",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Collections Configuration
|
|
3
|
+
*
|
|
4
|
+
* Define your content collections here. Each collection needs a loader
|
|
5
|
+
* (usually `glob` for local Markdown/MDX files) and a Zod schema.
|
|
6
|
+
*
|
|
7
|
+
* Fastest way to add one: `npm run add-collection`.
|
|
8
|
+
*
|
|
9
|
+
* Example — a `blog` collection, added to the `collections` export below:
|
|
10
|
+
*
|
|
11
|
+
* const blog = defineCollection({
|
|
12
|
+
* loader: glob({ pattern: '**\/*.{md,mdx}', base: './src/content/blog' }),
|
|
13
|
+
* schema: z.object({
|
|
14
|
+
* title: z.string(),
|
|
15
|
+
* description: z.string(),
|
|
16
|
+
* date: z.coerce.date(),
|
|
17
|
+
* draft: z.boolean().optional(),
|
|
18
|
+
* }),
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { defineCollection } from 'astro:content';
|
|
23
|
+
import { glob } from 'astro/loaders';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
|
|
26
|
+
// Export all collections
|
|
27
|
+
export const collections = {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/global.css"
|
|
3
|
+
import SEO from "@lad-sapienza/scms-core/components/SEO/SEO.astro"
|
|
4
|
+
import BSNavbar from "@lad-sapienza/scms-core/components/BSNavbar"
|
|
5
|
+
import { siteMetadata } from "../user.config.mjs"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title?: string
|
|
9
|
+
description?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { title = siteMetadata.title, description = siteMetadata.description } =
|
|
13
|
+
Astro.props
|
|
14
|
+
|
|
15
|
+
const currentPath = Astro.url.pathname
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<!doctype html>
|
|
19
|
+
<html lang="en">
|
|
20
|
+
<head>
|
|
21
|
+
<meta charset="UTF-8" />
|
|
22
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
23
|
+
<SEO title={title} description={description} siteMetadata={siteMetadata} />
|
|
24
|
+
<slot name="head" />
|
|
25
|
+
</head>
|
|
26
|
+
|
|
27
|
+
<body class="d-flex flex-column min-vh-100">
|
|
28
|
+
<BSNavbar
|
|
29
|
+
client:load
|
|
30
|
+
menuItems={[]}
|
|
31
|
+
currentPath={currentPath}
|
|
32
|
+
cssClasses={{
|
|
33
|
+
nav: "bg-body-tertiary navbar-expand-md sticky-top shadow-sm border-bottom",
|
|
34
|
+
ul: "ms-auto mb-2 mb-md-0",
|
|
35
|
+
li: "text-center fw-medium",
|
|
36
|
+
}}
|
|
37
|
+
brand={{
|
|
38
|
+
text: siteMetadata.title,
|
|
39
|
+
link: "/",
|
|
40
|
+
cssClasses: "fw-bold",
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
|
|
44
|
+
<main
|
|
45
|
+
class="container flex-grow-1"
|
|
46
|
+
style="padding-top: 6rem; padding-bottom: 3rem;"
|
|
47
|
+
>
|
|
48
|
+
<slot />
|
|
49
|
+
</main>
|
|
50
|
+
|
|
51
|
+
<footer class="py-4 mt-auto border-top">
|
|
52
|
+
<div class="container text-center text-muted small">
|
|
53
|
+
Built with <a href="https://github.com/lad-sapienza/scms-core" target="_blank" rel="noreferrer">s:CMS</a>
|
|
54
|
+
</div>
|
|
55
|
+
</footer>
|
|
56
|
+
|
|
57
|
+
<script
|
|
58
|
+
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
59
|
+
defer
|
|
60
|
+
is:inline></script>
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<BaseLayout>
|
|
6
|
+
<h1>Welcome to your new s:CMS site</h1>
|
|
7
|
+
<p class="lead">
|
|
8
|
+
Edit <code>src/pages/index.astro</code> to change this page.
|
|
9
|
+
</p>
|
|
10
|
+
<p>
|
|
11
|
+
Run <code>npm run add-collection</code> to scaffold your first content
|
|
12
|
+
collection, or start writing components directly in <code>src/</code>.
|
|
13
|
+
</p>
|
|
14
|
+
</BaseLayout>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site Configuration
|
|
3
|
+
*
|
|
4
|
+
* Edit this file to configure your site's metadata, integrations, and more.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const userConfig = {
|
|
8
|
+
// Site URL (required for sitemap and canonical URLs)
|
|
9
|
+
site: '__SITE_URL__',
|
|
10
|
+
|
|
11
|
+
// Base path (if deploying to a subdirectory, e.g. GitHub Pages project sites)
|
|
12
|
+
// base: '/my-site',
|
|
13
|
+
|
|
14
|
+
// Additional Astro integrations (merged with scms()'s own)
|
|
15
|
+
integrations: [
|
|
16
|
+
// Add your custom integrations here
|
|
17
|
+
],
|
|
18
|
+
|
|
19
|
+
// Custom Vite configuration
|
|
20
|
+
vite: {
|
|
21
|
+
// Your custom Vite config
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Site Metadata
|
|
27
|
+
*
|
|
28
|
+
* Used throughout the site for SEO, social media cards, and general info.
|
|
29
|
+
*/
|
|
30
|
+
export const siteMetadata = {
|
|
31
|
+
title: '__TITLE__',
|
|
32
|
+
description: '__DESCRIPTION__',
|
|
33
|
+
author: '__AUTHOR__',
|
|
34
|
+
};
|
package/utils/content.ts
CHANGED
|
@@ -49,7 +49,7 @@ export async function getSortedCollection(
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Menu extraction logic should be implemented in
|
|
52
|
+
// Menu extraction logic should be implemented in the consuming site's own code, not here.
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Parse CSV string to JSON
|