@lad-sapienza/scms-core 0.1.0-alpha.0 → 0.1.0-alpha.1
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 +17 -1
- package/bin/add-collection.mjs +547 -0
- package/bin/add-content.mjs +262 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -18,7 +18,12 @@ Register the integration in `astro.config.mjs`:
|
|
|
18
18
|
|
|
19
19
|
```js
|
|
20
20
|
import { defineConfig } from 'astro/config';
|
|
21
|
-
|
|
21
|
+
// Import from the `/scms` subpath, not the bare `@lad-sapienza/scms-core`
|
|
22
|
+
// specifier — the bare specifier resolves to index.ts, which re-exports
|
|
23
|
+
// .astro components (Gallery, TableOfContents). Astro can't compile .astro
|
|
24
|
+
// files yet at config-load time, before its own Vite plugin is registered,
|
|
25
|
+
// so a bare import here breaks with a Vite parse error.
|
|
26
|
+
import { scms } from '@lad-sapienza/scms-core/scms';
|
|
22
27
|
|
|
23
28
|
export default defineConfig({
|
|
24
29
|
integrations: [
|
|
@@ -60,6 +65,17 @@ import { directusLoader } from '@lad-sapienza/scms-core/integrations/directusLoa
|
|
|
60
65
|
|
|
61
66
|
See each component folder's own `README.md` (`components/DataTb`, `components/Gallery`, `components/ZoteroGeoViewer`, `components/`) for detailed API docs and examples.
|
|
62
67
|
|
|
68
|
+
## Scaffolding CLI
|
|
69
|
+
|
|
70
|
+
This package also ships the interactive scaffolding tools sCMS sites use to add content, as `bin` commands — run from the consuming site's project root:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx scms-add-collection # scaffold a new Astro content collection under usr/
|
|
74
|
+
npx scms-add-content # add a new content file to an existing collection
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A consuming site typically wires these up as `npm run add-collection` / `npm run add-content` in its own `package.json`.
|
|
78
|
+
|
|
63
79
|
## Development
|
|
64
80
|
|
|
65
81
|
```bash
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// add-collection.mjs — Scaffold a new Astro content collection in s:CMS
|
|
3
|
+
// Usage: npx scms-add-collection
|
|
4
|
+
// or: npm run add-collection (from a consuming site's package.json)
|
|
5
|
+
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
8
|
+
import { join, relative } from 'node:path';
|
|
9
|
+
|
|
10
|
+
// This file ships inside the @lad-sapienza/scms-core package (bin/), so its
|
|
11
|
+
// own location says nothing about the consuming site's layout. ROOT_DIR must
|
|
12
|
+
// be the invoking project's root, i.e. wherever the CLI was run from.
|
|
13
|
+
const ROOT_DIR = process.cwd();
|
|
14
|
+
const CONFIG_FILE = join(ROOT_DIR, 'usr', 'content.config.ts');
|
|
15
|
+
|
|
16
|
+
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
17
|
+
const G = '\x1b[32m', Y = '\x1b[33m', R = '\x1b[31m';
|
|
18
|
+
const B = '\x1b[1m', X = '\x1b[0m';
|
|
19
|
+
const ok = (...a) => console.log(`${G}✔${X} ${a.join(' ')}`);
|
|
20
|
+
const warn = (...a) => console.log(`${Y}⚠${X} ${a.join(' ')}`);
|
|
21
|
+
const error = (...a) => { console.error(`${R}✖${X} ${a.join(' ')}`); process.exit(1); };
|
|
22
|
+
|
|
23
|
+
if (!existsSync(CONFIG_FILE)) error(`Cannot find ${CONFIG_FILE} — are you running from the project root?`);
|
|
24
|
+
|
|
25
|
+
// ─── Object key helper ─────────────────────────────────────────────────────
|
|
26
|
+
// Collection names may contain hyphens (the prompt below explicitly allows
|
|
27
|
+
// them), which aren't valid in a bare JS/TS object-key identifier — e.g.
|
|
28
|
+
// `my-collection: fooCollection` is a syntax error (parsed as subtraction).
|
|
29
|
+
// Quote the key whenever it isn't a valid identifier on its own.
|
|
30
|
+
const toObjectKey = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : `'${name}'`;
|
|
31
|
+
|
|
32
|
+
// ─── Readline helper ──────────────────────────────────────────────────────────
|
|
33
|
+
// Uses the readline interface as an async iterator rather than repeated
|
|
34
|
+
// rl.question() calls: with piped/pasted (non-TTY) input, multiple lines can
|
|
35
|
+
// arrive in a single chunk, and question()'s one-shot 'line' listener isn't
|
|
36
|
+
// registered in time to catch answers past the first — they're silently
|
|
37
|
+
// dropped and the prompt hangs forever with no error. Iterating the interface
|
|
38
|
+
// consumes lines through its internal queue instead, so it doesn't race.
|
|
39
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
40
|
+
rl.on('SIGINT', () => { console.log(''); process.exit(0); });
|
|
41
|
+
const rlIterator = rl[Symbol.asyncIterator]();
|
|
42
|
+
const ask = async (prompt) => {
|
|
43
|
+
process.stdout.write(prompt);
|
|
44
|
+
const { value, done } = await rlIterator.next();
|
|
45
|
+
if (done) { console.log(''); process.exit(0); }
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ─── Schema fields per collection type ───────────────────────────────────────
|
|
50
|
+
const SCHEMA = {
|
|
51
|
+
blog: [
|
|
52
|
+
' title: z.string(),',
|
|
53
|
+
' description: z.string(),',
|
|
54
|
+
' date: z.coerce.date(),',
|
|
55
|
+
' author: z.string().optional(),',
|
|
56
|
+
' tags: z.array(z.string()).optional(),',
|
|
57
|
+
' image: z.string().optional(),',
|
|
58
|
+
' draft: z.boolean().optional(),',
|
|
59
|
+
].join('\n'),
|
|
60
|
+
docs: [
|
|
61
|
+
' title: z.string(),',
|
|
62
|
+
' description: z.string().optional(),',
|
|
63
|
+
' order: z.number().optional(),',
|
|
64
|
+
' category: z.string().optional(),',
|
|
65
|
+
' draft: z.boolean().optional(),',
|
|
66
|
+
].join('\n'),
|
|
67
|
+
generic: [
|
|
68
|
+
' title: z.string(),',
|
|
69
|
+
' description: z.string().optional(),',
|
|
70
|
+
' draft: z.boolean().optional(),',
|
|
71
|
+
].join('\n'),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ─── Page templates ───────────────────────────────────────────────────────────
|
|
75
|
+
// __COL__ is substituted with the collection name after the template is defined.
|
|
76
|
+
// Inside templates: \` → ` and \${ → ${ so JS doesn't interpolate them.
|
|
77
|
+
// \\ → \ in the output (needed for JS regex /\.mdx?$/ inside Astro files).
|
|
78
|
+
|
|
79
|
+
const INDEX = {
|
|
80
|
+
|
|
81
|
+
blog: `---
|
|
82
|
+
import { getCollection } from 'astro:content';
|
|
83
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
84
|
+
|
|
85
|
+
export const prerender = true;
|
|
86
|
+
|
|
87
|
+
// TODO: customise title and description below
|
|
88
|
+
const title = '__COL__';
|
|
89
|
+
const description = 'All entries in the __COL__ collection';
|
|
90
|
+
|
|
91
|
+
const entries = (await getCollection('__COL__'))
|
|
92
|
+
.filter(e => !e.data.draft)
|
|
93
|
+
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
<BaseLayout title={title} description={description}>
|
|
97
|
+
<main class="container py-5">
|
|
98
|
+
<div class="mb-5">
|
|
99
|
+
<h1 class="mb-0">{title}</h1>
|
|
100
|
+
<p class="lead">{description}</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="row g-4">
|
|
104
|
+
{entries.map(entry => (
|
|
105
|
+
<div class="col-12 col-md-6 col-lg-4">
|
|
106
|
+
<article class="card h-100 shadow-sm">
|
|
107
|
+
{entry.data.image && (
|
|
108
|
+
<img
|
|
109
|
+
src={entry.data.image}
|
|
110
|
+
alt={entry.data.title}
|
|
111
|
+
class="card-img-top"
|
|
112
|
+
style="height:200px;object-fit:cover;"
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
<div class="card-body d-flex flex-column">
|
|
116
|
+
<h5 class="card-title">
|
|
117
|
+
<a
|
|
118
|
+
href={\`/__COL__/\${entry.id.replace(/\\.mdx?$/, '')}\`}
|
|
119
|
+
class="text-decoration-none text-dark"
|
|
120
|
+
>
|
|
121
|
+
{entry.data.title}
|
|
122
|
+
</a>
|
|
123
|
+
</h5>
|
|
124
|
+
<p class="card-text text-muted small flex-grow-1">
|
|
125
|
+
{entry.data.description}
|
|
126
|
+
</p>
|
|
127
|
+
{entry.data.tags && (
|
|
128
|
+
<div class="d-flex flex-wrap gap-1 mt-2">
|
|
129
|
+
{entry.data.tags.map(tag => (
|
|
130
|
+
<span class="badge bg-primary-subtle text-primary-emphasis">
|
|
131
|
+
{tag}
|
|
132
|
+
</span>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
<div class="card-footer text-muted small">
|
|
138
|
+
<time datetime={entry.data.date.toISOString()}>
|
|
139
|
+
{entry.data.date.toLocaleDateString('en-US', {
|
|
140
|
+
year: 'numeric', month: 'long', day: 'numeric',
|
|
141
|
+
})}
|
|
142
|
+
</time>
|
|
143
|
+
{entry.data.author && <span> · {entry.data.author}</span>}
|
|
144
|
+
</div>
|
|
145
|
+
</article>
|
|
146
|
+
</div>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{entries.length === 0 && (
|
|
151
|
+
<p class="text-center text-muted mt-5">
|
|
152
|
+
No entries yet — add Markdown files to{' '}
|
|
153
|
+
<code>usr/content/__COL__/</code> and set <code>draft: false</code>.
|
|
154
|
+
</p>
|
|
155
|
+
)}
|
|
156
|
+
</main>
|
|
157
|
+
</BaseLayout>
|
|
158
|
+
`,
|
|
159
|
+
|
|
160
|
+
docs: `---
|
|
161
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
162
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
163
|
+
|
|
164
|
+
export const prerender = true;
|
|
165
|
+
|
|
166
|
+
// TODO: customise title, description, and category groupings below
|
|
167
|
+
const title = '__COL__';
|
|
168
|
+
const description = 'Documentation for __COL__';
|
|
169
|
+
|
|
170
|
+
type Entry = CollectionEntry<'__COL__'>;
|
|
171
|
+
|
|
172
|
+
const entries = (await getCollection('__COL__'))
|
|
173
|
+
.filter((e: Entry) => !e.data.draft)
|
|
174
|
+
.sort((a: Entry, b: Entry) => (a.data.order ?? 999) - (b.data.order ?? 999));
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
<BaseLayout title={title} description={description}>
|
|
178
|
+
<main class="container py-5">
|
|
179
|
+
<div class="mb-5">
|
|
180
|
+
<h1 class="mb-0">{title}</h1>
|
|
181
|
+
<p class="lead text-secondary">{description}</p>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div class="row g-3">
|
|
185
|
+
{entries.map(entry => (
|
|
186
|
+
<div class="col-12 col-md-6">
|
|
187
|
+
<a
|
|
188
|
+
href={\`/__COL__/\${entry.id.replace(/\\.mdx?$/, '')}\`}
|
|
189
|
+
class="card p-4 h-100 text-decoration-none border shadow-sm d-block"
|
|
190
|
+
>
|
|
191
|
+
<h3 class="h5 fw-semibold text-body mb-2 mt-0">
|
|
192
|
+
{entry.data.title}
|
|
193
|
+
</h3>
|
|
194
|
+
{entry.data.description && (
|
|
195
|
+
<p class="text-secondary mb-0 small">{entry.data.description}</p>
|
|
196
|
+
)}
|
|
197
|
+
{entry.data.category && (
|
|
198
|
+
<span class="badge bg-secondary-subtle text-secondary-emphasis mt-2">
|
|
199
|
+
{entry.data.category}
|
|
200
|
+
</span>
|
|
201
|
+
)}
|
|
202
|
+
</a>
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{entries.length === 0 && (
|
|
208
|
+
<p class="text-center text-muted mt-5">
|
|
209
|
+
No entries yet — add Markdown files to{' '}
|
|
210
|
+
<code>usr/content/__COL__/</code> and set <code>draft: false</code>.
|
|
211
|
+
</p>
|
|
212
|
+
)}
|
|
213
|
+
</main>
|
|
214
|
+
</BaseLayout>
|
|
215
|
+
`,
|
|
216
|
+
|
|
217
|
+
generic: `---
|
|
218
|
+
import { getCollection } from 'astro:content';
|
|
219
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
220
|
+
|
|
221
|
+
export const prerender = true;
|
|
222
|
+
|
|
223
|
+
// TODO: customise title and description below
|
|
224
|
+
const title = '__COL__';
|
|
225
|
+
const description = 'All entries in the __COL__ collection';
|
|
226
|
+
|
|
227
|
+
const entries = (await getCollection('__COL__'))
|
|
228
|
+
.filter(e => !e.data.draft);
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
<BaseLayout title={title} description={description}>
|
|
232
|
+
<main class="container py-5">
|
|
233
|
+
<div class="mb-5">
|
|
234
|
+
<h1 class="mb-0">{title}</h1>
|
|
235
|
+
<p class="lead">{description}</p>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<ul class="list-group list-group-flush">
|
|
239
|
+
{entries.map(entry => (
|
|
240
|
+
<li class="list-group-item">
|
|
241
|
+
<a
|
|
242
|
+
href={\`/__COL__/\${entry.id.replace(/\\.mdx?$/, '')}\`}
|
|
243
|
+
class="text-decoration-none fw-semibold"
|
|
244
|
+
>
|
|
245
|
+
{entry.data.title}
|
|
246
|
+
</a>
|
|
247
|
+
{entry.data.description && (
|
|
248
|
+
<p class="mb-0 text-muted small">{entry.data.description}</p>
|
|
249
|
+
)}
|
|
250
|
+
</li>
|
|
251
|
+
))}
|
|
252
|
+
</ul>
|
|
253
|
+
|
|
254
|
+
{entries.length === 0 && (
|
|
255
|
+
<p class="text-center text-muted mt-5">
|
|
256
|
+
No entries yet — add Markdown files to{' '}
|
|
257
|
+
<code>usr/content/__COL__/</code> and set <code>draft: false</code>.
|
|
258
|
+
</p>
|
|
259
|
+
)}
|
|
260
|
+
</main>
|
|
261
|
+
</BaseLayout>
|
|
262
|
+
`,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const SLUG = {
|
|
266
|
+
|
|
267
|
+
blog: `---
|
|
268
|
+
import { getCollection, render } from 'astro:content';
|
|
269
|
+
import { type CollectionEntry } from 'astro:content';
|
|
270
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
271
|
+
|
|
272
|
+
export const prerender = true;
|
|
273
|
+
|
|
274
|
+
export async function getStaticPaths() {
|
|
275
|
+
const entries = await getCollection('__COL__');
|
|
276
|
+
return entries.map(entry => ({
|
|
277
|
+
params: { slug: entry.id.replace(/\\.mdx?$/, '') },
|
|
278
|
+
props: { entry },
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
interface Props { entry: CollectionEntry<'__COL__'>; }
|
|
283
|
+
|
|
284
|
+
const { entry } = Astro.props;
|
|
285
|
+
const { Content } = await render(entry);
|
|
286
|
+
|
|
287
|
+
const formattedDate = entry.data.date.toLocaleDateString('en-US', {
|
|
288
|
+
year: 'numeric', month: 'long', day: 'numeric',
|
|
289
|
+
});
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
|
293
|
+
<main class="container py-5">
|
|
294
|
+
<article>
|
|
295
|
+
<div class="mb-4 text-end">
|
|
296
|
+
<a href="/__COL__" class="btn btn-outline-secondary btn-sm">
|
|
297
|
+
← Back to __COL__
|
|
298
|
+
</a>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<h1 class="mb-3">{entry.data.title}</h1>
|
|
302
|
+
<p class="lead text-muted mb-4">{entry.data.description}</p>
|
|
303
|
+
|
|
304
|
+
<div class="d-flex flex-wrap align-items-center gap-3 small text-muted mb-4 pb-4 border-bottom">
|
|
305
|
+
<time datetime={entry.data.date.toISOString()}>{formattedDate}</time>
|
|
306
|
+
{entry.data.author && <span>· By {entry.data.author}</span>}
|
|
307
|
+
{entry.data.tags && entry.data.tags.map(tag => (
|
|
308
|
+
<span class="badge bg-primary-subtle text-primary-emphasis">{tag}</span>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
{entry.data.image && (
|
|
313
|
+
<img
|
|
314
|
+
src={entry.data.image}
|
|
315
|
+
alt={entry.data.title}
|
|
316
|
+
class="img-fluid rounded mb-5 w-100"
|
|
317
|
+
style="max-height:400px;object-fit:cover;"
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
<div class="markdown-content">
|
|
322
|
+
<Content />
|
|
323
|
+
</div>
|
|
324
|
+
</article>
|
|
325
|
+
</main>
|
|
326
|
+
</BaseLayout>
|
|
327
|
+
`,
|
|
328
|
+
|
|
329
|
+
docs: `---
|
|
330
|
+
import { getCollection, render } from 'astro:content';
|
|
331
|
+
import { type CollectionEntry } from 'astro:content';
|
|
332
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
333
|
+
|
|
334
|
+
export const prerender = true;
|
|
335
|
+
|
|
336
|
+
export async function getStaticPaths() {
|
|
337
|
+
const entries = await getCollection('__COL__');
|
|
338
|
+
return entries.map(entry => ({
|
|
339
|
+
params: { slug: entry.id.replace(/\\.mdx?$/, '') },
|
|
340
|
+
props: { entry },
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
interface Props { entry: CollectionEntry<'__COL__'>; }
|
|
345
|
+
|
|
346
|
+
const { entry } = Astro.props;
|
|
347
|
+
const { Content } = await render(entry);
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
|
351
|
+
<main class="container py-4">
|
|
352
|
+
<article>
|
|
353
|
+
<div class="mb-4 text-end">
|
|
354
|
+
<a href="/__COL__" class="small btn btn-outline-secondary btn-sm">
|
|
355
|
+
← Back to __COL__
|
|
356
|
+
</a>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<h1 class="mb-2">{entry.data.title}</h1>
|
|
360
|
+
{entry.data.description && (
|
|
361
|
+
<p class="lead text-muted mb-4">{entry.data.description}</p>
|
|
362
|
+
)}
|
|
363
|
+
{entry.data.category && (
|
|
364
|
+
<span class="badge bg-secondary-subtle text-secondary-emphasis mb-4 d-inline-block">
|
|
365
|
+
{entry.data.category}
|
|
366
|
+
</span>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
<div class="markdown-content mt-4">
|
|
370
|
+
<Content />
|
|
371
|
+
</div>
|
|
372
|
+
</article>
|
|
373
|
+
</main>
|
|
374
|
+
</BaseLayout>
|
|
375
|
+
`,
|
|
376
|
+
|
|
377
|
+
generic: `---
|
|
378
|
+
import { getCollection, render } from 'astro:content';
|
|
379
|
+
import { type CollectionEntry } from 'astro:content';
|
|
380
|
+
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
381
|
+
|
|
382
|
+
export const prerender = true;
|
|
383
|
+
|
|
384
|
+
export async function getStaticPaths() {
|
|
385
|
+
const entries = await getCollection('__COL__');
|
|
386
|
+
return entries.map(entry => ({
|
|
387
|
+
params: { slug: entry.id.replace(/\\.mdx?$/, '') },
|
|
388
|
+
props: { entry },
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface Props { entry: CollectionEntry<'__COL__'>; }
|
|
393
|
+
|
|
394
|
+
const { entry } = Astro.props;
|
|
395
|
+
const { Content } = await render(entry);
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
<BaseLayout title={entry.data.title} description={entry.data.description}>
|
|
399
|
+
<main class="container py-5">
|
|
400
|
+
<article>
|
|
401
|
+
<div class="mb-4 text-end">
|
|
402
|
+
<a href="/__COL__" class="btn btn-outline-secondary btn-sm">
|
|
403
|
+
← Back to __COL__
|
|
404
|
+
</a>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<h1 class="mb-3">{entry.data.title}</h1>
|
|
408
|
+
{entry.data.description && (
|
|
409
|
+
<p class="lead text-muted mb-4">{entry.data.description}</p>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
<div class="markdown-content mt-4">
|
|
413
|
+
<Content />
|
|
414
|
+
</div>
|
|
415
|
+
</article>
|
|
416
|
+
</main>
|
|
417
|
+
</BaseLayout>
|
|
418
|
+
`,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
422
|
+
async function main() {
|
|
423
|
+
// Parse existing collections
|
|
424
|
+
const cfgSrc = readFileSync(CONFIG_FILE, 'utf8');
|
|
425
|
+
const exportM = cfgSrc.match(/export const collections\s*=\s*\{([^}]+)\}/s);
|
|
426
|
+
const existing = exportM
|
|
427
|
+
? [...exportM[1].matchAll(/(?:['"]([\w-]+)['"]|(\w+))\s*:/g)].map(m => m[1] ?? m[2])
|
|
428
|
+
: [];
|
|
429
|
+
|
|
430
|
+
console.log('');
|
|
431
|
+
console.log(`${B}Existing collections:${X}`);
|
|
432
|
+
if (existing.length === 0) console.log(' (none)');
|
|
433
|
+
else for (const c of existing) console.log(` • ${c}`);
|
|
434
|
+
console.log('');
|
|
435
|
+
|
|
436
|
+
// ─── Prompt: collection name ────────────────────────────────────────────────
|
|
437
|
+
let colName;
|
|
438
|
+
while (true) {
|
|
439
|
+
const raw = (await ask(`${B}New collection name${X} (lowercase letters, numbers, hyphens): `)).replace(/\s/g, '');
|
|
440
|
+
if (!/^[a-z]([a-z0-9-]*[a-z0-9])?$/.test(raw)) {
|
|
441
|
+
warn('Name must start with a letter and contain only lowercase letters, numbers, and hyphens.');
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (existing.includes(raw)) {
|
|
445
|
+
warn(`Collection '${raw}' already exists. Choose a different name.`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
colName = raw;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ─── Derive camelCase variable name: my-news → myNewsCollection ──────────
|
|
453
|
+
const parts = colName.split('-');
|
|
454
|
+
const varName = parts[0] + parts.slice(1).map(p => p[0].toUpperCase() + p.slice(1)).join('') + 'Collection';
|
|
455
|
+
|
|
456
|
+
// ─── Prompt: collection type ──────────────────────────────────────────────
|
|
457
|
+
console.log('');
|
|
458
|
+
console.log(`${B}Collection type:${X}`);
|
|
459
|
+
console.log(' 1) blog — articles (date, author, tags, image, draft)');
|
|
460
|
+
console.log(' 2) docs — documentation pages (order, category, draft)');
|
|
461
|
+
console.log(' 3) generic — simple entries (title, optional description, draft)');
|
|
462
|
+
console.log('');
|
|
463
|
+
const typeRaw = (await ask('Choose type [1/2/3] (default: 3): ')).trim() || '3';
|
|
464
|
+
const colType = typeRaw === '1' ? 'blog' : typeRaw === '2' ? 'docs' : 'generic';
|
|
465
|
+
ok(`Type: ${colType}`);
|
|
466
|
+
rl.close();
|
|
467
|
+
|
|
468
|
+
// ─── 1. Update usr/content.config.ts ──────────────────────────────────────
|
|
469
|
+
const newBlock =
|
|
470
|
+
`\n// Schema for ${colName} entries [${colType} type]\n` +
|
|
471
|
+
`// TODO: customize the schema fields to match your content structure\n` +
|
|
472
|
+
`const ${varName} = defineCollection({\n` +
|
|
473
|
+
` loader: glob({\n` +
|
|
474
|
+
` pattern: '**/*.{md,mdx}',\n` +
|
|
475
|
+
` base: './usr/content/${colName}',\n` +
|
|
476
|
+
` }),\n` +
|
|
477
|
+
` schema: z.object({\n` +
|
|
478
|
+
`${SCHEMA[colType]}\n` +
|
|
479
|
+
` }),\n` +
|
|
480
|
+
`});\n`;
|
|
481
|
+
|
|
482
|
+
const MARKER = '// Export all collections';
|
|
483
|
+
let cfg = readFileSync(CONFIG_FILE, 'utf8');
|
|
484
|
+
if (!cfg.includes(MARKER)) error(`Marker '${MARKER}' not found in config.ts`);
|
|
485
|
+
|
|
486
|
+
cfg = cfg.replace(MARKER, newBlock + MARKER);
|
|
487
|
+
cfg = cfg.replace(
|
|
488
|
+
/(export const collections\s*=\s*\{)([^}]+)(\};)/s,
|
|
489
|
+
(_, open, inner, close) => `${open}${inner.trimEnd()}\n ${toObjectKey(colName)}: ${varName},\n${close}`
|
|
490
|
+
);
|
|
491
|
+
writeFileSync(CONFIG_FILE, cfg, 'utf8');
|
|
492
|
+
ok('usr/content.config.ts updated');
|
|
493
|
+
|
|
494
|
+
// ─── 2. Sample content file ───────────────────────────────────────────────
|
|
495
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
496
|
+
const contentDir = join(ROOT_DIR, 'usr', 'content', colName);
|
|
497
|
+
mkdirSync(contentDir, { recursive: true });
|
|
498
|
+
|
|
499
|
+
const SAMPLE_NAME = { blog: 'sample-post.md', docs: 'sample-doc.md', generic: 'sample.md' };
|
|
500
|
+
const SAMPLE_BODY = {
|
|
501
|
+
blog:
|
|
502
|
+
`---\ntitle: "Sample Post"\n` +
|
|
503
|
+
`description: "A sample post in the ${colName} collection."\n` +
|
|
504
|
+
`date: ${today}\nauthor: "Your Name"\ntags: ["sample"]\ndraft: true\n---\n\n` +
|
|
505
|
+
`# Sample Post\n\nThis is a sample post. Replace this file with real content.\n`,
|
|
506
|
+
docs:
|
|
507
|
+
`---\ntitle: "Sample Document"\n` +
|
|
508
|
+
`description: "A sample document in the ${colName} collection."\n` +
|
|
509
|
+
`order: 1\ncategory: "general"\ndraft: true\n---\n\n` +
|
|
510
|
+
`# Sample Document\n\nThis is a sample document. Replace this file with real content.\n`,
|
|
511
|
+
generic:
|
|
512
|
+
`---\ntitle: "Sample Entry"\n` +
|
|
513
|
+
`description: "A sample entry in the ${colName} collection."\n` +
|
|
514
|
+
`draft: true\n---\n\n` +
|
|
515
|
+
`# Sample Entry\n\nThis is a sample entry. Replace this file with real content.\n`,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const samplePath = join(contentDir, SAMPLE_NAME[colType]);
|
|
519
|
+
writeFileSync(samplePath, SAMPLE_BODY[colType], 'utf8');
|
|
520
|
+
ok(relative(ROOT_DIR, samplePath));
|
|
521
|
+
|
|
522
|
+
// ─── 3. Page templates ────────────────────────────────────────────────────
|
|
523
|
+
const pagesDir = join(ROOT_DIR, 'usr', 'pages', colName);
|
|
524
|
+
mkdirSync(pagesDir, { recursive: true });
|
|
525
|
+
|
|
526
|
+
const sub = (s) => s.replaceAll('__COL__', colName);
|
|
527
|
+
const indexPath = join(pagesDir, 'index.astro');
|
|
528
|
+
const slugPath = join(pagesDir, '[...slug].astro');
|
|
529
|
+
|
|
530
|
+
writeFileSync(indexPath, sub(INDEX[colType]), 'utf8');
|
|
531
|
+
writeFileSync(slugPath, sub(SLUG[colType]), 'utf8');
|
|
532
|
+
ok(relative(ROOT_DIR, indexPath));
|
|
533
|
+
ok(relative(ROOT_DIR, slugPath));
|
|
534
|
+
|
|
535
|
+
// ─── Summary ──────────────────────────────────────────────────────────────
|
|
536
|
+
console.log('');
|
|
537
|
+
console.log(`${G}${B}Collection '${colName}' scaffolded successfully!${X}`);
|
|
538
|
+
console.log('');
|
|
539
|
+
console.log(` ${B}Next steps:${X}`);
|
|
540
|
+
console.log(` 1. Open ${B}usr/content.config.ts${X} and review the schema for '${colName}'`);
|
|
541
|
+
console.log(` 2. Edit the page files in ${B}usr/pages/${colName}/${X} to customise the UI`);
|
|
542
|
+
console.log(` 3. Replace the sample file in ${B}usr/content/${colName}/${X} with real content`);
|
|
543
|
+
console.log(` 4. Run ${B}npm run dev${X} and visit ${B}/${colName}${X}`);
|
|
544
|
+
console.log('');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
main().catch(err => error(String(err)));
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// add-content.mjs — Add a new content file to an existing s:CMS collection.
|
|
3
|
+
// Usage: npx scms-add-content
|
|
4
|
+
// or: npm run add-content (from a consuming site's package.json)
|
|
5
|
+
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
8
|
+
import { join, relative } from 'node:path';
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
// This file ships inside the @lad-sapienza/scms-core package (bin/), so its
|
|
12
|
+
// own location says nothing about the consuming site's layout. ROOT_DIR must
|
|
13
|
+
// be the invoking project's root, i.e. wherever the CLI was run from.
|
|
14
|
+
const ROOT_DIR = process.cwd();
|
|
15
|
+
const CONFIG_FILE = join(ROOT_DIR, 'usr', 'content.config.ts');
|
|
16
|
+
const CONTENT_BASE = join(ROOT_DIR, 'usr', 'content');
|
|
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
|
+
if (!existsSync(CONFIG_FILE)) error(`Cannot find ${CONFIG_FILE}`);
|
|
26
|
+
|
|
27
|
+
// ─── Readline helper ──────────────────────────────────────────────────────────
|
|
28
|
+
// Uses the readline interface as an async iterator rather than repeated
|
|
29
|
+
// rl.question() calls: with piped/pasted (non-TTY) input, multiple lines can
|
|
30
|
+
// arrive in a single chunk, and question()'s one-shot 'line' listener isn't
|
|
31
|
+
// registered in time to catch answers past the first — they're silently
|
|
32
|
+
// dropped and the prompt hangs forever with no error. Iterating the interface
|
|
33
|
+
// consumes lines through its internal queue instead, so it doesn't race.
|
|
34
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
rl.on('SIGINT', () => { console.log(''); process.exit(0); });
|
|
36
|
+
const rlIterator = rl[Symbol.asyncIterator]();
|
|
37
|
+
const ask = async (prompt) => {
|
|
38
|
+
process.stdout.write(prompt);
|
|
39
|
+
const { value, done } = await rlIterator.next();
|
|
40
|
+
if (done) { console.log(''); process.exit(0); }
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/** Parse top-level collection keys from export const collections = { ... } */
|
|
47
|
+
function parseCollections(src) {
|
|
48
|
+
const m = src.match(/export const collections\s*=\s*\{([^}]+)\}/s);
|
|
49
|
+
if (!m) return [];
|
|
50
|
+
// Keys may be quoted (e.g. 'my-collection': ...) when the name isn't a
|
|
51
|
+
// valid bare identifier — see toObjectKey() in add-collection.mjs.
|
|
52
|
+
return [...m[1].matchAll(/(?:['"]([\w-]+)['"]|(\w+))\s*:/g)].map(x => x[1] ?? x[2]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Count .md / .mdx files recursively in a directory */
|
|
56
|
+
function countMdFiles(dir) {
|
|
57
|
+
try {
|
|
58
|
+
return readdirSync(dir, { recursive: true })
|
|
59
|
+
.filter(f => f.endsWith('.md') || f.endsWith('.mdx'))
|
|
60
|
+
.length;
|
|
61
|
+
} catch { return 0; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Return true if any .mdx file exists under dir */
|
|
65
|
+
function hasMdx(dir) {
|
|
66
|
+
try {
|
|
67
|
+
return readdirSync(dir, { recursive: true }).some(f => f.endsWith('.mdx'));
|
|
68
|
+
} catch { return false; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Extract z.object field lines for a named collection from config.ts */
|
|
72
|
+
function parseSchemaFields(src, colName) {
|
|
73
|
+
const pat = new RegExp(
|
|
74
|
+
`defineCollection\\(\\{.*?base\\s*:\\s*['"]\\./usr/content/${colName}['"].*?z\\.object\\(\\{(.*?)\\}\\)`,
|
|
75
|
+
's'
|
|
76
|
+
);
|
|
77
|
+
const m = src.match(pat);
|
|
78
|
+
if (!m) return [];
|
|
79
|
+
|
|
80
|
+
const fields = [];
|
|
81
|
+
for (let line of m[1].split('\n')) {
|
|
82
|
+
line = line.trim().replace(/,$/, '');
|
|
83
|
+
if (!line || line.startsWith('//')) continue;
|
|
84
|
+
const fm = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
85
|
+
if (!fm) continue;
|
|
86
|
+
const [, name, ztype] = fm;
|
|
87
|
+
const optional = ztype.includes('.optional()');
|
|
88
|
+
let kind;
|
|
89
|
+
if (ztype.includes('z.array')) kind = 'array';
|
|
90
|
+
else if (ztype.includes('z.number')) kind = 'number';
|
|
91
|
+
else if (ztype.includes('z.boolean')) kind = 'boolean';
|
|
92
|
+
else if (ztype.includes('z.coerce.date') || ztype.includes('z.date')) kind = 'date';
|
|
93
|
+
else kind = 'string';
|
|
94
|
+
fields.push({ name, kind, optional });
|
|
95
|
+
}
|
|
96
|
+
return fields;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Escape a value for a YAML double-quoted scalar (backslash first, then quotes) */
|
|
100
|
+
function yamlQuote(value) {
|
|
101
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Format a single frontmatter line for a given value and Zod kind */
|
|
105
|
+
function toYamlLine(name, value, kind) {
|
|
106
|
+
if (kind === 'boolean') {
|
|
107
|
+
const b = ['true', 'yes', '1'].includes(value.toLowerCase());
|
|
108
|
+
return `${name}: ${b ? 'true' : 'false'}`;
|
|
109
|
+
}
|
|
110
|
+
if (kind === 'number') return `${name}: ${value}`;
|
|
111
|
+
if (kind === 'array') {
|
|
112
|
+
const items = value.split(',').map(s => s.trim()).filter(Boolean);
|
|
113
|
+
return `${name}: [${items.map(yamlQuote).join(', ')}]`;
|
|
114
|
+
}
|
|
115
|
+
if (kind === 'date') return `${name}: ${value}`;
|
|
116
|
+
return `${name}: ${yamlQuote(value)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
120
|
+
async function main() {
|
|
121
|
+
const cfgSrc = readFileSync(CONFIG_FILE, 'utf8');
|
|
122
|
+
const allCollections = parseCollections(cfgSrc);
|
|
123
|
+
|
|
124
|
+
// Keep only collections that have a content directory with actual files
|
|
125
|
+
const collections = allCollections.filter(col => {
|
|
126
|
+
const dir = join(CONTENT_BASE, col);
|
|
127
|
+
return existsSync(dir) && statSync(dir).isDirectory();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log(`${B}Available collections:${X}`);
|
|
132
|
+
if (collections.length === 0) {
|
|
133
|
+
console.log(" (none found — run 'npm run add-collection' first)");
|
|
134
|
+
rl.close();
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
collections.forEach((col, i) => {
|
|
138
|
+
const count = countMdFiles(join(CONTENT_BASE, col));
|
|
139
|
+
console.log(` ${i + 1}) ${col} (${count} file(s))`);
|
|
140
|
+
});
|
|
141
|
+
console.log('');
|
|
142
|
+
|
|
143
|
+
// ─── Prompt: collection ───────────────────────────────────────────────────
|
|
144
|
+
let collection;
|
|
145
|
+
while (true) {
|
|
146
|
+
const raw = (await ask(`${B}Collection${X} [name or number]: `)).trim();
|
|
147
|
+
if (!raw) { warn('Please enter a name or number.'); continue; }
|
|
148
|
+
if (/^\d+$/.test(raw)) {
|
|
149
|
+
const idx = parseInt(raw, 10) - 1;
|
|
150
|
+
if (idx >= 0 && idx < collections.length) { collection = collections[idx]; break; }
|
|
151
|
+
warn('Number out of range.');
|
|
152
|
+
} else {
|
|
153
|
+
if (collections.includes(raw)) { collection = raw; break; }
|
|
154
|
+
warn(`'${raw}' is not an available collection.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
ok(`Collection: ${collection}`);
|
|
158
|
+
|
|
159
|
+
// ─── Prompt: file format ─────────────────────────────────────────────────
|
|
160
|
+
const colDir = join(CONTENT_BASE, collection);
|
|
161
|
+
const defaultExt = hasMdx(colDir) ? 'mdx' : 'md';
|
|
162
|
+
console.log('');
|
|
163
|
+
const extRaw = (await ask(`${B}File format${X} [md/mdx] (default: ${defaultExt}): `)).trim() || defaultExt;
|
|
164
|
+
const ext = (extRaw === 'md' || extRaw === 'mdx') ? extRaw : defaultExt;
|
|
165
|
+
|
|
166
|
+
// ─── Prompt: slug ─────────────────────────────────────────────────────────
|
|
167
|
+
console.log('');
|
|
168
|
+
console.log(' Slug becomes the URL path and file name.');
|
|
169
|
+
console.log(' Use lowercase letters, numbers, and hyphens. Subfolders allowed (e.g. guides/my-topic).');
|
|
170
|
+
console.log('');
|
|
171
|
+
let slug;
|
|
172
|
+
while (true) {
|
|
173
|
+
const raw = (await ask(`${B}Slug${X}: `)).trim().replace(/^\//, '').replace(/\/$/, '');
|
|
174
|
+
if (!raw) { warn('Slug cannot be empty.'); continue; }
|
|
175
|
+
if (!/^[a-z0-9/_-]+$/.test(raw)) {
|
|
176
|
+
warn('Slug must contain only lowercase letters, numbers, hyphens, underscores, and forward slashes.');
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (existsSync(join(colDir, raw, `index.${ext}`))) {
|
|
180
|
+
warn(`File already exists: usr/content/${collection}/${raw}/index.${ext}`);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
slug = raw;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Collect frontmatter values ───────────────────────────────────────────
|
|
188
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
189
|
+
const fields = parseSchemaFields(cfgSrc, collection);
|
|
190
|
+
|
|
191
|
+
let gitAuthor = '';
|
|
192
|
+
try {
|
|
193
|
+
gitAuthor = execSync('git config user.name', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
194
|
+
} catch { /* git not available or no config */ }
|
|
195
|
+
|
|
196
|
+
console.log('');
|
|
197
|
+
console.log(`${B}Frontmatter fields${X} (press Enter to use the default value shown in brackets):`);
|
|
198
|
+
console.log('');
|
|
199
|
+
|
|
200
|
+
const fmLines = [];
|
|
201
|
+
let titleValue = slug; // used as the markdown H1 heading fallback
|
|
202
|
+
|
|
203
|
+
for (const { name, kind, optional } of fields) {
|
|
204
|
+
let defaultVal = '';
|
|
205
|
+
if (kind === 'date') defaultVal = today;
|
|
206
|
+
else if (name === 'draft') defaultVal = 'true';
|
|
207
|
+
else if (name === 'author' && gitAuthor) defaultVal = gitAuthor;
|
|
208
|
+
|
|
209
|
+
let prompt = ` ${name}`;
|
|
210
|
+
if (optional) prompt += ' (optional)';
|
|
211
|
+
if (kind === 'array') prompt += ' [comma-separated]';
|
|
212
|
+
if (defaultVal) prompt += ` [${defaultVal}]`;
|
|
213
|
+
prompt += ': ';
|
|
214
|
+
|
|
215
|
+
let value = (await ask(prompt)).trim();
|
|
216
|
+
if (!value && defaultVal) value = defaultVal;
|
|
217
|
+
|
|
218
|
+
if (!value) {
|
|
219
|
+
if (optional) continue; // skip optional empty fields
|
|
220
|
+
fmLines.push(`${name}: ""`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (name === 'title') titleValue = value;
|
|
225
|
+
fmLines.push(toYamlLine(name, value, kind));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
rl.close();
|
|
229
|
+
|
|
230
|
+
// ─── Write file ───────────────────────────────────────────────────────────
|
|
231
|
+
const targetDir = join(colDir, slug);
|
|
232
|
+
const targetPath = join(targetDir, `index.${ext}`);
|
|
233
|
+
mkdirSync(targetDir, { recursive: true });
|
|
234
|
+
|
|
235
|
+
const content = [
|
|
236
|
+
'---',
|
|
237
|
+
...fmLines,
|
|
238
|
+
'---',
|
|
239
|
+
'',
|
|
240
|
+
`# ${titleValue}`,
|
|
241
|
+
'',
|
|
242
|
+
'<!-- Write your content here -->',
|
|
243
|
+
'',
|
|
244
|
+
].join('\n');
|
|
245
|
+
|
|
246
|
+
writeFileSync(targetPath, content, 'utf8');
|
|
247
|
+
|
|
248
|
+
console.log('');
|
|
249
|
+
ok(`Created: ${relative(ROOT_DIR, targetPath)}`);
|
|
250
|
+
|
|
251
|
+
// ─── Summary ──────────────────────────────────────────────────────────────
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(`${G}${B}Done!${X}`);
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(` ${B}Next steps:${X}`);
|
|
256
|
+
console.log(` 1. Open ${B}usr/content/${collection}/${slug}/index.${ext}${X} and write your content`);
|
|
257
|
+
console.log(` 2. Set ${B}draft: false${X} when the content is ready to publish`);
|
|
258
|
+
console.log(` 3. Run ${B}npm run dev${X} and visit ${B}/${collection}/${slug}${X}`);
|
|
259
|
+
console.log('');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
main().catch(err => error(String(err)));
|
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.1",
|
|
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",
|
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
},
|
|
11
11
|
"author": "Julian Bogdani <julian.bogdani@uniroma1.it>",
|
|
12
12
|
"main": "./index.ts",
|
|
13
|
+
"bin": {
|
|
14
|
+
"scms-add-collection": "./bin/add-collection.mjs",
|
|
15
|
+
"scms-add-content": "./bin/add-content.mjs"
|
|
16
|
+
},
|
|
13
17
|
"files": [
|
|
14
18
|
"index.ts",
|
|
15
19
|
"scms.ts",
|
|
20
|
+
"bin",
|
|
16
21
|
"components",
|
|
17
22
|
"integrations",
|
|
18
23
|
"utils",
|