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

feat: added draft field to frontmatter config

authored by stevedylan.dev and committed by tangled.org e1bd0d82 e51d33d5

+52 -1
+4 -1
docs/docs/pages/config.mdx
··· 51 | `publishDate` | `string` | Yes | `"publishDate"`, `"pubDate"`, `"date"`, `"createdAt"`, `"created_at"` | Publication date | 52 | `coverImage` | `string` | No | `"ogImage"` | Cover image filename | 53 | `tags` | `string[]` | No | `"tags"` | Post tags/categories | 54 55 ### Example 56 ··· 61 publishDate: 2024-01-15 62 ogImage: cover.jpg 63 tags: [welcome, intro] 64 --- 65 ``` 66 ··· 72 { 73 "frontmatter": { 74 "publishDate": "date", 75 - "coverImage": "thumbnail" 76 } 77 } 78 ```
··· 51 | `publishDate` | `string` | Yes | `"publishDate"`, `"pubDate"`, `"date"`, `"createdAt"`, `"created_at"` | Publication date | 52 | `coverImage` | `string` | No | `"ogImage"` | Cover image filename | 53 | `tags` | `string[]` | No | `"tags"` | Post tags/categories | 54 + | `draft` | `boolean` | No | `"draft"` | If `true`, post is skipped during publish | 55 56 ### Example 57 ··· 62 publishDate: 2024-01-15 63 ogImage: cover.jpg 64 tags: [welcome, intro] 65 + draft: false 66 --- 67 ``` 68 ··· 74 { 75 "frontmatter": { 76 "publishDate": "date", 77 + "coverImage": "thumbnail", 78 + "draft": "private" 79 } 80 } 81 ```
+21
docs/docs/pages/publishing.mdx
··· 45 46 The `maxAgeDays` setting prevents flooding your feed when first setting up Sequoia. For example, if you have 40 existing blog posts, only those published within the last 30 days will be posted to Bluesky. 47 48 ## Troubleshooting 49 50 - If you have files in your markdown directory that should be ignored, use the [`ignore` array in the config](/config#ignoring-files).
··· 45 46 The `maxAgeDays` setting prevents flooding your feed when first setting up Sequoia. For example, if you have 40 existing blog posts, only those published within the last 30 days will be posted to Bluesky. 47 48 + ## Draft Posts 49 + 50 + Posts with `draft: true` in their frontmatter are automatically skipped during publishing. This lets you work on content without accidentally publishing it. 51 + 52 + ```yaml 53 + --- 54 + title: Work in Progress 55 + draft: true 56 + --- 57 + ``` 58 + 59 + If your framework uses a different field name (like `private` or `hidden`), configure it in `sequoia.json`: 60 + 61 + ```json 62 + { 63 + "frontmatter": { 64 + "draft": "private" 65 + } 66 + } 67 + ``` 68 + 69 ## Troubleshooting 70 71 - If you have files in your markdown directory that should be ignored, use the [`ignore` array in the config](/config#ignoring-files).
+7
packages/cli/src/commands/init.ts
··· 138 defaultValue: "tags", 139 placeholder: "tags, categories, keywords, etc.", 140 }), 141 }, 142 { onCancel }, 143 ); ··· 149 ["publishDate", frontmatterConfig.dateField, "publishDate"], 150 ["coverImage", frontmatterConfig.coverField, "ogImage"], 151 ["tags", frontmatterConfig.tagsField, "tags"], 152 ]; 153 154 const builtMapping = fieldMappings.reduce<FrontmatterMapping>(
··· 138 defaultValue: "tags", 139 placeholder: "tags, categories, keywords, etc.", 140 }), 141 + draftField: () => 142 + text({ 143 + message: "Field name for draft status:", 144 + defaultValue: "draft", 145 + placeholder: "draft, private, hidden, etc.", 146 + }), 147 }, 148 { onCancel }, 149 ); ··· 155 ["publishDate", frontmatterConfig.dateField, "publishDate"], 156 ["coverImage", frontmatterConfig.coverField, "ogImage"], 157 ["tags", frontmatterConfig.tagsField, "tags"], 158 + ["draft", frontmatterConfig.draftField, "draft"], 159 ]; 160 161 const builtMapping = fieldMappings.reduce<FrontmatterMapping>(
+11
packages/cli/src/commands/publish.ts
··· 96 action: "create" | "update"; 97 reason: string; 98 }> = []; 99 100 for (const post of posts) { 101 const contentHash = await getContentHash(post.rawContent); 102 const relativeFilePath = path.relative(configDir, post.filePath); 103 const postState = state.posts[relativeFilePath]; ··· 123 reason: "content changed", 124 }); 125 } 126 } 127 128 if (postsToPublish.length === 0) {
··· 96 action: "create" | "update"; 97 reason: string; 98 }> = []; 99 + const draftPosts: BlogPost[] = []; 100 101 for (const post of posts) { 102 + // Skip draft posts 103 + if (post.frontmatter.draft) { 104 + draftPosts.push(post); 105 + continue; 106 + } 107 + 108 const contentHash = await getContentHash(post.rawContent); 109 const relativeFilePath = path.relative(configDir, post.filePath); 110 const postState = state.posts[relativeFilePath]; ··· 130 reason: "content changed", 131 }); 132 } 133 + } 134 + 135 + if (draftPosts.length > 0) { 136 + log.info(`Skipping ${draftPosts.length} draft post${draftPosts.length === 1 ? "" : "s"}`); 137 } 138 139 if (postsToPublish.length === 0) {
+7
packages/cli/src/lib/markdown.ts
··· 99 const tagsField = mapping?.tags || "tags"; 100 frontmatter.tags = raw[tagsField] || raw.tags; 101 102 // Always preserve atUri (internal field) 103 frontmatter.atUri = raw.atUri; 104
··· 99 const tagsField = mapping?.tags || "tags"; 100 frontmatter.tags = raw[tagsField] || raw.tags; 101 102 + // Draft mapping 103 + const draftField = mapping?.draft || "draft"; 104 + const draftValue = raw[draftField] ?? raw.draft; 105 + if (draftValue !== undefined) { 106 + frontmatter.draft = draftValue === true || draftValue === "true"; 107 + } 108 + 109 // Always preserve atUri (internal field) 110 frontmatter.atUri = raw.atUri; 111
+2
packages/cli/src/lib/types.ts
··· 4 publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at") 5 coverImage?: string; // Field name for cover image (default: "ogImage") 6 tags?: string; // Field name for tags (default: "tags") 7 } 8 9 // Strong reference for Bluesky post (com.atproto.repo.strongRef) ··· 46 tags?: string[]; 47 ogImage?: string; 48 atUri?: string; 49 } 50 51 export interface BlogPost {
··· 4 publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at") 5 coverImage?: string; // Field name for cover image (default: "ogImage") 6 tags?: string; // Field name for tags (default: "tags") 7 + draft?: string; // Field name for draft status (default: "draft") 8 } 9 10 // Strong reference for Bluesky post (com.atproto.repo.strongRef) ··· 47 tags?: string[]; 48 ogImage?: string; 49 atUri?: string; 50 + draft?: boolean; 51 } 52 53 export interface BlogPost {