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

feat: add option to disable publishing (text) content #40

merged opened by willow.sh targeting main from willow.sh/sequoia: disable-text-content

Adds an option to disable publishing the text content since it may not be desired - defaults to true since that's the current behavior.

PR is dependent on #39 because of the json schema changes, so maybe best to look at that first

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:dfkjiu36xs6ogt7pux7i7o2b/sh.tangled.repo.pull/3mgemcc6tsb22
+284 -32
Diff #0
+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": {
+5
packages/cli/src/commands/init.ts
··· 98 98 message: "URL path prefix for posts:", 99 99 placeholder: "/posts, /blog, /articles, etc.", 100 100 }), 101 + publishContent: () => confirm({ 102 + message: 'Publish the post content on the standard.site document?', 103 + initialValue: true 104 + }) 101 105 }, 102 106 { onCancel }, 103 107 ); ··· 341 345 pdsUrl, 342 346 frontmatter: frontmatterMapping, 343 347 bluesky: blueskyConfig, 348 + publishContent: siteConfig.publishContent 344 349 }); 345 350 346 351 const configPath = path.join(process.cwd(), "sequoia.json");
+9
packages/cli/src/commands/update.ts
··· 162 162 stripDatePrefix: configUpdated.stripDatePrefix, 163 163 pathTemplate: configUpdated.pathTemplate, 164 164 textContentField: configUpdated.textContentField, 165 + publishContent: configUpdated.publishContent, 165 166 bluesky: configUpdated.bluesky, 166 167 }); 167 168 ··· 373 374 }), 374 375 ); 375 376 377 + const publishContent = exitOnCancel( 378 + await confirm({ 379 + message: 'Publish the post content on the standard.site document?', 380 + initialValue: config.publishContent ?? true 381 + }) 382 + ) 383 + 376 384 const textContentField = exitOnCancel( 377 385 await text({ 378 386 message: ··· 397 405 removeIndexFromSlug: removeIndexFromSlug || undefined, 398 406 stripDatePrefix: stripDatePrefix || undefined, 399 407 textContentField: textContentField || undefined, 408 + publishContent: publishContent ?? true 400 409 }; 401 410 } 402 411
+11 -9
packages/cli/src/lib/atproto.ts
··· 252 252 ); 253 253 const publishDate = new Date(post.frontmatter.publishDate); 254 254 255 - // Determine textContent: use configured field from frontmatter, or fallback to markdown body 256 - let textContent: string; 255 + // Determine textContent (if enabled): use configured field from frontmatter, or fallback to markdown body 256 + let textContent: string | null = null; 257 257 if ( 258 + config.publishContent && 258 259 config.textContentField && 259 260 post.rawFrontmatter?.[config.textContentField] 260 261 ) { 261 262 textContent = String(post.rawFrontmatter[config.textContentField]); 262 - } else { 263 + } else if (config.publishContent) { 263 264 textContent = stripMarkdownForText(post.content); 264 265 } 265 266 ··· 268 269 title: post.frontmatter.title, 269 270 site: config.publicationUri, 270 271 path: postPath, 271 - textContent: textContent.slice(0, 10000), 272 + textContent: textContent?.slice(0, 10000), 272 273 publishedAt: publishDate.toISOString(), 273 274 canonicalUrl: `${config.siteUrl}${postPath}`, 274 275 }; ··· 317 318 ); 318 319 const publishDate = new Date(post.frontmatter.publishDate); 319 320 320 - // Determine textContent: use configured field from frontmatter, or fallback to markdown body 321 - let textContent: string; 321 + // Determine textContent (if enabled): use configured field from frontmatter, or fallback to markdown body 322 + let textContent: string | null = null; 322 323 if ( 324 + config.publishContent && 323 325 config.textContentField && 324 326 post.rawFrontmatter?.[config.textContentField] 325 327 ) { 326 328 textContent = String(post.rawFrontmatter[config.textContentField]); 327 - } else { 329 + } else if (config.publishContent) { 328 330 textContent = stripMarkdownForText(post.content); 329 331 } 330 332 ··· 342 344 title: post.frontmatter.title, 343 345 site: config.publicationUri, 344 346 path: postPath, 345 - textContent: textContent.slice(0, 10000), 347 + textContent: textContent?.slice(0, 10000), 346 348 publishedAt: publishDate.toISOString(), 347 349 canonicalUrl: `${config.siteUrl}${postPath}`, 348 350 }; ··· 384 386 title: string; 385 387 site: string; 386 388 path: string; 387 - textContent: string; 389 + textContent?: string; 388 390 publishedAt: string; 389 391 canonicalUrl?: string; 390 392 description?: string;
+7
packages/cli/src/lib/config.ts
··· 85 85 stripDatePrefix?: boolean; 86 86 pathTemplate?: string; 87 87 textContentField?: string; 88 + publishContent?: boolean; 88 89 bluesky?: BlueskyConfig; 89 90 }): string { 90 91 const config: Record<string, unknown> = { 92 + $schema: 'https://tangled.org/stevedylan.dev/sequoia/raw/main/sequoia.schema.json', 91 93 siteUrl: options.siteUrl, 92 94 contentDir: options.contentDir, 93 95 }; ··· 137 139 if (options.textContentField) { 138 140 config.textContentField = options.textContentField; 139 141 } 142 + 143 + if (options.publishContent) { 144 + config.publishContent = options.publishContent 145 + } 146 + 140 147 if (options.bluesky) { 141 148 config.bluesky = options.bluesky; 142 149 }
+1
packages/cli/src/lib/types.ts
··· 41 41 stripDatePrefix?: boolean; // Remove YYYY-MM-DD- prefix from filenames (Jekyll-style, default: false) 42 42 pathTemplate?: string; // URL path template with tokens like {year}/{month}/{day}/{slug} (overrides pathPrefix + slug) 43 43 textContentField?: string; // Frontmatter field to use for textContent instead of markdown body 44 + publishContent?: boolean; // Whether or not to publish the documents content on the standard.site document (default: true) 44 45 bluesky?: BlueskyConfig; // Optional Bluesky posting configuration 45 46 ui?: UIConfig; // Optional UI components configuration 46 47 }
+157
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 + "publishContent": { 120 + "type": "boolean", 121 + "description": "Whether or not to publish the documents content on the standard.site document", 122 + "default": true 123 + }, 124 + "bluesky": { 125 + "type": "object", 126 + "additionalProperties": false, 127 + "description": "Optional Bluesky posting configuration", 128 + "required": ["enabled"], 129 + "properties": { 130 + "enabled": { 131 + "type": "boolean", 132 + "description": "Whether Bluesky posting is enabled", 133 + "default": false 134 + }, 135 + "maxAgeDays": { 136 + "type": "integer", 137 + "minimum": 0, 138 + "description": "Only post if published within N days", 139 + "default": 7 140 + } 141 + } 142 + }, 143 + "ui": { 144 + "type": "object", 145 + "additionalProperties": false, 146 + "description": "Optional UI components configuration", 147 + "properties": { 148 + "components": { 149 + "type": "string", 150 + "description": "Directory to install UI components", 151 + "default": "src/components" 152 + } 153 + }, 154 + "required": ["components"] 155 + } 156 + } 157 + }

History

3 rounds 5 comments
sign up or login to add to the discussion
1 commit
expand
feat: add option to disable publishing (text) content
expand 2 comments

@stevedylan.dev merged conflicts fixed - the only issue I can foresee is that the diff checking system when you publish isn't aware of this field. We could fix that or I could open a PR that makes the diff system "simpler" by essentially building the site.standard.document for each post and then comparing that with the published site.standard.document for the post to see if there is a difference. That way it should catch any changes that it's currently missing. WDYT?

Good catch! I think a separate PR with your solution would work great. Will go ahead and merge this!

pull request successfully merged
2 commits
expand
feat: add json schema
feat: add option to disable publishing (text) content
expand 1 comment

I’ve had the in the back of my head; thank you for carrying it out! For some reason Tangled is saying there is some merge conflicts due to the last PR, so happy to merge once it gets cleared up!

willow.sh submitted #0
2 commits
expand
feat: add json schema
feat: add option to disable publishing (text) content
expand 2 comments