A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing

feat: add json schema #39

merged opened by willow.sh targeting main from willow.sh/sequoia: schema

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.

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:dfkjiu36xs6ogt7pux7i7o2b/sh.tangled.repo.pull/3mgel7kx5mp22
+247 -23
Diff #3
+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 1 { 2 + "$schema": "../sequoia.schema.json", 2 3 "siteUrl": "https://sequoia.pub", 3 4 "contentDir": "docs/pages/blog", 4 5 "imagesDir": "docs/public",
+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
··· 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": {
+1
packages/cli/src/lib/config.ts
··· 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 };
+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
sign up or login to add to the discussion
1 commit
expand
feat: add json schema
expand 1 comment

This is fantastic!! Thank you!! ๐Ÿ™๐Ÿป

pull request successfully merged
1 commit
expand
feat: add json schema
expand 0 comments
1 commit
expand
feat: add json schema
expand 0 comments
1 commit
expand
feat: add json schema
expand 0 comments