Add a json schema for the sequoia config file, and use it to generate the documentation config table. If you're interested then I'm also happy to open another PR to port the config logic to valibot or similar, which can then generate the json schema, so you should never run into a skew issue again.
+247
-23
Diff
round #3
+3
-22
docs/docs/pages/config.mdx
+3
-22
docs/docs/pages/config.mdx
···
1
+
import ConfigTable from '../../src/lib/ConfigTable.tsx'
2
+
1
3
# Configuration Reference
2
4
3
5
## `sequoia.json`
4
6
5
-
| Field | Type | Required | Default | Description |
6
-
|-------|------|----------|---------|-------------|
7
-
| `siteUrl` | `string` | Yes | - | Base URL of your website |
8
-
| `contentDir` | `string` | Yes | - | Directory containing blog post files |
9
-
| `publicationUri` | `string` | Yes | - | AT-URI of your publication record |
10
-
| `imagesDir` | `string` | No | - | Directory containing cover images |
11
-
| `publicDir` | `string` | No | `"./public"` | Static folder for `.well-known` files |
12
-
| `outputDir` | `string` | No | - | Built output directory for inject command |
13
-
| `pathPrefix` | `string` | No | `"/posts"` | URL path prefix for posts |
14
-
| `pdsUrl` | `string` | No | `"https://bsky.social"` | PDS server URL, generated automatically |
15
-
| `identity` | `string` | No | - | Which stored identity to use |
16
-
| `frontmatter` | `object` | No | - | Custom frontmatter field mappings |
17
-
| `frontmatter.slugField` | `string` | No | - | Frontmatter field to use for slug (defaults to filepath) |
18
-
| `ignore` | `string[]` | No | - | Glob patterns for files to ignore |
19
-
| `removeIndexFromSlug` | `boolean` | No | `false` | Remove `/index` or `/_index` suffix from slugs |
20
-
| `stripDatePrefix` | `boolean` | No | `false` | Remove `YYYY-MM-DD-` date prefixes from slugs (Jekyll-style) |
21
-
| `pathTemplate` | `string` | No | - | URL path template with tokens (overrides `pathPrefix` + slug) |
22
-
| `bluesky` | `object` | No | - | Bluesky posting configuration |
23
-
| `bluesky.enabled` | `boolean` | No | `false` | Post to Bluesky when publishing documents (also enables [comments](/comments)) |
24
-
| `bluesky.maxAgeDays` | `number` | No | `30` | Only post documents published within this many days |
25
-
| `ui` | `object` | No | - | UI components configuration |
26
-
| `ui.components` | `string` | No | `"src/components"` | Directory where UI components are installed |
7
+
<ConfigTable />
27
8
28
9
### Example
29
10
+1
docs/sequoia.json
+1
docs/sequoia.json
+88
docs/src/lib/ConfigTable.tsx
+88
docs/src/lib/ConfigTable.tsx
···
1
+
import schema from "../../../sequoia.schema.json" with { type: "json" };
2
+
3
+
type PropertyInfo = {
4
+
path: string;
5
+
type: string;
6
+
required: boolean;
7
+
default?: string | number | boolean;
8
+
description?: string;
9
+
};
10
+
11
+
function extractProperties(
12
+
properties: Record<string, unknown>,
13
+
required: string[],
14
+
parentPath: string,
15
+
result: PropertyInfo[],
16
+
): void {
17
+
for (const [key, value] of Object.entries(properties)) {
18
+
const prop = value as Record<string, unknown>;
19
+
const fullPath = parentPath ? `${parentPath}.${key}` : key;
20
+
const isRequired = required.includes(key);
21
+
22
+
if (prop.properties) {
23
+
extractProperties(
24
+
prop.properties as Record<string, unknown>,
25
+
(prop.required as string[]) || [],
26
+
fullPath,
27
+
result,
28
+
);
29
+
} else {
30
+
result.push({
31
+
path: fullPath,
32
+
type: prop.type,
33
+
required: isRequired,
34
+
default: prop.default,
35
+
description: prop.description,
36
+
} as PropertyInfo);
37
+
}
38
+
}
39
+
}
40
+
41
+
export default function ConfigTable() {
42
+
const rows: PropertyInfo[] = [];
43
+
extractProperties(
44
+
schema.properties as Record<string, unknown>,
45
+
schema.required as string[],
46
+
"",
47
+
rows,
48
+
);
49
+
50
+
return (
51
+
<table className="vocs_Table">
52
+
<thead>
53
+
<tr className="vocs_TableRow">
54
+
<th className="vocs_TableHeader">Field</th>
55
+
<th className="vocs_TableHeader">Type</th>
56
+
<th className="vocs_TableHeader">Required</th>
57
+
<th className="vocs_TableHeader">Default</th>
58
+
<th className="vocs_TableHeader">Description</th>
59
+
</tr>
60
+
</thead>
61
+
<tbody>
62
+
{rows.map((row) => (
63
+
<tr key={row.path} className="vocs_TableRow">
64
+
<td className="vocs_TableCell">
65
+
<code className="vocs_Code">{row.path}</code>
66
+
</td>
67
+
<td className="vocs_TableCell">
68
+
<code className="vocs_Code">{row.type}</code>
69
+
</td>
70
+
<td className="vocs_TableCell">{row.required ? "Yes" : ""}</td>
71
+
<td className="vocs_TableCell">
72
+
{row.default === undefined ? (
73
+
"-"
74
+
) : (
75
+
<code className="vocs_Code">
76
+
{typeof row.default === "string"
77
+
? `"${row.default}"`
78
+
: `${row.default}`}
79
+
</code>
80
+
)}
81
+
</td>
82
+
<td className="vocs_TableCell">{row.description || "โ"}</td>
83
+
</tr>
84
+
))}
85
+
</tbody>
86
+
</table>
87
+
);
88
+
}
+2
-1
packages/cli/package.json
+2
-1
packages/cli/package.json
+1
packages/cli/src/lib/config.ts
+1
packages/cli/src/lib/config.ts
+152
sequoia.schema.json
+152
sequoia.schema.json
···
1
+
{
2
+
"$schema": "http://json-schema.org/draft-07/schema#",
3
+
"title": "PublisherConfig",
4
+
"type": "object",
5
+
"additionalProperties": false,
6
+
"required": ["siteUrl", "contentDir", "publicationUri"],
7
+
"properties": {
8
+
"$schema": {
9
+
"type": "string",
10
+
"description": "JSON schema hint"
11
+
},
12
+
"siteUrl": {
13
+
"type": "string",
14
+
"format": "uri",
15
+
"description": "Base site URL"
16
+
},
17
+
"contentDir": {
18
+
"type": "string",
19
+
"description": "Directory containing content"
20
+
},
21
+
"imagesDir": {
22
+
"type": "string",
23
+
"description": "Directory containing cover images"
24
+
},
25
+
"publicDir": {
26
+
"type": "string",
27
+
"description": "Static/public folder for `.well-known` files",
28
+
"default": "public"
29
+
},
30
+
"outputDir": {
31
+
"type": "string",
32
+
"description": "Built output directory for inject command"
33
+
},
34
+
"pathPrefix": {
35
+
"type": "string",
36
+
"description": "URL path prefix for posts",
37
+
"default": "/posts"
38
+
},
39
+
"publicationUri": {
40
+
"type": "string",
41
+
"description": "Publication URI"
42
+
},
43
+
"pdsUrl": {
44
+
"type": "string",
45
+
"format": "uri",
46
+
"description": "Personal data server URL (PDS)",
47
+
"default": "https://bsky.social"
48
+
},
49
+
"identity": {
50
+
"type": "string",
51
+
"description": "Which stored identity to use (matches identifier)"
52
+
},
53
+
"frontmatter": {
54
+
"type": "object",
55
+
"additionalProperties": false,
56
+
"description": "Custom frontmatter field mappings",
57
+
"properties": {
58
+
"title": {
59
+
"type": "string",
60
+
"description": "Field name for title",
61
+
"default": "title"
62
+
},
63
+
"description": {
64
+
"type": "string",
65
+
"description": "Field name for description",
66
+
"default": "description"
67
+
},
68
+
"publishDate": {
69
+
"type": "string",
70
+
"description": "Field name for publish date (checks \"publishDate\", \"pubDate\", \"date\", \"createdAt\", and \"created_at\" by default)",
71
+
"default": "publishDate"
72
+
},
73
+
"coverImage": {
74
+
"type": "string",
75
+
"description": "Field name for cover image",
76
+
"default": "ogImage"
77
+
},
78
+
"tags": {
79
+
"type": "string",
80
+
"description": "Field name for tags",
81
+
"default": "tags"
82
+
},
83
+
"draft": {
84
+
"type": "string",
85
+
"description": "Field name for draft status",
86
+
"default": "draft"
87
+
},
88
+
"slugField": {
89
+
"type": "string",
90
+
"description": "Frontmatter field to use for slug (if set, uses frontmatter value; otherwise uses filepath)"
91
+
}
92
+
}
93
+
},
94
+
"ignore": {
95
+
"type": "array",
96
+
"description": "Glob patterns for files to ignore",
97
+
"items": {
98
+
"type": "string"
99
+
}
100
+
},
101
+
"removeIndexFromSlug": {
102
+
"type": "boolean",
103
+
"description": "Remove \"/index\" or \"/_index\" suffix from paths",
104
+
"default": false
105
+
},
106
+
"stripDatePrefix": {
107
+
"type": "boolean",
108
+
"description": "Remove YYYY-MM-DD- prefix from filenames (Jekyll-style)",
109
+
"default": false
110
+
},
111
+
"pathTemplate": {
112
+
"type": "string",
113
+
"description": "URL path template with tokens like {year}/{month}/{day}/{slug} (overrides pathPrefix + slug)"
114
+
},
115
+
"textContentField": {
116
+
"type": "string",
117
+
"description": "Frontmatter field to use for textContent instead of markdown body"
118
+
},
119
+
"bluesky": {
120
+
"type": "object",
121
+
"additionalProperties": false,
122
+
"description": "Optional Bluesky posting configuration",
123
+
"required": ["enabled"],
124
+
"properties": {
125
+
"enabled": {
126
+
"type": "boolean",
127
+
"description": "Whether Bluesky posting is enabled",
128
+
"default": false
129
+
},
130
+
"maxAgeDays": {
131
+
"type": "integer",
132
+
"minimum": 0,
133
+
"description": "Only post if published within N days",
134
+
"default": 7
135
+
}
136
+
}
137
+
},
138
+
"ui": {
139
+
"type": "object",
140
+
"additionalProperties": false,
141
+
"description": "Optional UI components configuration",
142
+
"properties": {
143
+
"components": {
144
+
"type": "string",
145
+
"description": "Directory to install UI components",
146
+
"default": "src/components"
147
+
}
148
+
},
149
+
"required": ["components"]
150
+
}
151
+
}
152
+
}
History
4 rounds
1 comment
expand 1 comment
pull request successfully merged
This is fantastic!! Thank you!! ๐๐ป