* Scans the blog directory for .md files and extracts validated frontmatter. * Returns all posts (including drafts) sorted by date descending. * Resolves Bluesky avatars at build time.
(
blogDir: string,
options: {
imagesDir: string
resolveAvatars: boolean
},
)
| 88 | * Resolves Bluesky avatars at build time. |
| 89 | */ |
| 90 | async function loadBlogPosts( |
| 91 | blogDir: string, |
| 92 | options: { |
| 93 | imagesDir: string |
| 94 | resolveAvatars: boolean |
| 95 | }, |
| 96 | ): Promise<BlogPostFrontmatter[]> { |
| 97 | const { imagesDir, resolveAvatars } = options |
| 98 | const files = await Array.fromAsync(glob(join(blogDir, '**/*.md').replace(/\\/g, '/'))) |
| 99 | |
| 100 | // First pass: extract raw frontmatter and collect all Bluesky handles |
| 101 | const rawPosts: Array<{ frontmatter: Record<string, unknown> }> = [] |
| 102 | const allHandles = new Set<string>() |
| 103 | |
| 104 | for (const file of files) { |
| 105 | const { data: frontmatter } = read(file) |
| 106 | |
| 107 | // Normalise slug → path (same logic as standard-site-sync) |
| 108 | if (typeof frontmatter.slug === 'string' && !frontmatter.path) { |
| 109 | frontmatter.path = `/blog/${frontmatter.slug}` |
| 110 | } |
| 111 | // Normalise date to ISO string |
| 112 | if (frontmatter.date) { |
| 113 | const raw = frontmatter.date |
| 114 | frontmatter.date = new Date(raw instanceof Date ? raw : String(raw)).toISOString() |
| 115 | } |
| 116 | |
| 117 | // Validate authors before resolving so we can extract handles |
| 118 | const authorsResult = safeParse(array(AuthorSchema), frontmatter.authors) |
| 119 | if (authorsResult.success) { |
| 120 | for (const author of authorsResult.output) { |
| 121 | if (author.blueskyHandle) { |
| 122 | allHandles.add(author.blueskyHandle) |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | rawPosts.push({ frontmatter }) |
| 128 | } |
| 129 | |
| 130 | // Batch-fetch all Bluesky avatars in a single request when avatar resolution is enabled. |
| 131 | const avatarMap = resolveAvatars |
| 132 | ? await fetchBlueskyAvatars(imagesDir, [...allHandles]) |
| 133 | : new Map<string, string>() |
| 134 | |
| 135 | // Second pass: validate with raw schema, then enrich authors with avatars |
| 136 | const posts: BlogPostFrontmatter[] = [] |
| 137 | |
| 138 | for (const { frontmatter } of rawPosts) { |
| 139 | const result = safeParse(RawBlogPostSchema, frontmatter) |
| 140 | if (!result.success) continue |
| 141 | |
| 142 | posts.push({ |
| 143 | ...result.output, |
| 144 | authors: resolveAuthors(result.output.authors, avatarMap), |
| 145 | }) |
| 146 | } |
| 147 |
no test coverage detected