@@ -258,4 +258,333 @@ More content.
258258 } ) ;
259259 } ) ;
260260 } ) ;
261+
262+ describe ( "slug generation" , ( ) => {
263+ it ( "uses frontmatter slug when provided" , ( ) => {
264+ fsMocks . existsSync . mockReturnValue ( true ) ;
265+ fsMocks . readdirSync . mockReturnValue ( [ "post.md" ] ) ;
266+ fsMocks . readFileSync . mockReturnValue ( `+++
267+ title = "My Post"
268+ slug = "explicit-slug"
269+ date = 2026-01-01
270+ +++
271+ Content` ) ;
272+
273+ const posts = getAllPosts ( ) ;
274+ expect ( posts [ 0 ] . slug ) . toBe ( "explicit-slug" ) ;
275+ } ) ;
276+
277+ it ( "derives slug from filename when no frontmatter slug is given" , ( ) => {
278+ fsMocks . existsSync . mockReturnValue ( true ) ;
279+ fsMocks . readdirSync . mockReturnValue ( [ "my-filename.md" ] ) ;
280+ fsMocks . readFileSync . mockReturnValue ( `+++
281+ title = "My Post"
282+ date = 2026-01-01
283+ +++
284+ Content` ) ;
285+
286+ const posts = getAllPosts ( ) ;
287+ expect ( posts [ 0 ] . slug ) . toBe ( "my-filename" ) ;
288+ } ) ;
289+
290+ it ( "sanitizes filename-derived slug: lowercase, spaces to hyphens, removes special chars" , ( ) => {
291+ fsMocks . existsSync . mockReturnValue ( true ) ;
292+ fsMocks . readdirSync . mockReturnValue ( [ "My Cool Post! (2026).md" ] ) ;
293+ fsMocks . readFileSync . mockReturnValue ( `+++
294+ title = "My Cool Post! (2026)"
295+ date = 2026-01-01
296+ +++
297+ Content` ) ;
298+
299+ const posts = getAllPosts ( ) ;
300+ // Expect: lowercase, spaces → hyphens, remove punctuation except hyphens
301+ expect ( posts [ 0 ] . slug ) . toBe ( "my-cool-post-2026" ) ;
302+ } ) ;
303+
304+ it ( "handles uppercase and mixed-case filenames" , ( ) => {
305+ fsMocks . existsSync . mockReturnValue ( true ) ;
306+ fsMocks . readdirSync . mockReturnValue ( [ "UPPERCASE_POST.md" ] ) ;
307+ fsMocks . readFileSync . mockReturnValue ( `+++
308+ title = "Uppercase"
309+ date = 2026-01-01
310+ +++
311+ Content` ) ;
312+
313+ const posts = getAllPosts ( ) ;
314+ expect ( posts [ 0 ] . slug ) . toBe ( "uppercase-post" ) ; // or "uppercase-post" depending on impl
315+ } ) ;
316+
317+ it ( "uses the slug for lookups in getPostBySlug" , ( ) => {
318+ fsMocks . existsSync . mockReturnValue ( true ) ;
319+ fsMocks . readdirSync . mockReturnValue ( [ "filename.md" ] ) ;
320+ fsMocks . readFileSync . mockReturnValue ( `+++
321+ title = "Test"
322+ slug = "custom-lookup"
323+ date = 2026-01-01
324+ +++
325+ Content` ) ;
326+
327+ const post = getPostBySlug ( "custom-lookup" ) ;
328+ expect ( post ) . not . toBeNull ( ) ;
329+ expect ( post ?. slug ) . toBe ( "custom-lookup" ) ;
330+ } ) ;
331+
332+ it ( "returns null when slug does not match any post (derived or explicit)" , ( ) => {
333+ fsMocks . existsSync . mockReturnValue ( true ) ;
334+ fsMocks . readdirSync . mockReturnValue ( [ "existing.md" ] ) ;
335+ fsMocks . readFileSync . mockReturnValue ( `+++
336+ title = "Existing"
337+ date = 2026-01-01
338+ +++
339+ Content` ) ;
340+
341+ const post = getPostBySlug ( "nonexistent" ) ;
342+ expect ( post ) . toBeNull ( ) ;
343+ } ) ;
344+ } ) ;
345+
346+ describe ( "date parsing and sorting" , ( ) => {
347+ it ( "parses YYYY-MM-DD date strings correctly" , ( ) => {
348+ fsMocks . existsSync . mockReturnValue ( true ) ;
349+ fsMocks . readdirSync . mockReturnValue ( [ "post.md" ] ) ;
350+ fsMocks . readFileSync . mockReturnValue ( `+++
351+ title = "Post"
352+ date = 2025-12-25
353+ +++
354+ Content` ) ;
355+
356+ const posts = getAllPosts ( ) ;
357+ expect ( posts [ 0 ] . date ) . toBe ( "2025-12-25T00:00:00.000Z" ) ;
358+ } ) ;
359+
360+ it ( "parses ISO 8601 date strings with time" , ( ) => {
361+ fsMocks . existsSync . mockReturnValue ( true ) ;
362+ fsMocks . readdirSync . mockReturnValue ( [ "post.md" ] ) ;
363+ fsMocks . readFileSync . mockReturnValue ( `+++
364+ title = "Post"
365+ date = "2026-03-20T15:30:00Z"
366+ +++
367+ Content` ) ;
368+
369+ const posts = getAllPosts ( ) ;
370+ expect ( posts [ 0 ] . date ) . toBe ( "2026-03-20T15:30:00.000Z" ) ;
371+ } ) ;
372+
373+ it ( "handles missing date by falling back to a default (e.g., file mtime or epoch)" , ( ) => {
374+ // Implementation dependent; test that it doesn't crash and returns some date string.
375+ fsMocks . existsSync . mockReturnValue ( true ) ;
376+ fsMocks . readdirSync . mockReturnValue ( [ "nodate.md" ] ) ;
377+ fsMocks . readFileSync . mockReturnValue ( `+++
378+ title = "No Date"
379+ +++
380+ Content` ) ;
381+
382+ const posts = getAllPosts ( ) ;
383+ expect ( posts [ 0 ] ) . toHaveProperty ( "date" ) ;
384+ expect ( typeof posts [ 0 ] . date ) . toBe ( "string" ) ;
385+ } ) ;
386+ it ( "skips posts with invalid date formats (or treats as oldest)" , ( ) => {
387+ fsMocks . existsSync . mockReturnValue ( true ) ;
388+ fsMocks . readdirSync . mockReturnValue ( [ "valid.md" , "invalid.md" ] ) ;
389+ fsMocks . readFileSync . mockImplementation ( ( filePath : string ) => {
390+ if ( filePath . includes ( "valid.md" ) ) {
391+ return `+++\ntitle = "Valid"\ndate = 2026-01-01\n+++\nContent` ;
392+ } else {
393+ return `+++\ntitle = "Invalid"\ndate = "not-a-date"\n+++\nContent` ;
394+ }
395+ } ) ;
396+
397+ const posts = getAllPosts ( ) ;
398+
399+ // Both posts are kept (length 2)
400+ expect ( posts ) . toHaveLength ( 2 ) ;
401+
402+ // At least one post has the correct valid date
403+ expect ( posts . some ( p => p . date === "2026-01-01T00:00:00.000Z" ) ) . toBe ( true ) ;
404+
405+ // All posts have some date string (fallback for invalid)
406+ expect ( posts . every ( p => typeof p . date === "string" ) ) . toBe ( true ) ;
407+ } ) ;
408+
409+ it ( "sorts posts in descending date order (newest first)" , ( ) => {
410+ fsMocks . existsSync . mockReturnValue ( true ) ;
411+ fsMocks . readdirSync . mockReturnValue ( [ "old.md" , "new.md" , "middle.md" ] ) ;
412+ fsMocks . readFileSync . mockImplementation ( ( filePath : string ) => {
413+ if ( filePath . includes ( "old.md" ) )
414+ return `+++\ntitle = "Old"\ndate = 2024-01-01\n+++` ;
415+ if ( filePath . includes ( "middle.md" ) )
416+ return `+++\ntitle = "Middle"\ndate = 2025-01-01\n+++` ;
417+ return `+++\ntitle = "New"\ndate = 2026-01-01\n+++` ;
418+ } ) ;
419+
420+ const posts = getAllPosts ( ) ;
421+ expect ( posts . map ( p => p . title ) ) . toEqual ( [ "New" , "Middle" , "Old" ] ) ;
422+ } ) ;
423+
424+ it ( "handles equal dates by stable sort (e.g., by title or filename)" , ( ) => {
425+ fsMocks . existsSync . mockReturnValue ( true ) ;
426+ fsMocks . readdirSync . mockReturnValue ( [ "a.md" , "b.md" ] ) ;
427+ fsMocks . readFileSync . mockImplementation ( ( filePath : string ) => {
428+ if ( filePath . includes ( "a.md" ) )
429+ return `+++\ntitle = "A"\ndate = 2026-01-01\n+++` ;
430+ return `+++\ntitle = "B"\ndate = 2026-01-01\n+++` ;
431+ } ) ;
432+
433+ const posts = getAllPosts ( ) ;
434+
435+ expect ( posts [ 0 ] . title ) . toBe ( "A" ) ;
436+ expect ( posts [ 1 ] . title ) . toBe ( "B" ) ;
437+ } ) ;
438+ } ) ;
439+ describe ( "draft filtering" , ( ) => {
440+ it ( "excludes posts with draft = true from getAllPosts" , ( ) => {
441+ fsMocks . existsSync . mockReturnValue ( true ) ;
442+ fsMocks . readdirSync . mockReturnValue ( [ "published.md" , "draft.md" ] ) ;
443+ fsMocks . readFileSync . mockImplementation ( ( filePath : string ) => {
444+ if ( filePath . includes ( "published.md" ) ) {
445+ return `+++\ntitle = "Published"\ndate = 2026-01-01\ndraft = false\n+++` ;
446+ }
447+ return `+++\ntitle = "Draft"\ndate = 2026-01-02\ndraft = true\n+++` ;
448+ } ) ;
449+
450+ const posts = getAllPosts ( ) ;
451+
452+ // Should only find the one published post
453+ expect ( posts ) . toHaveLength ( 1 ) ;
454+ expect ( posts [ 0 ] . title ) . toBe ( "Published" ) ;
455+ } ) ;
456+
457+ it ( "strictly excludes draft posts from getPostBySlug lookup" , ( ) => {
458+ fsMocks . existsSync . mockReturnValue ( true ) ;
459+ fsMocks . readdirSync . mockReturnValue ( [ "draft.md" ] ) ;
460+ fsMocks . readFileSync . mockReturnValue ( `+++
461+ title = "Draft Post"
462+ slug = "draft-post"
463+ date = 2026-01-01
464+ draft = true
465+ +++
466+ Draft content` ) ;
467+
468+ const post = getPostBySlug ( "draft-post" ) ;
469+
470+ // Because data.draft === true, the function hits 'continue' and returns null
471+ expect ( post ) . toBeNull ( ) ;
472+ } ) ;
473+
474+ it ( "allows retrieving posts via getPostBySlug when draft is false" , ( ) => {
475+ fsMocks . existsSync . mockReturnValue ( true ) ;
476+ fsMocks . readdirSync . mockReturnValue ( [ "live-post.md" ] ) ;
477+ fsMocks . readFileSync . mockReturnValue ( `+++
478+ title = "Live Post"
479+ slug = "live-post"
480+ date = 2026-01-01
481+ draft = false
482+ +++
483+ Live content` ) ;
484+
485+ const post = getPostBySlug ( "live-post" ) ;
486+
487+ expect ( post ) . not . toBeNull ( ) ;
488+ expect ( post ?. title ) . toBe ( "Live Post" ) ;
489+ expect ( post ?. slug ) . toBe ( "live-post" ) ;
490+ } ) ;
491+ } ) ;
492+
493+ describe ( "metadata fallbacks and edge cases" , ( ) => {
494+ it ( "uses FALLBACK_IMAGE when featured_image is missing" , ( ) => {
495+ fsMocks . existsSync . mockReturnValue ( true ) ;
496+ fsMocks . readdirSync . mockReturnValue ( [ "no-image.md" ] ) ;
497+ fsMocks . readFileSync . mockReturnValue ( `+++
498+ title = "No Image"
499+ date = 2026-01-01
500+ +++
501+ Content` ) ;
502+
503+ const posts = getAllPosts ( ) ;
504+ expect ( posts [ 0 ] . featuredImage ) . toBe ( FALLBACK_IMAGE ) ;
505+ } ) ;
506+
507+ it ( "keeps provided featured_image when present" , ( ) => {
508+ fsMocks . existsSync . mockReturnValue ( true ) ;
509+ fsMocks . readdirSync . mockReturnValue ( [ "with-image.md" ] ) ;
510+ fsMocks . readFileSync . mockReturnValue ( `+++
511+ title = "With Image"
512+ date = 2026-01-01
513+ featured_image = "/custom.png"
514+ +++
515+ Content` ) ;
516+
517+ const posts = getAllPosts ( ) ;
518+ expect ( posts [ 0 ] . featuredImage ) . toBe ( "/custom.png" ) ;
519+ } ) ;
520+
521+ it ( "handles missing description/abstract gracefully (undefined or empty)" , ( ) => {
522+ fsMocks . existsSync . mockReturnValue ( true ) ;
523+ fsMocks . readdirSync . mockReturnValue ( [ "no-desc.md" ] ) ;
524+ fsMocks . readFileSync . mockReturnValue ( `+++
525+ title = "No Description"
526+ date = 2026-01-01
527+ +++
528+ Content` ) ;
529+
530+ const posts = getAllPosts ( ) ;
531+ expect ( posts [ 0 ] . abstract ) . toBeUndefined ( ) ;
532+ } ) ;
533+
534+ it ( "handles missing categories and tags as empty arrays" , ( ) => {
535+ fsMocks . existsSync . mockReturnValue ( true ) ;
536+ fsMocks . readdirSync . mockReturnValue ( [ "no-tax.md" ] ) ;
537+ fsMocks . readFileSync . mockReturnValue ( `+++
538+ title = "No Categories"
539+ date = 2026-01-01
540+ +++
541+ Content` ) ;
542+
543+ const posts = getAllPosts ( ) ;
544+ expect ( posts [ 0 ] . categories ) . toEqual ( [ ] ) ;
545+ expect ( posts [ 0 ] . tags ) . toEqual ( [ ] ) ;
546+ } ) ;
547+
548+ it ( "handles missing authors field" , ( ) => {
549+ fsMocks . existsSync . mockReturnValue ( true ) ;
550+ fsMocks . readdirSync . mockReturnValue ( [ "no-author.md" ] ) ;
551+ fsMocks . readFileSync . mockReturnValue ( `+++
552+ title = "No Author"
553+ date = 2026-01-01
554+ +++
555+ Content` ) ;
556+
557+ const posts = getAllPosts ( ) ;
558+ expect ( posts [ 0 ] . authors ) . toEqual ( [ ] ) ;
559+ } ) ;
560+
561+ it ( "skips malformed frontmatter files without crashing" , ( ) => {
562+ fsMocks . existsSync . mockReturnValue ( true ) ;
563+ fsMocks . readdirSync . mockReturnValue ( [ "good.md" , "bad.md" ] ) ;
564+ fsMocks . readFileSync . mockImplementation ( ( filePath : string ) => {
565+ if ( filePath . includes ( "good.md" ) ) {
566+ return `+++\ntitle = "Good"\ndate = 2026-01-01\n+++` ;
567+ }
568+ return `+++\ntitle = "Bad"\ndate = [invalid]\n+++` ;
569+ } ) ;
570+
571+ const posts = getAllPosts ( ) ;
572+ expect ( posts ) . toHaveLength ( 1 ) ;
573+ expect ( posts [ 0 ] . title ) . toBe ( "Good" ) ;
574+ } ) ;
575+
576+ it ( "ignores non-markdown files in posts directory" , ( ) => {
577+ fsMocks . existsSync . mockReturnValue ( true ) ;
578+ fsMocks . readdirSync . mockReturnValue ( [ "post.md" , "image.png" , "draft.md" ] ) ;
579+ fsMocks . readFileSync . mockImplementation ( ( filePath : string ) => {
580+ if ( filePath . includes ( ".md" ) ) {
581+ return `+++\ntitle = "Markdown"\ndate = 2026-01-01\n+++` ;
582+ }
583+ throw new Error ( "Should not read non-md files" ) ;
584+ } ) ;
585+
586+ const posts = getAllPosts ( ) ;
587+ expect ( posts ) . toHaveLength ( 2 ) ; // both .md files
588+ } ) ;
589+ } ) ;
261590} ) ;
0 commit comments