diff --git a/docs/docs/pages/config.mdx b/docs/docs/pages/config.mdx index 8958f60..c2e850a 100644 --- a/docs/docs/pages/config.mdx +++ b/docs/docs/pages/config.mdx @@ -51,6 +51,7 @@ | `publishDate` | `string` | Yes | `"publishDate"`, `"pubDate"`, `"date"`, `"createdAt"`, `"created_at"` | Publication date | | `coverImage` | `string` | No | `"ogImage"` | Cover image filename | | `tags` | `string[]` | No | `"tags"` | Post tags/categories | +| `draft` | `boolean` | No | `"draft"` | If `true`, post is skipped during publish | ### Example @@ -61,6 +62,7 @@ description: An introduction to my blog publishDate: 2024-01-15 ogImage: cover.jpg tags: [welcome, intro] +draft: false --- ``` @@ -72,7 +74,8 @@ Override default field names in `sequoia.json`: { "frontmatter": { "publishDate": "date", - "coverImage": "thumbnail" + "coverImage": "thumbnail", + "draft": "private" } } ``` diff --git a/docs/docs/pages/publishing.mdx b/docs/docs/pages/publishing.mdx index 8799a67..c4d88b0 100644 --- a/docs/docs/pages/publishing.mdx +++ b/docs/docs/pages/publishing.mdx @@ -45,6 +45,27 @@ When enabled, each new document will create a Bluesky post with the title, descr 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. +## Draft Posts + +Posts with `draft: true` in their frontmatter are automatically skipped during publishing. This lets you work on content without accidentally publishing it. + +```yaml +--- +title: Work in Progress +draft: true +--- +``` + +If your framework uses a different field name (like `private` or `hidden`), configure it in `sequoia.json`: + +```json +{ + "frontmatter": { + "draft": "private" + } +} +``` + ## Troubleshooting - If you have files in your markdown directory that should be ignored, use the [`ignore` array in the config](/config#ignoring-files). diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 1a84335..d37bdf8 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -138,6 +138,12 @@ export const initCommand = command({ defaultValue: "tags", placeholder: "tags, categories, keywords, etc.", }), + draftField: () => + text({ + message: "Field name for draft status:", + defaultValue: "draft", + placeholder: "draft, private, hidden, etc.", + }), }, { onCancel }, ); @@ -149,6 +155,7 @@ export const initCommand = command({ ["publishDate", frontmatterConfig.dateField, "publishDate"], ["coverImage", frontmatterConfig.coverField, "ogImage"], ["tags", frontmatterConfig.tagsField, "tags"], + ["draft", frontmatterConfig.draftField, "draft"], ]; const builtMapping = fieldMappings.reduce( diff --git a/packages/cli/src/commands/publish.ts b/packages/cli/src/commands/publish.ts index bc873ac..746e95a 100644 --- a/packages/cli/src/commands/publish.ts +++ b/packages/cli/src/commands/publish.ts @@ -96,8 +96,15 @@ export const publishCommand = command({ action: "create" | "update"; reason: string; }> = []; + const draftPosts: BlogPost[] = []; for (const post of posts) { + // Skip draft posts + if (post.frontmatter.draft) { + draftPosts.push(post); + continue; + } + const contentHash = await getContentHash(post.rawContent); const relativeFilePath = path.relative(configDir, post.filePath); const postState = state.posts[relativeFilePath]; @@ -125,6 +132,10 @@ export const publishCommand = command({ } } + if (draftPosts.length > 0) { + log.info(`Skipping ${draftPosts.length} draft post${draftPosts.length === 1 ? "" : "s"}`); + } + if (postsToPublish.length === 0) { log.success("All posts are up to date. Nothing to publish."); return; diff --git a/packages/cli/src/lib/markdown.ts b/packages/cli/src/lib/markdown.ts index 3c299a2..be9c3c3 100644 --- a/packages/cli/src/lib/markdown.ts +++ b/packages/cli/src/lib/markdown.ts @@ -99,6 +99,13 @@ export function parseFrontmatter(content: string, mapping?: FrontmatterMapping): const tagsField = mapping?.tags || "tags"; frontmatter.tags = raw[tagsField] || raw.tags; + // Draft mapping + const draftField = mapping?.draft || "draft"; + const draftValue = raw[draftField] ?? raw.draft; + if (draftValue !== undefined) { + frontmatter.draft = draftValue === true || draftValue === "true"; + } + // Always preserve atUri (internal field) frontmatter.atUri = raw.atUri; diff --git a/packages/cli/src/lib/types.ts b/packages/cli/src/lib/types.ts index 8d922d0..059a761 100644 --- a/packages/cli/src/lib/types.ts +++ b/packages/cli/src/lib/types.ts @@ -4,6 +4,7 @@ export interface FrontmatterMapping { publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at") coverImage?: string; // Field name for cover image (default: "ogImage") tags?: string; // Field name for tags (default: "tags") + draft?: string; // Field name for draft status (default: "draft") } // Strong reference for Bluesky post (com.atproto.repo.strongRef) @@ -46,6 +47,7 @@ export interface PostFrontmatter { tags?: string[]; ogImage?: string; atUri?: string; + draft?: boolean; } export interface BlogPost {