koguma 2.3.7 → 2.3.8
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 +36 -10
- package/cli/content.ts +47 -1
- package/cli/index.ts +14 -3
- package/cli/scaffold.ts +12 -4
- package/package.json +1 -1
- package/src/admin/_bundle.ts +1 -1
package/README.md
CHANGED
|
@@ -145,29 +145,55 @@ Koguma uses a `content/` directory for file-based content that lives in your git
|
|
|
145
145
|
|
|
146
146
|
```
|
|
147
147
|
content/
|
|
148
|
-
├── post/
|
|
149
|
-
│ ├── hello-world.md
|
|
150
|
-
│
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
├── post/ # collection — one file per entry
|
|
149
|
+
│ ├── hello-world.md # slug = "hello-world"
|
|
150
|
+
│ ├── our-mission.md
|
|
151
|
+
│ └── our-mission.heroBody.md # sibling markdown field
|
|
152
|
+
├── landingPage/ # singleton — always index.md
|
|
153
|
+
│ ├── index.md # slug = content type ID
|
|
154
|
+
│ ├── heroBody.md # sibling markdown field
|
|
155
|
+
│ └── ctaBody.md # sibling markdown field
|
|
156
|
+
└── media/ # local images (synced to R2)
|
|
154
157
|
└── hero-banner.jpg
|
|
155
158
|
```
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
### File Format
|
|
161
|
+
|
|
162
|
+
Every content file uses **YAML frontmatter + markdown body**:
|
|
158
163
|
|
|
159
164
|
```markdown
|
|
160
165
|
---
|
|
161
166
|
title: Our Mission
|
|
162
167
|
slug: our-mission
|
|
163
|
-
heroImage: hero-banner
|
|
168
|
+
heroImage: media-hero-banner
|
|
164
169
|
published: true
|
|
165
170
|
date: 2026-03-01
|
|
166
171
|
---
|
|
167
172
|
|
|
168
|
-
|
|
173
|
+
This body content maps to the **first** `field.markdown()` in the content type.
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Singletons vs Collections
|
|
177
|
+
|
|
178
|
+
- **Collections** (default): Each entry is a file named `{slug}.md`
|
|
179
|
+
- **Singletons** (`singleton: true`): One entry, always `index.md`. The slug is auto-set to the content type ID.
|
|
180
|
+
|
|
181
|
+
### Sibling Markdown Fields
|
|
169
182
|
|
|
170
|
-
|
|
183
|
+
When a content type has **multiple** `field.markdown()` fields, the first one maps to the file body. Additional markdown fields are stored as **sibling files** (pure markdown, no frontmatter):
|
|
184
|
+
|
|
185
|
+
| | Collections | Singletons |
|
|
186
|
+
| ------------------- | --------------------- | -------------- |
|
|
187
|
+
| **Main file** | `{slug}.md` | `index.md` |
|
|
188
|
+
| **Sibling pattern** | `{slug}.{fieldId}.md` | `{fieldId}.md` |
|
|
189
|
+
|
|
190
|
+
Example singleton with 3 markdown fields:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
content/landingPage/
|
|
194
|
+
├── index.md # frontmatter + 1st markdown field body
|
|
195
|
+
├── heroBody.md # 2nd markdown field (pure markdown)
|
|
196
|
+
└── ctaBody.md # 3rd markdown field (pure markdown)
|
|
171
197
|
```
|
|
172
198
|
|
|
173
199
|
**Git is your version history.** No custom versioning table needed.
|
package/cli/content.ts
CHANGED
|
@@ -611,9 +611,55 @@ export function validateContent(
|
|
|
611
611
|
if (dotIdx > 0 && extraMdFieldIds.has(name.slice(dotIdx + 1))) continue;
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
const entry = parseContentFile(filePath, subdir);
|
|
615
614
|
const relPath = `${subdir}/${file}`;
|
|
616
615
|
|
|
616
|
+
// Check for orphan / misnamed files
|
|
617
|
+
if (ctInfo.singleton) {
|
|
618
|
+
// Singletons: only index.md is a valid entry
|
|
619
|
+
if (name !== 'index') {
|
|
620
|
+
// Catch the index.{fieldId}.md mistake
|
|
621
|
+
if (name.startsWith('index.')) {
|
|
622
|
+
const possibleFieldId = name.slice('index.'.length);
|
|
623
|
+
if (extraMdFieldIds.has(possibleFieldId)) {
|
|
624
|
+
warnings.push({
|
|
625
|
+
level: 'warn',
|
|
626
|
+
file: relPath,
|
|
627
|
+
message: `misnamed sibling — singleton siblings should be "${possibleFieldId}.md", not "index.${possibleFieldId}.md"`
|
|
628
|
+
});
|
|
629
|
+
} else {
|
|
630
|
+
warnings.push({
|
|
631
|
+
level: 'warn',
|
|
632
|
+
file: relPath,
|
|
633
|
+
message: `unrecognized file in singleton — only index.md and {fieldId}.md siblings are expected`
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
} else if (!extraMdFieldIds.has(name)) {
|
|
637
|
+
warnings.push({
|
|
638
|
+
level: 'warn',
|
|
639
|
+
file: relPath,
|
|
640
|
+
message: `unrecognized file in singleton — expected index.md or a sibling like {fieldId}.md`
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
// Collections: warn about dotted names that aren't valid siblings
|
|
647
|
+
const dotIdx = name.lastIndexOf('.');
|
|
648
|
+
if (dotIdx > 0) {
|
|
649
|
+
const possibleFieldId = name.slice(dotIdx + 1);
|
|
650
|
+
if (!extraMdFieldIds.has(possibleFieldId)) {
|
|
651
|
+
warnings.push({
|
|
652
|
+
level: 'warn',
|
|
653
|
+
file: relPath,
|
|
654
|
+
message: `unrecognized dotted filename — not a known sibling field. Did you mean "{slug}.md"?`
|
|
655
|
+
});
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const entry = parseContentFile(filePath, subdir);
|
|
662
|
+
|
|
617
663
|
// Check 2: Unknown frontmatter keys
|
|
618
664
|
for (const key of Object.keys(entry.fields)) {
|
|
619
665
|
if (!knownKeys.has(key)) {
|
package/cli/index.ts
CHANGED
|
@@ -248,14 +248,25 @@ async function syncContentToLocalD1(
|
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
-
|
|
251
|
+
// Wipe and rebuild: DELETE all existing entries+assets, then re-insert
|
|
252
|
+
// from content/ files. This eliminates schema drift (stale fields, ghost
|
|
253
|
+
// entries) and ensures the local DB always matches the filesystem.
|
|
254
|
+
const allStatements = [
|
|
255
|
+
'DELETE FROM entries',
|
|
256
|
+
'DELETE FROM assets',
|
|
257
|
+
...assetSql,
|
|
258
|
+
...entrySql
|
|
259
|
+
];
|
|
260
|
+
if (allStatements.length > 2) {
|
|
261
|
+
// Only run if there's actual content to sync (beyond the 2 DELETEs)
|
|
254
262
|
if (s)
|
|
255
263
|
s.message(
|
|
256
264
|
`Syncing ${entrySql.length} entries + ${assetSql.length} assets...`
|
|
257
265
|
);
|
|
258
266
|
await d1ExecuteBatchSqlAsync(root, dbName, '--local', allStatements);
|
|
267
|
+
} else if (allStatements.length === 2) {
|
|
268
|
+
// No content files — still wipe stale data
|
|
269
|
+
await d1ExecuteBatchSqlAsync(root, dbName, '--local', allStatements);
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
// Upload media files to local R2
|
package/cli/scaffold.ts
CHANGED
|
@@ -404,7 +404,9 @@ export function scaffoldContentDirFromTemplate(
|
|
|
404
404
|
// Write sibling example files for extra markdown fields
|
|
405
405
|
if (siblingFiles) {
|
|
406
406
|
for (const { fieldId, content: siblingContent } of siblingFiles) {
|
|
407
|
-
const siblingName =
|
|
407
|
+
const siblingName = ct.singleton
|
|
408
|
+
? `${fieldId}.md`
|
|
409
|
+
: `_example.${fieldId}.md`;
|
|
408
410
|
writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
|
|
409
411
|
}
|
|
410
412
|
}
|
|
@@ -453,7 +455,9 @@ export function scaffoldContentDir(
|
|
|
453
455
|
// Write sibling example files for extra markdown fields
|
|
454
456
|
if (siblingFiles) {
|
|
455
457
|
for (const { fieldId, content: siblingContent } of siblingFiles) {
|
|
456
|
-
const siblingName =
|
|
458
|
+
const siblingName = ct.singleton
|
|
459
|
+
? `${fieldId}.md`
|
|
460
|
+
: `_example.${fieldId}.md`;
|
|
457
461
|
writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
|
|
458
462
|
}
|
|
459
463
|
}
|
|
@@ -596,7 +600,9 @@ export function syncContentDirsWithConfig(
|
|
|
596
600
|
// Write sibling example files for extra markdown fields
|
|
597
601
|
if (siblingFiles) {
|
|
598
602
|
for (const { fieldId, content: siblingContent } of siblingFiles) {
|
|
599
|
-
const siblingName =
|
|
603
|
+
const siblingName = ct.singleton
|
|
604
|
+
? `${fieldId}.md`
|
|
605
|
+
: `_example.${fieldId}.md`;
|
|
600
606
|
writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
|
|
601
607
|
}
|
|
602
608
|
}
|
|
@@ -625,7 +631,9 @@ export function syncContentDirsWithConfig(
|
|
|
625
631
|
// Write sibling example files for extra markdown fields
|
|
626
632
|
if (siblingFiles) {
|
|
627
633
|
for (const { fieldId, content: siblingContent } of siblingFiles) {
|
|
628
|
-
const siblingName =
|
|
634
|
+
const siblingName = ct.singleton
|
|
635
|
+
? `${fieldId}.md`
|
|
636
|
+
: `_example.${fieldId}.md`;
|
|
629
637
|
writeFileSync(resolve(typeDir, siblingName), siblingContent + '\n');
|
|
630
638
|
}
|
|
631
639
|
}
|