feat: add json schema da42a8c8
Willow (GHOST) · 2026-03-06 05:23 6 file(s) · +247 −23
docs/docs/pages/config.mdx +3 −22
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
docs/sequoia.json +1 −0
1 1
{
2 +
	"$schema": "../sequoia.schema.json",
2 3
	"siteUrl": "https://sequoia.pub",
3 4
	"contentDir": "docs/pages/blog",
4 5
	"imagesDir": "docs/public",
docs/src/lib/ConfigTable.tsx (added) +88 −0
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 +
}
packages/cli/package.json +2 −1
7 7
	},
8 8
	"files": [
9 9
		"dist",
10 -
		"README.md"
10 +
		"README.md",
11 +
		"sequoia.schema.json"
11 12
	],
12 13
	"main": "./dist/index.js",
13 14
	"exports": {
packages/cli/src/lib/config.ts +1 −0
88 88
	bluesky?: BlueskyConfig;
89 89
}): string {
90 90
	const config: Record<string, unknown> = {
91 +
		$schema: 'https://tangled.org/stevedylan.dev/sequoia/raw/main/sequoia.schema.json',
91 92
		siteUrl: options.siteUrl,
92 93
		contentDir: options.contentDir,
93 94
	};
sequoia.schema.json (added) +152 −0
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 +
}