feat: added draft field to frontmatter config e1bd0d82
Steve · 2026-01-31 20:59 6 file(s) · +52 −1
docs/docs/pages/config.mdx +4 −1
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
```
docs/docs/pages/publishing.mdx +21 −0
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).
packages/cli/src/commands/init.ts +7 −0
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>(
packages/cli/src/commands/publish.ts +11 −0
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) {
packages/cli/src/lib/markdown.ts +7 −0
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
packages/cli/src/lib/types.ts +2 −0
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 {