@millstone/synapse-site 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 +194 -0
- package/decap/build-config.ts +435 -0
- package/decap/build-vault-index.ts +178 -0
- package/decap/validate-browser.ts +638 -0
- package/package.json +67 -0
- package/plugins/CustomHeaderFooter.ts +387 -0
- package/quartz.config.ts +107 -0
- package/quartz.layout.ts +124 -0
- package/setup-quartz.ts +151 -0
- package/static/edit/body-grammars/index.json +1532 -0
- package/static/edit/config.yml +1393 -0
- package/static/edit/index.html +402 -0
- package/static/edit/schemas/index.json +1477 -0
- package/static/edit/validate.bundle.js +22720 -0
- package/static/edit/vault-index.json +78 -0
- package/sync-plugins.ts +126 -0
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Synapse Documentation Site (Quartz 4)
|
|
2
|
+
|
|
3
|
+
This directory contains configuration files for Quartz 4 static site generator, which is integrated as a git submodule for the Synapse Documentation Framework.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
Quartz 4 is included as a git submodule at `site/quartz/`. This approach:
|
|
8
|
+
- Keeps Quartz updates manageable via git submodule commands
|
|
9
|
+
- Maintains clear separation between Synapse and Quartz code
|
|
10
|
+
- Allows version pinning to specific Quartz releases
|
|
11
|
+
- Enables easy rollback if needed
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
### Prerequisites
|
|
16
|
+
- Node.js v20+ (v22+ preferred)
|
|
17
|
+
- npm v10+
|
|
18
|
+
- Git
|
|
19
|
+
|
|
20
|
+
### Initial Setup
|
|
21
|
+
|
|
22
|
+
From the project root:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Install site dependencies
|
|
26
|
+
cd site
|
|
27
|
+
npm install
|
|
28
|
+
|
|
29
|
+
# Run the setup script (TypeScript)
|
|
30
|
+
npm run setup
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The setup script will:
|
|
34
|
+
1. Initialize the Quartz submodule if not present
|
|
35
|
+
2. Install Quartz dependencies
|
|
36
|
+
3. Copy our configuration files into Quartz
|
|
37
|
+
4. Create a symlink from `quartz/content` to `../../content`
|
|
38
|
+
|
|
39
|
+
### Manual Setup (if needed)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# From project root
|
|
43
|
+
git submodule add -b v4 https://github.com/jackyzha0/quartz.git site/quartz
|
|
44
|
+
cd site/quartz
|
|
45
|
+
npm install
|
|
46
|
+
|
|
47
|
+
# Copy configurations
|
|
48
|
+
cp ../quartz.config.ts .
|
|
49
|
+
cp ../quartz.layout.ts .
|
|
50
|
+
|
|
51
|
+
# Link content
|
|
52
|
+
rm -rf content
|
|
53
|
+
ln -s ../../content content
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
### Start Development Server
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# From site directory
|
|
62
|
+
npm run dev
|
|
63
|
+
|
|
64
|
+
# Or directly from quartz directory
|
|
65
|
+
cd site/quartz
|
|
66
|
+
npx quartz build --serve
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The site will be available at `http://localhost:8080` with hot reload enabled.
|
|
70
|
+
|
|
71
|
+
### Available Scripts
|
|
72
|
+
|
|
73
|
+
From the `site/` directory:
|
|
74
|
+
|
|
75
|
+
- `npm run setup` - Initialize and configure Quartz submodule
|
|
76
|
+
- `npm run build` - Build the static site
|
|
77
|
+
- `npm run serve` - Build and serve locally
|
|
78
|
+
- `npm run dev` - Development server with hot reload
|
|
79
|
+
- `npm run update-submodule` - Update Quartz to latest version
|
|
80
|
+
- `npm run clean` - Clean build artifacts
|
|
81
|
+
|
|
82
|
+
## Configuration Files
|
|
83
|
+
|
|
84
|
+
### `quartz.config.ts`
|
|
85
|
+
Main Quartz configuration with:
|
|
86
|
+
- Site title and metadata
|
|
87
|
+
- Theme and color scheme
|
|
88
|
+
- Plugin configuration
|
|
89
|
+
- Graph, backlinks, and tags enabled
|
|
90
|
+
- Obsidian-flavored markdown support
|
|
91
|
+
|
|
92
|
+
### `quartz.layout.ts`
|
|
93
|
+
Layout configuration defining:
|
|
94
|
+
- Graph component in right sidebar (interactive, zoomable)
|
|
95
|
+
- Backlinks below the graph
|
|
96
|
+
- Tags in article headers
|
|
97
|
+
- Search and explorer in left sidebar
|
|
98
|
+
- Table of contents positioning
|
|
99
|
+
|
|
100
|
+
### `custom.scss` (optional)
|
|
101
|
+
Custom styles for:
|
|
102
|
+
- Document type badges
|
|
103
|
+
- Status indicators
|
|
104
|
+
- Enhanced graph styling
|
|
105
|
+
- Responsive design adjustments
|
|
106
|
+
|
|
107
|
+
## Features Configured
|
|
108
|
+
|
|
109
|
+
✅ **Graph Visualization**
|
|
110
|
+
- Local graph: 2-level depth from current document
|
|
111
|
+
- Global graph: Full documentation network
|
|
112
|
+
- Interactive drag, zoom, and hover
|
|
113
|
+
- Tags shown on nodes
|
|
114
|
+
|
|
115
|
+
✅ **Automatic Backlinks**
|
|
116
|
+
- Shows documents that reference the current page
|
|
117
|
+
- Positioned below the graph in right sidebar
|
|
118
|
+
|
|
119
|
+
✅ **Tag Navigation**
|
|
120
|
+
- Tags displayed in article headers
|
|
121
|
+
- Dedicated tag index pages
|
|
122
|
+
- Tag-based content discovery
|
|
123
|
+
|
|
124
|
+
✅ **Additional Features**
|
|
125
|
+
- Full-text search
|
|
126
|
+
- Dark/light theme toggle
|
|
127
|
+
- Breadcrumb navigation
|
|
128
|
+
- Table of contents
|
|
129
|
+
- Obsidian wikilink support
|
|
130
|
+
|
|
131
|
+
## Building for Production
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Build the static site
|
|
135
|
+
cd site
|
|
136
|
+
npm run build
|
|
137
|
+
|
|
138
|
+
# Output will be in site/quartz/public/
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The `public/` directory can be deployed to any static hosting service:
|
|
142
|
+
- GitHub Pages
|
|
143
|
+
- Netlify
|
|
144
|
+
- Vercel
|
|
145
|
+
- AWS S3 + CloudFront
|
|
146
|
+
- Any web server
|
|
147
|
+
|
|
148
|
+
## Updating Quartz
|
|
149
|
+
|
|
150
|
+
To update to the latest Quartz v4:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
cd site
|
|
154
|
+
npm run update-submodule
|
|
155
|
+
|
|
156
|
+
# Re-run setup to apply our configurations
|
|
157
|
+
npm run setup
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Troubleshooting
|
|
161
|
+
|
|
162
|
+
### Submodule not initialized
|
|
163
|
+
```bash
|
|
164
|
+
git submodule update --init --recursive
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Content not appearing
|
|
168
|
+
- Check symlink: `ls -la site/quartz/content`
|
|
169
|
+
- Should point to `../../content`
|
|
170
|
+
- Verify files have proper YAML frontmatter
|
|
171
|
+
|
|
172
|
+
### Build errors
|
|
173
|
+
- Ensure Node v20+ with `node --version`
|
|
174
|
+
- Run `npm install` in both `site/` and `site/quartz/`
|
|
175
|
+
- Check frontmatter syntax is valid YAML
|
|
176
|
+
|
|
177
|
+
### Permission errors on setup
|
|
178
|
+
```bash
|
|
179
|
+
chmod +x site/setup-quartz.ts
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Integration with Synapse
|
|
183
|
+
|
|
184
|
+
The Quartz site automatically processes all content in the Synapse vault:
|
|
185
|
+
- Documents in numbered folders (10_Policies, 20_Standards, etc.)
|
|
186
|
+
- Templates and schemas (excluded from build)
|
|
187
|
+
- Examples generated from YAML seeds
|
|
188
|
+
- All cross-references via wikilinks
|
|
189
|
+
|
|
190
|
+
The graph visualization will show the relationships between:
|
|
191
|
+
- Processes and their standards
|
|
192
|
+
- SOPs and their parent processes
|
|
193
|
+
- Runbooks and their systems
|
|
194
|
+
- ADRs and their related documents
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Generate Decap CMS config.yml from JSON schemas and body grammars
|
|
4
|
+
*
|
|
5
|
+
* This ensures the CMS config stays in sync with schema definitions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import * as yaml from 'js-yaml';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../../..');
|
|
17
|
+
const SCHEMAS_DIR = path.join(PROJECT_ROOT, 'schemas/frontmatter');
|
|
18
|
+
const GRAMMARS_DIR = path.join(PROJECT_ROOT, 'schemas/body-grammars');
|
|
19
|
+
const OUTPUT_PATH = path.join(__dirname, '../static/edit/config.yml');
|
|
20
|
+
|
|
21
|
+
// Collection configuration - maps doc types to their CMS settings
|
|
22
|
+
const COLLECTIONS: Record<string, {
|
|
23
|
+
name: string;
|
|
24
|
+
label: string;
|
|
25
|
+
labelSingular: string;
|
|
26
|
+
folder: string;
|
|
27
|
+
slug?: string;
|
|
28
|
+
statusOptions?: string[];
|
|
29
|
+
}> = {
|
|
30
|
+
policy: {
|
|
31
|
+
name: 'policies',
|
|
32
|
+
label: '📜 Policies',
|
|
33
|
+
labelSingular: 'Policy',
|
|
34
|
+
folder: 'content/10_Policies',
|
|
35
|
+
},
|
|
36
|
+
standard: {
|
|
37
|
+
name: 'standards',
|
|
38
|
+
label: '📐 Standards',
|
|
39
|
+
labelSingular: 'Standard',
|
|
40
|
+
folder: 'content/20_Standards',
|
|
41
|
+
},
|
|
42
|
+
process: {
|
|
43
|
+
name: 'processes',
|
|
44
|
+
label: '🔄 Processes',
|
|
45
|
+
labelSingular: 'Process',
|
|
46
|
+
folder: 'content/30_Processes',
|
|
47
|
+
},
|
|
48
|
+
sop: {
|
|
49
|
+
name: 'sops',
|
|
50
|
+
label: '📋 SOPs',
|
|
51
|
+
labelSingular: 'SOP',
|
|
52
|
+
folder: 'content/40_SOPs',
|
|
53
|
+
},
|
|
54
|
+
runbook: {
|
|
55
|
+
name: 'runbooks',
|
|
56
|
+
label: '🔧 Runbooks',
|
|
57
|
+
labelSingular: 'Runbook',
|
|
58
|
+
folder: 'content/50_Runbooks',
|
|
59
|
+
},
|
|
60
|
+
meeting: {
|
|
61
|
+
name: 'meetings',
|
|
62
|
+
label: '📅 Meetings',
|
|
63
|
+
labelSingular: 'Meeting',
|
|
64
|
+
folder: 'content/60_Meetings',
|
|
65
|
+
slug: '{{year}}-{{month}}-{{day}}-{{slug}}',
|
|
66
|
+
},
|
|
67
|
+
system: {
|
|
68
|
+
name: 'systems',
|
|
69
|
+
label: '🖥️ Systems',
|
|
70
|
+
labelSingular: 'System',
|
|
71
|
+
folder: 'content/75_Systems',
|
|
72
|
+
},
|
|
73
|
+
scorecard: {
|
|
74
|
+
name: 'scorecards',
|
|
75
|
+
label: '📊 Scorecards',
|
|
76
|
+
labelSingular: 'Scorecard',
|
|
77
|
+
folder: 'content/80_Scorecards',
|
|
78
|
+
},
|
|
79
|
+
adr: {
|
|
80
|
+
name: 'adrs',
|
|
81
|
+
label: '🏗️ ADRs',
|
|
82
|
+
labelSingular: 'ADR',
|
|
83
|
+
folder: 'content/90_Architecture/ADRs',
|
|
84
|
+
slug: 'ADR-{{fields.adr_number}}-{{slug}}',
|
|
85
|
+
statusOptions: ['proposed', 'accepted', 'deprecated', 'superseded'],
|
|
86
|
+
},
|
|
87
|
+
tdd: {
|
|
88
|
+
name: 'tdds',
|
|
89
|
+
label: '📐 TDDs',
|
|
90
|
+
labelSingular: 'TDD',
|
|
91
|
+
folder: 'content/90_Architecture/TDDs',
|
|
92
|
+
},
|
|
93
|
+
prd: {
|
|
94
|
+
name: 'prds',
|
|
95
|
+
label: '📦 PRDs',
|
|
96
|
+
labelSingular: 'PRD',
|
|
97
|
+
folder: 'content/100_Products/PRDs',
|
|
98
|
+
},
|
|
99
|
+
capability: {
|
|
100
|
+
name: 'capabilities',
|
|
101
|
+
label: '🎯 Capabilities',
|
|
102
|
+
labelSingular: 'Capability',
|
|
103
|
+
folder: 'content/110_Capabilities',
|
|
104
|
+
},
|
|
105
|
+
reference: {
|
|
106
|
+
name: 'references',
|
|
107
|
+
label: '📚 References',
|
|
108
|
+
labelSingular: 'Reference',
|
|
109
|
+
folder: 'content/200_References',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
interface SchemaProperty {
|
|
114
|
+
type?: string;
|
|
115
|
+
enum?: string[];
|
|
116
|
+
const?: string;
|
|
117
|
+
format?: string;
|
|
118
|
+
items?: { type: string };
|
|
119
|
+
minLength?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface Schema {
|
|
123
|
+
properties: Record<string, SchemaProperty>;
|
|
124
|
+
required?: string[];
|
|
125
|
+
allOf?: Array<{ $ref: string }>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface BodyGrammarSection {
|
|
129
|
+
id: string;
|
|
130
|
+
title: string;
|
|
131
|
+
required: boolean;
|
|
132
|
+
order: number;
|
|
133
|
+
shape: {
|
|
134
|
+
type: string;
|
|
135
|
+
ordered?: boolean;
|
|
136
|
+
minItems?: number;
|
|
137
|
+
allowedNodes?: string[];
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface BodyGrammar {
|
|
142
|
+
type: string;
|
|
143
|
+
displayName: string;
|
|
144
|
+
sections: BodyGrammarSection[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface DecapField {
|
|
148
|
+
label: string;
|
|
149
|
+
name: string;
|
|
150
|
+
widget: string;
|
|
151
|
+
required?: boolean;
|
|
152
|
+
default?: string | boolean;
|
|
153
|
+
hint?: string;
|
|
154
|
+
options?: string[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Map JSON schema types to Decap widget types
|
|
158
|
+
function schemaTypeToWidget(prop: SchemaProperty, name: string): string {
|
|
159
|
+
if (prop.enum) return 'select';
|
|
160
|
+
if (prop.const) return 'hidden';
|
|
161
|
+
if (prop.format === 'date-time') return 'datetime';
|
|
162
|
+
if (prop.type === 'array') return 'list';
|
|
163
|
+
if (prop.type === 'boolean') return 'boolean';
|
|
164
|
+
if (name === 'summary') return 'text';
|
|
165
|
+
return 'string';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Generate placeholder content based on section shape
|
|
169
|
+
function generateSectionPlaceholder(section: BodyGrammarSection): string {
|
|
170
|
+
const shape = section.shape;
|
|
171
|
+
|
|
172
|
+
if (shape.type === 'list') {
|
|
173
|
+
if (shape.ordered) {
|
|
174
|
+
return '1. First item\n2. Second item';
|
|
175
|
+
}
|
|
176
|
+
return '- Item one\n- Item two';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (shape.type === 'table') {
|
|
180
|
+
return '| Column 1 | Column 2 |\n|----------|----------|\n| Data | Data |';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return '[Describe here...]';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Generate default body template from body grammar
|
|
187
|
+
function generateDefaultBody(grammar: BodyGrammar): string {
|
|
188
|
+
if (!grammar || !grammar.sections) return '';
|
|
189
|
+
|
|
190
|
+
const sections = grammar.sections
|
|
191
|
+
.filter(s => s.required)
|
|
192
|
+
.sort((a, b) => a.order - b.order);
|
|
193
|
+
|
|
194
|
+
return sections.map(section => {
|
|
195
|
+
const placeholder = generateSectionPlaceholder(section);
|
|
196
|
+
return `## ${section.title}\n\n${placeholder}`;
|
|
197
|
+
}).join('\n\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Generate hint showing required sections
|
|
201
|
+
function generateBodyHint(grammar: BodyGrammar): string {
|
|
202
|
+
if (!grammar || !grammar.sections) return '';
|
|
203
|
+
|
|
204
|
+
const required = grammar.sections
|
|
205
|
+
.filter(s => s.required)
|
|
206
|
+
.sort((a, b) => a.order - b.order)
|
|
207
|
+
.map(s => {
|
|
208
|
+
let hint = s.title;
|
|
209
|
+
if (s.shape.type === 'list') {
|
|
210
|
+
hint += s.shape.ordered ? ' (numbered list)' : ' (bullet list)';
|
|
211
|
+
}
|
|
212
|
+
return hint;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (required.length === 0) return '';
|
|
216
|
+
|
|
217
|
+
return `Required sections: ${required.join(', ')}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Load and merge schema with base
|
|
221
|
+
function loadSchema(type: string): Schema {
|
|
222
|
+
const schemaPath = path.join(SCHEMAS_DIR, `${type}.schema.json`);
|
|
223
|
+
const basePath = path.join(SCHEMAS_DIR, 'base.schema.json');
|
|
224
|
+
|
|
225
|
+
const baseSchema: Schema = JSON.parse(fs.readFileSync(basePath, 'utf-8'));
|
|
226
|
+
|
|
227
|
+
if (!fs.existsSync(schemaPath)) {
|
|
228
|
+
return baseSchema;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const typeSchema: Schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
232
|
+
|
|
233
|
+
// Merge properties
|
|
234
|
+
return {
|
|
235
|
+
properties: {
|
|
236
|
+
...baseSchema.properties,
|
|
237
|
+
...typeSchema.properties,
|
|
238
|
+
},
|
|
239
|
+
required: [
|
|
240
|
+
...(baseSchema.required || []),
|
|
241
|
+
...(typeSchema.required || []),
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Load body grammar
|
|
247
|
+
function loadBodyGrammar(type: string): BodyGrammar | null {
|
|
248
|
+
const grammarPath = path.join(GRAMMARS_DIR, `${type}.body-grammar.json`);
|
|
249
|
+
|
|
250
|
+
if (!fs.existsSync(grammarPath)) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return JSON.parse(fs.readFileSync(grammarPath, 'utf-8'));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Generate Decap fields from schema
|
|
258
|
+
function generateFields(
|
|
259
|
+
type: string,
|
|
260
|
+
schema: Schema,
|
|
261
|
+
grammar: BodyGrammar | null,
|
|
262
|
+
collectionConfig: typeof COLLECTIONS[string]
|
|
263
|
+
): DecapField[] {
|
|
264
|
+
const fields: DecapField[] = [];
|
|
265
|
+
const required = new Set(schema.required || []);
|
|
266
|
+
|
|
267
|
+
// Define field order (important fields first)
|
|
268
|
+
const fieldOrder = [
|
|
269
|
+
'adr_number', // ADR-specific
|
|
270
|
+
'id',
|
|
271
|
+
'title',
|
|
272
|
+
'type',
|
|
273
|
+
'status',
|
|
274
|
+
'owner',
|
|
275
|
+
'created',
|
|
276
|
+
'updated',
|
|
277
|
+
'tags',
|
|
278
|
+
'summary',
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
// Get all property names and sort by order
|
|
282
|
+
const propNames = Object.keys(schema.properties);
|
|
283
|
+
const sortedProps = [
|
|
284
|
+
...fieldOrder.filter(f => propNames.includes(f)),
|
|
285
|
+
...propNames.filter(f => !fieldOrder.includes(f) && f !== 'example'),
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
for (const name of sortedProps) {
|
|
289
|
+
const prop = schema.properties[name];
|
|
290
|
+
if (!prop) continue;
|
|
291
|
+
|
|
292
|
+
// Skip 'example' field - internal use only
|
|
293
|
+
if (name === 'example') continue;
|
|
294
|
+
|
|
295
|
+
const widget = schemaTypeToWidget(prop, name);
|
|
296
|
+
const field: DecapField = {
|
|
297
|
+
label: formatLabel(name),
|
|
298
|
+
name,
|
|
299
|
+
widget,
|
|
300
|
+
required: required.has(name),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Handle special cases
|
|
304
|
+
if (widget === 'hidden') {
|
|
305
|
+
field.default = prop.const || type;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (widget === 'select' && prop.enum) {
|
|
309
|
+
// Use collection-specific status options if defined
|
|
310
|
+
if (name === 'status' && collectionConfig.statusOptions) {
|
|
311
|
+
field.options = collectionConfig.statusOptions;
|
|
312
|
+
field.default = collectionConfig.statusOptions[0];
|
|
313
|
+
} else {
|
|
314
|
+
field.options = prop.enum;
|
|
315
|
+
field.default = prop.enum[0];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fields.push(field);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Add body field with hint and default
|
|
323
|
+
// Raw mode only - the preview pane provides visual feedback
|
|
324
|
+
// and raw mode is more predictable for structured docs with required sections
|
|
325
|
+
const bodyField: Record<string, any> = {
|
|
326
|
+
label: 'Body',
|
|
327
|
+
name: 'body',
|
|
328
|
+
widget: 'markdown',
|
|
329
|
+
required: true,
|
|
330
|
+
modes: ['raw'],
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
if (grammar) {
|
|
334
|
+
const hint = generateBodyHint(grammar);
|
|
335
|
+
if (hint) {
|
|
336
|
+
bodyField.hint = hint;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const defaultBody = generateDefaultBody(grammar);
|
|
340
|
+
if (defaultBody) {
|
|
341
|
+
bodyField.default = defaultBody;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
fields.push(bodyField);
|
|
346
|
+
|
|
347
|
+
return fields;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Format field name as label
|
|
351
|
+
function formatLabel(name: string): string {
|
|
352
|
+
return name
|
|
353
|
+
.split('_')
|
|
354
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
355
|
+
.join(' ');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Generate full config
|
|
359
|
+
function generateConfig(): object {
|
|
360
|
+
const collections = [];
|
|
361
|
+
|
|
362
|
+
for (const [type, config] of Object.entries(COLLECTIONS)) {
|
|
363
|
+
const schema = loadSchema(type);
|
|
364
|
+
const grammar = loadBodyGrammar(type);
|
|
365
|
+
const fields = generateFields(type, schema, grammar, config);
|
|
366
|
+
|
|
367
|
+
const collection: Record<string, any> = {
|
|
368
|
+
name: config.name,
|
|
369
|
+
label: config.label,
|
|
370
|
+
label_singular: config.labelSingular,
|
|
371
|
+
folder: config.folder,
|
|
372
|
+
create: true,
|
|
373
|
+
nested: { depth: 10 },
|
|
374
|
+
slug: config.slug || '{{slug}}',
|
|
375
|
+
extension: 'md',
|
|
376
|
+
format: 'yaml-frontmatter',
|
|
377
|
+
fields,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
collections.push(collection);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
backend: {
|
|
385
|
+
name: process.env.DECAP_BACKEND || 'github',
|
|
386
|
+
repo: process.env.DECAP_REPO || 'OWNER/REPO',
|
|
387
|
+
branch: process.env.DECAP_BRANCH || 'main',
|
|
388
|
+
auth_type: 'pkce',
|
|
389
|
+
app_id: process.env.DECAP_APP_ID || '',
|
|
390
|
+
api_root: process.env.DECAP_API_ROOT || 'https://api.github.com',
|
|
391
|
+
base_url: process.env.DECAP_BASE_URL || 'https://github.com',
|
|
392
|
+
},
|
|
393
|
+
media_folder: 'content/_assets',
|
|
394
|
+
public_folder: '/_assets',
|
|
395
|
+
local_backend: true,
|
|
396
|
+
site_url: '/',
|
|
397
|
+
display_url: '/',
|
|
398
|
+
logo_url: '/static/logo.png',
|
|
399
|
+
collections,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Main
|
|
404
|
+
function main() {
|
|
405
|
+
console.log('Generating Decap CMS config from schemas...');
|
|
406
|
+
|
|
407
|
+
const config = generateConfig();
|
|
408
|
+
|
|
409
|
+
// Convert to YAML
|
|
410
|
+
const yamlContent = yaml.dump(config, {
|
|
411
|
+
indent: 2,
|
|
412
|
+
lineWidth: 120,
|
|
413
|
+
noRefs: true,
|
|
414
|
+
quotingType: '"',
|
|
415
|
+
forceQuotes: false,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Add header comment
|
|
419
|
+
const output = `# Decap CMS Configuration - AUTO-GENERATED
|
|
420
|
+
# DO NOT EDIT MANUALLY - Run 'npm run build:decap-config' to regenerate
|
|
421
|
+
# Source: schemas/frontmatter/*.schema.json, schemas/body-grammars/*.body-grammar.json
|
|
422
|
+
|
|
423
|
+
${yamlContent}`;
|
|
424
|
+
|
|
425
|
+
// Ensure output directory exists
|
|
426
|
+
const outputDir = path.dirname(OUTPUT_PATH);
|
|
427
|
+
if (!fs.existsSync(outputDir)) {
|
|
428
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fs.writeFileSync(OUTPUT_PATH, output);
|
|
432
|
+
console.log(`✓ Generated config.yml with ${Object.keys(COLLECTIONS).length} collections`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
main();
|