(slug: string)
| 156 | } |
| 157 | |
| 158 | export async function getPostBySlug(slug: string): Promise<BlogPost> { |
| 159 | const meta = await scanFrontmatters() |
| 160 | const found = meta.find((m) => m.slug === slug) |
| 161 | if (!found) throw new Error(`Post not found: ${slug}`) |
| 162 | const mdxPath = path.join(BLOG_DIR, slug, 'index.mdx') |
| 163 | const raw = await fs.readFile(mdxPath, 'utf-8') |
| 164 | const { content, data } = matter(raw) |
| 165 | const fm = BlogFrontmatterSchema.parse(data) |
| 166 | |
| 167 | const postComponents = await loadPostComponents(slug) |
| 168 | const mergedComponents = { ...mdxComponents, ...postComponents } |
| 169 | |
| 170 | const compiled = await compileMDX({ |
| 171 | source: content, |
| 172 | components: mergedComponents as any, |
| 173 | options: { |
| 174 | parseFrontmatter: false, |
| 175 | mdxOptions: { |
| 176 | remarkPlugins: [remarkGfm], |
| 177 | rehypePlugins: [ |
| 178 | rehypeSlug, |
| 179 | [rehypeAutolinkHeadings, { behavior: 'wrap', properties: { className: 'anchor' } }], |
| 180 | ], |
| 181 | }, |
| 182 | }, |
| 183 | }) |
| 184 | const headings: { text: string; id: string }[] = [] |
| 185 | const lines = content.split('\n') |
| 186 | for (const line of lines) { |
| 187 | const match = /^##\s+(.+)$/.exec(line.trim()) |
| 188 | if (match) { |
| 189 | const text = match[1].trim() |
| 190 | headings.push({ text, id: slugify(text) }) |
| 191 | } |
| 192 | } |
| 193 | return { |
| 194 | ...found, |
| 195 | Content: () => (compiled as any).content, |
| 196 | updated: fm.updated ? toIsoDate(fm.updated) : found.updated, |
| 197 | headings, |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | export function invalidateBlogCaches() { |
| 202 | cachedMeta = null |
no test coverage detected