Skip to content

Commit 39ea7e4

Browse files
committed
feat: add initial blog posts with proper markdown formatting
1 parent 87aeec9 commit 39ea7e4

4 files changed

Lines changed: 869 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import { BlogPostFrontmatter } from '@/types';
4+
5+
describe('Blog Posts Content Validation', () => {
6+
const postsDirectory = path.join(process.cwd(), 'src/content/posts');
7+
8+
// Helper function to get all markdown files
9+
const getMarkdownFiles = (): string[] => {
10+
if (!fs.existsSync(postsDirectory)) {
11+
return [];
12+
}
13+
return fs.readdirSync(postsDirectory)
14+
.filter(file => file.endsWith('.md'))
15+
.map(file => path.join(postsDirectory, file));
16+
};
17+
18+
// Helper function to extract frontmatter from markdown
19+
const extractFrontmatter = (content: string): BlogPostFrontmatter => {
20+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
21+
if (!frontmatterMatch) {
22+
throw new Error('No frontmatter found');
23+
}
24+
25+
const frontmatterText = frontmatterMatch[1];
26+
const frontmatter: any = {};
27+
28+
frontmatterText.split('\n').forEach(line => {
29+
const [key, ...valueParts] = line.split(':');
30+
if (key && valueParts.length > 0) {
31+
const value = valueParts.join(':').trim();
32+
33+
if (key === 'tags') {
34+
// Parse tags array
35+
frontmatter[key] = value.replace(/[\[\]]/g, '').split(',').map(tag => tag.trim());
36+
} else if (key === 'date') {
37+
frontmatter[key] = value;
38+
} else if (key === 'readingTime' || key === 'published') {
39+
frontmatter[key] = key === 'published' ? value === 'true' : parseInt(value);
40+
} else {
41+
frontmatter[key] = value;
42+
}
43+
}
44+
});
45+
46+
return frontmatter as BlogPostFrontmatter;
47+
};
48+
49+
// Helper function to calculate reading time
50+
const calculateReadingTime = (content: string): number => {
51+
const words = content.split(/\s+/).length;
52+
return Math.ceil(words / 200); // 200 words per minute
53+
};
54+
55+
describe('Frontmatter Validation', () => {
56+
it('should have valid frontmatter for all blog posts', () => {
57+
const markdownFiles = getMarkdownFiles();
58+
59+
if (markdownFiles.length === 0) {
60+
// This test will fail initially as no posts exist yet
61+
throw new Error('No blog posts found in content/posts directory');
62+
}
63+
64+
markdownFiles.forEach(filePath => {
65+
const content = fs.readFileSync(filePath, 'utf-8');
66+
const frontmatter = extractFrontmatter(content);
67+
68+
// Validate required fields
69+
expect(frontmatter.title).toBeDefined();
70+
expect(frontmatter.title.length).toBeGreaterThan(0);
71+
expect(frontmatter.date).toBeDefined();
72+
expect(frontmatter.excerpt).toBeDefined();
73+
expect(frontmatter.excerpt.length).toBeGreaterThan(0);
74+
expect(frontmatter.tags).toBeDefined();
75+
expect(Array.isArray(frontmatter.tags)).toBe(true);
76+
expect(frontmatter.category).toBeDefined();
77+
expect(frontmatter.readingTime).toBeDefined();
78+
expect(typeof frontmatter.readingTime).toBe('number');
79+
expect(frontmatter.published).toBeDefined();
80+
expect(typeof frontmatter.published).toBe('boolean');
81+
});
82+
});
83+
84+
it('should have valid date format in frontmatter', () => {
85+
const markdownFiles = getMarkdownFiles();
86+
87+
if (markdownFiles.length === 0) {
88+
throw new Error('No blog posts found in content/posts directory');
89+
}
90+
91+
markdownFiles.forEach(filePath => {
92+
const content = fs.readFileSync(filePath, 'utf-8');
93+
const frontmatter = extractFrontmatter(content);
94+
95+
// Validate date format (YYYY-MM-DD)
96+
expect(frontmatter.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
97+
expect(() => new Date(frontmatter.date)).not.toThrow();
98+
});
99+
});
100+
});
101+
102+
describe('Markdown Content Validation', () => {
103+
it('should have properly formatted markdown content', () => {
104+
const markdownFiles = getMarkdownFiles();
105+
106+
if (markdownFiles.length === 0) {
107+
throw new Error('No blog posts found in content/posts directory');
108+
}
109+
110+
markdownFiles.forEach(filePath => {
111+
const content = fs.readFileSync(filePath, 'utf-8');
112+
113+
// Remove frontmatter to get content only
114+
const contentOnly = content.replace(/^---\n[\s\S]*?\n---\n/, '');
115+
116+
// Should have content after frontmatter
117+
expect(contentOnly.trim().length).toBeGreaterThan(0);
118+
119+
// Should have at least one heading
120+
expect(contentOnly).toMatch(/^#\s+/m);
121+
122+
// Should have proper paragraph spacing
123+
expect(contentOnly).toMatch(/\n\n/);
124+
});
125+
});
126+
127+
it('should have code blocks with proper syntax highlighting', () => {
128+
const markdownFiles = getMarkdownFiles();
129+
130+
if (markdownFiles.length === 0) {
131+
throw new Error('No blog posts found in content/posts directory');
132+
}
133+
134+
markdownFiles.forEach(filePath => {
135+
const content = fs.readFileSync(filePath, 'utf-8');
136+
const contentOnly = content.replace(/^---\n[\s\S]*?\n---\n/, '');
137+
138+
// Should have at least one code block
139+
expect(contentOnly).toMatch(/```\w*\n[\s\S]*?\n```/);
140+
141+
// Code blocks should have language specification
142+
const codeBlocks = contentOnly.match(/```(\w+)\n[\s\S]*?\n```/g);
143+
if (codeBlocks) {
144+
codeBlocks.forEach(block => {
145+
expect(block).toMatch(/```\w+\n/);
146+
});
147+
}
148+
});
149+
});
150+
});
151+
152+
describe('Reading Time Calculation', () => {
153+
it('should have accurate reading time calculation', () => {
154+
const markdownFiles = getMarkdownFiles();
155+
156+
if (markdownFiles.length === 0) {
157+
throw new Error('No blog posts found in content/posts directory');
158+
}
159+
160+
markdownFiles.forEach(filePath => {
161+
const content = fs.readFileSync(filePath, 'utf-8');
162+
const frontmatter = extractFrontmatter(content);
163+
const contentOnly = content.replace(/^---\n[\s\S]*?\n---\n/, '');
164+
165+
const calculatedReadingTime = calculateReadingTime(contentOnly);
166+
const frontmatterReadingTime = frontmatter.readingTime;
167+
168+
// Reading time should be within 1 minute of calculated time
169+
expect(Math.abs(calculatedReadingTime - frontmatterReadingTime)).toBeLessThanOrEqual(1);
170+
});
171+
});
172+
});
173+
174+
describe('Word Count Validation', () => {
175+
it('should have appropriate word count (800-1200 words)', () => {
176+
const markdownFiles = getMarkdownFiles();
177+
178+
if (markdownFiles.length === 0) {
179+
throw new Error('No blog posts found in content/posts directory');
180+
}
181+
182+
markdownFiles.forEach(filePath => {
183+
const content = fs.readFileSync(filePath, 'utf-8');
184+
const contentOnly = content.replace(/^---\n[\s\S]*?\n---\n/, '');
185+
186+
const wordCount = contentOnly.split(/\s+/).length;
187+
188+
// Should be between 800-1200 words
189+
expect(wordCount).toBeGreaterThanOrEqual(800);
190+
expect(wordCount).toBeLessThanOrEqual(1200);
191+
});
192+
});
193+
});
194+
195+
describe('Slug Naming Convention', () => {
196+
it('should follow snake_case naming convention', () => {
197+
const markdownFiles = getMarkdownFiles();
198+
199+
if (markdownFiles.length === 0) {
200+
throw new Error('No blog posts found in content/posts directory');
201+
}
202+
203+
markdownFiles.forEach(filePath => {
204+
const fileName = path.basename(filePath, '.md');
205+
206+
// Should be snake_case format
207+
expect(fileName).toMatch(/^[a-z0-9_]+$/);
208+
expect(fileName).not.toMatch(/[A-Z]/);
209+
expect(fileName).not.toMatch(/^-|-$/);
210+
});
211+
});
212+
});
213+
});

0 commit comments

Comments
 (0)