From 7ca8ee58d347dfc6b7fd18ea5d3fda9402b7eaea Mon Sep 17 00:00:00 2001 From: Feitong Yang Date: Thu, 14 May 2026 14:58:00 -0700 Subject: [PATCH] fix: allow empty description on draft essays and series MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drafts are work-in-progress — by definition the description hasn't been written yet. Requiring a non-empty description for drafts meant every `pnpm dev` of any essay page printed half a dozen error stacks to stderr (one per draft essay, fired every time getAllEssays was called — which is every essay page render in dev now that RelatedEssays + sitemap + feed + JSON-LD all use it). Functionally everything worked: drafts were silently dropped from results. Just very noisy. Fix: - validateFrontmatter (essays) and validateSeriesFrontmatter: if `draft: true`, allow description to be missing or empty. Still require it for non-drafts. - Empty description normalizes to '' in the returned EssayMeta so the type contract (description: string) is preserved. Consumers that show description already handle empty gracefully. Tests: 4 new cases — non-draft empty description still throws, draft without description passes, draft with empty-string description passes. 177 total tests pass. Note: this does not fix the one remaining error for `llm-comparison.mdx` — that file has zero frontmatter and is not marked draft. It is also untracked (not committed). Fix by adding minimal frontmatter or deleting the file. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/blog/__tests__/content-types.test.ts | 24 ++++++++++++++++++++++- apps/blog/types/content.ts | 24 +++++++++++++++++------ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/apps/blog/__tests__/content-types.test.ts b/apps/blog/__tests__/content-types.test.ts index 1e5daca..d4dbc7c 100644 --- a/apps/blog/__tests__/content-types.test.ts +++ b/apps/blog/__tests__/content-types.test.ts @@ -92,11 +92,33 @@ describe('Content Types', () => { expect(() => validateFrontmatter(rest)).toThrow('title is required'); }); - it('throws for missing description', () => { + it('throws for missing description on a non-draft', () => { const { description, ...rest } = validFrontmatter; expect(() => validateFrontmatter(rest)).toThrow('description is required'); }); + it('throws for empty-string description on a non-draft', () => { + expect(() => + validateFrontmatter({ ...validFrontmatter, description: '' }) + ).toThrow('description is required'); + }); + + it('allows missing description on a draft', () => { + const { description, ...rest } = validFrontmatter; + const result = validateFrontmatter({ ...rest, draft: true }); + expect(result.draft).toBe(true); + expect(result.description).toBe(''); + }); + + it('allows empty-string description on a draft', () => { + const result = validateFrontmatter({ + ...validFrontmatter, + description: '', + draft: true, + }); + expect(result.description).toBe(''); + }); + it('throws for missing date', () => { const { date, ...rest } = validFrontmatter; expect(() => validateFrontmatter(rest)).toThrow('date is required'); diff --git a/apps/blog/types/content.ts b/apps/blog/types/content.ts index d0ac72b..c91fd73 100644 --- a/apps/blog/types/content.ts +++ b/apps/blog/types/content.ts @@ -331,8 +331,15 @@ export function validateFrontmatter(data: Record): Frontmatter if (typeof data.title !== 'string' || !data.title) { errors.push('title is required and must be a string'); } - if (typeof data.description !== 'string' || !data.description) { - errors.push('description is required and must be a string'); + const isDraft = data.draft === true; + // Drafts are work-in-progress — description may not be filled yet. For + // published essays, description is required and must be non-empty. + if (!isDraft) { + if (typeof data.description !== 'string' || !data.description) { + errors.push('description is required and must be a string'); + } + } else if (data.description !== undefined && typeof data.description !== 'string') { + errors.push('description must be a string when provided'); } if (typeof data.date !== 'string' || !data.date) { errors.push('date is required and must be a string'); @@ -371,7 +378,7 @@ export function validateFrontmatter(data: Record): Frontmatter return { title: data.title as string, - description: data.description as string, + description: (data.description as string | undefined) ?? '', date: data.date as string, type: data.type as EssayType, topics: data.topics as Topic[], @@ -451,8 +458,13 @@ export function validateSeriesFrontmatter(data: Record): Series if (typeof data.title !== 'string' || !data.title) { errors.push('title is required and must be a string'); } - if (typeof data.description !== 'string' || !data.description) { - errors.push('description is required and must be a string'); + const isSeriesDraft = data.draft === true; + if (!isSeriesDraft) { + if (typeof data.description !== 'string' || !data.description) { + errors.push('description is required and must be a string'); + } + } else if (data.description !== undefined && typeof data.description !== 'string') { + errors.push('description must be a string when provided'); } if (typeof data.date !== 'string' || !data.date) { errors.push('date is required and must be a string'); @@ -490,7 +502,7 @@ export function validateSeriesFrontmatter(data: Record): Series return { title: data.title as string, - description: data.description as string, + description: (data.description as string | undefined) ?? '', date: data.date as string, updated: data.updated as string | undefined, category: data.category as SeriesCategory,