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

feat: add json schema

authored by willow.sh and committed by tangled.org da42a8c8 ec148ac7

+247 -23
+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 + }