A CLI for publishing standard.site documents to ATProto

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 51 | `publishDate` | `string` | Yes | `"publishDate"`, `"pubDate"`, `"date"`, `"createdAt"`, `"created_at"` | Publication date | 52 52 | `coverImage` | `string` | No | `"ogImage"` | Cover image filename | 53 53 | `tags` | `string[]` | No | `"tags"` | Post tags/categories | 54 + | `draft` | `boolean` | No | `"draft"` | If `true`, post is skipped during publish | 54 55 55 56 ### Example 56 57 ··· 61 62 publishDate: 2024-01-15 62 63 ogImage: cover.jpg 63 64 tags: [welcome, intro] 65 + draft: false 64 66 --- 65 67 ``` 66 68 ··· 72 74 { 73 75 "frontmatter": { 74 76 "publishDate": "date", 75 - "coverImage": "thumbnail" 77 + "coverImage": "thumbnail", 78 + "draft": "private" 76 79 } 77 80 } 78 81 ```
+21
docs/docs/pages/publishing.mdx
··· 45 45 46 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 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 + 48 69 ## Troubleshooting 49 70 50 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 138 defaultValue: "tags", 139 139 placeholder: "tags, categories, keywords, etc.", 140 140 }), 141 + draftField: () => 142 + text({ 143 + message: "Field name for draft status:", 144 + defaultValue: "draft", 145 + placeholder: "draft, private, hidden, etc.", 146 + }), 141 147 }, 142 148 { onCancel }, 143 149 ); ··· 149 155 ["publishDate", frontmatterConfig.dateField, "publishDate"], 150 156 ["coverImage", frontmatterConfig.coverField, "ogImage"], 151 157 ["tags", frontmatterConfig.tagsField, "tags"], 158 + ["draft", frontmatterConfig.draftField, "draft"], 152 159 ]; 153 160 154 161 const builtMapping = fieldMappings.reduce<FrontmatterMapping>(
+11
packages/cli/src/commands/publish.ts
··· 96 96 action: "create" | "update"; 97 97 reason: string; 98 98 }> = []; 99 + const draftPosts: BlogPost[] = []; 99 100 100 101 for (const post of posts) { 102 + // Skip draft posts 103 + if (post.frontmatter.draft) { 104 + draftPosts.push(post); 105 + continue; 106 + } 107 + 101 108 const contentHash = await getContentHash(post.rawContent); 102 109 const relativeFilePath = path.relative(configDir, post.filePath); 103 110 const postState = state.posts[relativeFilePath]; ··· 123 130 reason: "content changed", 124 131 }); 125 132 } 133 + } 134 + 135 + if (draftPosts.length > 0) { 136 + log.info(`Skipping ${draftPosts.length} draft post${draftPosts.length === 1 ? "" : "s"}`); 126 137 } 127 138 128 139 if (postsToPublish.length === 0) {
+7
packages/cli/src/lib/markdown.ts
··· 99 99 const tagsField = mapping?.tags || "tags"; 100 100 frontmatter.tags = raw[tagsField] || raw.tags; 101 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 + 102 109 // Always preserve atUri (internal field) 103 110 frontmatter.atUri = raw.atUri; 104 111
+2
packages/cli/src/lib/types.ts
··· 4 4 publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at") 5 5 coverImage?: string; // Field name for cover image (default: "ogImage") 6 6 tags?: string; // Field name for tags (default: "tags") 7 + draft?: string; // Field name for draft status (default: "draft") 7 8 } 8 9 9 10 // Strong reference for Bluesky post (com.atproto.repo.strongRef) ··· 46 47 tags?: string[]; 47 48 ogImage?: string; 48 49 atUri?: string; 50 + draft?: boolean; 49 51 } 50 52 51 53 export interface BlogPost {